Add Strip roll, a video effect that rolls the picture in Scanlines' strips

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Si
2026-09-26 20:43:47 +02:00
co-authored by Claude Opus 5.5
parent a7cb2505d7
commit 4b407281e2
6 changed files with 275 additions and 0 deletions
+93
View File
@@ -2095,6 +2095,99 @@ int main() {
check(alone, "a tile swap clip's frame is exactly that frame rendered on its own");
}
// Strip roll builds its frames out of the sources too, and rolls the
// picture in each strip through its image.
{
const auto roll = effectByName("striproll");
check(roll->video() && roll->needsSources(),
"strip roll is a video effect built from the sources");
ChainStage rollStage = stage("striproll", {{"roll_height", 6L}, {"roll_speed", 40L}});
ChainStage scanStage = stage("scanlines");
effector::Rng withRoll(3);
effector::Rng withoutRoll(3);
check(sameBytes(renderChain({rollStage, scanStage}, sources, 80, 48, withRoll),
renderChain({scanStage}, sources, 80, 48, withoutRoll)),
"a still skips strip roll as though it were not there");
bool refused = false;
try {
effector::Rng rng(1);
renderChain({rollStage}, {}, 80, 48, rng, {}, FrameTime{0, 8, 8});
} catch (const EffectError&) {
refused = true;
}
check(refused, "strip roll with no sources is refused");
const std::vector<effector::Image> photos = {gradient(60, 40, 20), gradient(50, 30, 150)};
auto frameOf = [&](const ChainStage& at, int index) {
effector::Rng rng(11);
return renderChain({at}, photos, 80, 48, rng, {}, FrameTime{index, 8, 8});
};
effector::Rng rollA(9);
effector::Rng rollB(9);
roll->renderFrame(photos, 80, 48, rollA, rollStage.values, FrameTime{3, 8, 8});
roll->renderFrame(photos, 80, 48, rollB, rollStage.values, FrameTime{5, 8, 8});
check(rollA.next() == rollB.next(), "strip roll leaves the shared rng in one state");
// Every row of a strip is a row of one photograph, turned round.
auto rowsOfPhotos = [&](const effector::Image& image) {
for (int y = 0; y < image.height(); ++y) {
bool found = false;
for (const auto& photo : photos) {
for (int py = 0; !found && py < photo.height(); ++py) {
for (int shift = 0; !found && shift < photo.width(); ++shift) {
bool same = true;
for (int x = 0; same && x < image.width(); ++x) {
const int px = (x + shift) % photo.width();
same = std::equal(image.row(y) + x * 3, image.row(y) + x * 3 + 3,
photo.row(py) + px * 3);
}
found = same;
}
}
}
if (!found) return false;
}
return true;
};
check(rowsOfPhotos(frameOf(rollStage, 0)) && rowsOfPhotos(frameOf(rollStage, 5)),
"its strips are rows of the photographs");
bool rolled = false;
for (int index = 1; index < 8; ++index) {
rolled = rolled || !sameBytes(frameOf(rollStage, 0), frameOf(rollStage, index));
}
check(rolled, "its strips roll over the clip");
check(sameBytes(frameOf(rollStage, 0), frameOf(rollStage, 8)),
"and the frame after the last is the first again");
ChainStage sideways = rollStage;
sideways.values["roll_axis"] = std::string("horizontal");
sideways.values["roll_direction"] = std::string("random");
sideways.values["roll_keep_order"] = true;
check(rowsOfPhotos(frameOf(sideways, 3)) &&
!sameBytes(frameOf(sideways, 0), frameOf(sideways, 3)) &&
sameBytes(frameOf(sideways, 0), frameOf(sideways, 8)),
"sideways, its strips slide along their rows and loop");
ChainStage frozenRoll = rollStage;
frozenRoll.values["roll_speed"] = 0L;
check(sameBytes(frameOf(frozenRoll, 0), frameOf(frozenRoll, 5)),
"at a speed of 0 it holds still");
const std::vector<ChainStage> rollChain = {rollStage, stage("grain")};
std::vector<effector::Image> clip;
effector::renderFrames(rollChain, 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(rollChain, sources, 80, 48, rng,
{}, FrameTime{index, 8, 8}),
clip[static_cast<std::size_t>(index)]);
}
check(alone, "a strip roll 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