Add Tile swap, a video effect built from the source images
Tile swap lays tiles cut from the photographs over the canvas as Tiles does, in a grid, in bricks or scattered, and swaps each one for another cut at a moment of its own, so the mosaic keeps changing piece by piece. The layout stays put for the whole clip; what a tile shows depends only on the tile and how many times it has swapped, counted round the clip, so it holds still between swaps and the clip loops without a jump. On the command line it is -e tileswap with --tileswap-rate and the rest. VideoEffect learns a second kind: an effect that answers needsSources() writes compose() instead of animate(), and the base then takes no crop, only the one seed, so it sees the photographs when it leads the chain and the stage before it otherwise. Transforms render exactly as before. The tile cutting and painting move out of tiles.cpp into tile_plan.hpp for both effects to share; Tiles renders byte for byte as before. Version 0.8.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1974,6 +1974,93 @@ int main() {
|
||||
check(small.width() == 100 && small.height() == 40, "a preview frame keeps its shape");
|
||||
check(big.scaledToFit(400).width() == 300, "a frame already small enough is left alone");
|
||||
|
||||
// Tile swap builds its frames out of the sources rather than
|
||||
// transforming a crop of one.
|
||||
{
|
||||
const auto swap = effectByName("tileswap");
|
||||
check(swap->video() && swap->needsSources(),
|
||||
"tile swap is a video effect built from the sources");
|
||||
ChainStage swapStage = stage("tileswap", {{"tileswap_width", 16L},
|
||||
{"tileswap_height", 12L},
|
||||
{"tileswap_rate", 2.0}});
|
||||
ChainStage scanStage = stage("scanlines");
|
||||
check(chainNeedsSources({swapStage}, true), "and a clip led by it needs sources");
|
||||
effector::Rng withSwap(3);
|
||||
effector::Rng withoutSwap(3);
|
||||
check(sameBytes(renderChain({swapStage, scanStage}, sources, 80, 48, withSwap),
|
||||
renderChain({scanStage}, sources, 80, 48, withoutSwap)),
|
||||
"a still skips tile swap as though it were not there");
|
||||
bool refused = false;
|
||||
try {
|
||||
effector::Rng rng(1);
|
||||
renderChain({swapStage}, {}, 80, 48, rng, {}, FrameTime{0, 8, 8});
|
||||
} catch (const EffectError&) {
|
||||
refused = true;
|
||||
}
|
||||
check(refused, "tile swap with no sources is refused");
|
||||
|
||||
const std::vector<effector::Image> photos = {solid(60, 40, 255, 0, 0),
|
||||
solid(50, 30, 0, 0, 255)};
|
||||
auto frameOf = [&](const ChainStage& at, int index, int count = 8) {
|
||||
effector::Rng rng(11);
|
||||
return renderChain({at}, photos, 80, 48, rng, {}, FrameTime{index, count, 8});
|
||||
};
|
||||
effector::Rng swapA(9);
|
||||
effector::Rng swapB(9);
|
||||
swap->renderFrame(photos, 80, 48, swapA, swapStage.values, FrameTime{3, 8, 8});
|
||||
swap->renderFrame(photos, 80, 48, swapB, swapStage.values, FrameTime{5, 8, 8});
|
||||
check(swapA.next() == swapB.next(), "tile swap leaves the shared rng in one state");
|
||||
|
||||
auto onlyPhotos = [](const effector::Image& image) {
|
||||
for (int y = 0; y < image.height(); ++y) {
|
||||
const std::uint8_t* row = image.row(y);
|
||||
for (int x = 0; x < image.width(); ++x) {
|
||||
const std::uint8_t* p = row + static_cast<std::size_t>(x) * 3;
|
||||
const bool red = p[0] == 255 && p[1] == 0 && p[2] == 0;
|
||||
const bool blue = p[0] == 0 && p[1] == 0 && p[2] == 255;
|
||||
if (!red && !blue) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
check(onlyPhotos(frameOf(swapStage, 0)) && onlyPhotos(frameOf(swapStage, 5)),
|
||||
"its tiles are cut from the photographs");
|
||||
bool swapped = false;
|
||||
for (int index = 1; index < 8; ++index) {
|
||||
swapped = swapped || !sameBytes(frameOf(swapStage, 0), frameOf(swapStage, index));
|
||||
}
|
||||
check(swapped, "its tiles change over the clip");
|
||||
check(sameBytes(frameOf(swapStage, 0), frameOf(swapStage, 8)),
|
||||
"and the frame after the last is the first again");
|
||||
ChainStage frozenSwap = swapStage;
|
||||
frozenSwap.values["tileswap_rate"] = 0.0;
|
||||
check(sameBytes(frameOf(frozenSwap, 0), frameOf(frozenSwap, 5)),
|
||||
"at a rate of 0 it holds still");
|
||||
ChainStage scattered = swapStage;
|
||||
scattered.values["tileswap_layout"] = std::string("scatter");
|
||||
ChainStage bricks = swapStage;
|
||||
bricks.values["tileswap_layout"] = std::string("brick");
|
||||
bricks.values["tileswap_keep_order"] = true;
|
||||
check(onlyPhotos(frameOf(scattered, 2)) && onlyPhotos(frameOf(bricks, 6)),
|
||||
"scattered and brick layouts are cut from the photographs too");
|
||||
|
||||
const std::vector<ChainStage> swapChain = {swapStage, stage("grain")};
|
||||
std::vector<effector::Image> clip;
|
||||
effector::renderFrames(swapChain, sources, 80, 48, 21, {}, video,
|
||||
[&](int, effector::Image&& frame) {
|
||||
clip.push_back(std::move(frame));
|
||||
return true;
|
||||
});
|
||||
bool alone = clip.size() == 8;
|
||||
for (const int index : {0, 4, 7}) {
|
||||
effector::Rng rng(21);
|
||||
alone = alone && sameBytes(effector::renderChain(swapChain, sources, 80, 48, rng,
|
||||
{}, FrameTime{index, 8, 8}),
|
||||
clip[static_cast<std::size_t>(index)]);
|
||||
}
|
||||
check(alone, "a tile swap clip's frame is exactly that frame rendered on its own");
|
||||
}
|
||||
|
||||
// The project file: a video says so, a still says nothing about time.
|
||||
effector::Project project;
|
||||
project.stages = {stage("tracking", wild)};
|
||||
|
||||
Reference in New Issue
Block a user