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:
@@ -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)};
|
||||
|
||||
Reference in New Issue
Block a user