Add Echo tunnel, a video effect that flies through Feedback's nested echoes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Si
2026-09-26 20:32:49 +02:00
co-authored by Claude Opus 5.5
parent 8537a3cf0a
commit 370d3e550e
6 changed files with 249 additions and 0 deletions
+75
View File
@@ -2095,6 +2095,81 @@ int main() {
check(alone, "a tile swap clip's frame is exactly that frame rendered on its own");
}
// The video transforms made from image effects. Each gets a setting
// that moves plenty, one without its per-frame noise (which, drawn
// anew on every frame, is the one thing that does not loop), and one
// that switches it off.
struct Moving {
std::string name;
effector::ParamValues lively;
effector::ParamValues smooth;
effector::ParamValues off;
};
const std::vector<Moving> moving = {
{"tunnel", {{"tunnel_drift", 6L}, {"tunnel_fade", 1.0}}, {}, {{"tunnel_fade", 0.0}}},
};
for (const auto& [name, lively, smooth, off] : moving) {
const auto effect = effectByName(name);
auto with = [&](const effector::ParamValues& changes) {
ChainStage built = stage(name);
for (const auto& [key, value] : changes) built.values[key] = value;
return built;
};
auto frameOf = [&](const ChainStage& at, int index) {
effector::Rng rng(6);
return renderChain({at}, input, 96, 64, rng, {}, FrameTime{index, 8, 8});
};
const ChainStage wildStage = with(lively);
check(effect->video() && !effect->needsSources(),
name + " is a video effect that needs no sources");
ChainStage scanStage = stage("scanlines");
effector::Rng withIt(3);
effector::Rng withoutIt(3);
check(sameBytes(renderChain({scanStage, wildStage}, sources, 80, 48, withIt),
renderChain({scanStage}, sources, 80, 48, withoutIt)),
"a still skips " + name + " as though it were not there");
effector::Rng stateA(9);
effector::Rng stateB(9);
effect->renderFrame(sources, 80, 48, stateA, wildStage.values, FrameTime{3, 8, 8});
effect->renderFrame(sources, 80, 48, stateB, wildStage.values, FrameTime{5, 8, 8});
check(stateA.next() == stateB.next(), name + " leaves the shared rng in one state");
bool changes = false;
for (int index = 1; index < 8; ++index) {
changes = changes || !sameBytes(frameOf(wildStage, 0), frameOf(wildStage, index));
}
check(changes, name + " moves over the clip");
const ChainStage smoothStage = with(smooth);
check(sameBytes(frameOf(smoothStage, 0), frameOf(smoothStage, 8)),
name + " loops: the frame after the last is the first again");
check(sameBytes(frameOf(with(off), 5), input[0]),
name + " switched off leaves its input alone");
effector::Rng sliverA(2);
effector::Rng sliverB(2);
check(renderChain({wildStage}, input, 1, 57, sliverA, {}, FrameTime{3, 8, 8}).height() == 57 &&
renderChain({wildStage}, input, 57, 1, sliverB, {}, FrameTime{3, 8, 8}).width() == 57,
name + " copes with a sliver of a canvas");
const std::vector<ChainStage> movingChain = {stage("tiles"), wildStage, stage("grain")};
std::vector<effector::Image> clip;
effector::renderFrames(movingChain, sources, 80, 48, 21, {}, video,
[&](int, effector::Image&& frame) {
clip.push_back(std::move(frame));
return true;
});
bool exactFrames = clip.size() == 8;
for (const int index : {0, 5, 7}) {
effector::Rng rng(21);
exactFrames = exactFrames &&
sameBytes(effector::renderChain(movingChain, sources, 80, 48, rng, {},
FrameTime{index, 8, 8}),
clip[static_cast<std::size_t>(index)]);
}
check(exactFrames, "a " + name + " 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)};