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
+143
View File
@@ -0,0 +1,143 @@
#include "effects/striproll.hpp"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
#include "core/parallel.hpp"
namespace effector {
const std::vector<ParamSpec>& StripRollEffect::params() const {
static const std::vector<ParamSpec> specs = {
ParamSpec::makeInt("roll_height", "Strip height", 24, 1, 512,
"Height in pixels of each strip."),
ParamSpec::makeInt("roll_jitter", "Height jitter", 0, 0, 256,
"Random +/- variation added to the strip height (0 = uniform)."),
ParamSpec::makeBool("roll_keep_order", "Cycle images in order", false,
"Take strips from the images in order instead of picking randomly."),
ParamSpec::makeChoice("roll_axis", "Axis", "vertical", {"vertical", "horizontal"},
"vertical rolls the picture in each strip up or down through its "
"image; horizontal slides it sideways."),
ParamSpec::makeChoice("roll_direction", "Direction", "alternate",
{"alternate", "same", "random"},
"alternate rolls every other strip the other way; same rolls them "
"all one way; random picks for each strip."),
ParamSpec::makeInt("roll_speed", "Speed", 120, 0, 8192,
"Pixels per second the picture rolls, rounded so that each strip goes "
"round its image a whole number of times in the clip; 0 holds still."),
};
return specs;
}
namespace {
struct Settings {
int stripHeight;
int jitter;
bool keepOrder;
bool horizontal;
std::string direction;
int speed;
explicit Settings(const ParamValues& values)
: stripHeight(static_cast<int>(std::max(1L, paramInt(values, "roll_height", 24)))),
jitter(static_cast<int>(std::max(0L, paramInt(values, "roll_jitter", 0)))),
keepOrder(paramBool(values, "roll_keep_order", false)),
horizontal(paramChoice(values, "roll_axis", "vertical") == "horizontal"),
direction(paramChoice(values, "roll_direction", "alternate")),
speed(static_cast<int>(std::max(0L, paramInt(values, "roll_speed", 120)))) {}
};
// One strip of the canvas: where it lies, the image it shows, where in that
// image its picture starts, and which way it rolls.
struct Strip {
int dstY;
int height;
std::size_t sourceIndex;
int sx;
int sy;
int way; // +1 or -1
};
int wrap(long value, int span) { return static_cast<int>((value % span + span) % span); }
} // namespace
Image StripRollEffect::compose(const std::vector<Image>& sources, int width, int height,
const ParamValues& values, const Moment& at) const {
const Settings settings(values);
// What stays put for the whole clip: the strips, drawn as Scanlines draws
// them, and the way each one rolls.
Rng steady = at.steady();
std::vector<Strip> strips;
strips.reserve(static_cast<std::size_t>(height / settings.stripHeight) + 8);
for (int y = 0; y < height;) {
Strip strip{};
strip.dstY = y;
strip.sourceIndex = settings.keepOrder ? strips.size() % sources.size()
: steady.below(sources.size() - 1);
const Image& src = sources[strip.sourceIndex];
int h = settings.stripHeight;
if (settings.jitter > 0) {
h += static_cast<int>(steady.inRange(-settings.jitter, settings.jitter));
}
strip.height = std::max(1, std::min(h, height - y));
strip.sy = static_cast<int>(steady.inRange(0, src.height() - 1));
strip.sx = static_cast<int>(steady.inRange(0, src.width() - 1));
if (settings.direction == "random") {
strip.way = (steady.next() >> 63) != 0 ? 1 : -1;
} else if (settings.direction == "alternate") {
strip.way = strips.size() % 2 == 0 ? 1 : -1;
} else {
strip.way = 1;
}
strips.push_back(strip);
y += strip.height;
}
// Where this frame is in time: how far round its image each strip has
// rolled, a whole number of rounds in the clip, the nearest to the speed.
std::vector<long> offsets(strips.size(), 0);
if (settings.speed > 0) {
for (std::size_t i = 0; i < strips.size(); ++i) {
const Image& src = sources[strips[i].sourceIndex];
const int span = settings.horizontal ? src.width() : src.height();
const double rounds = std::max(1.0, std::round(settings.speed * at.length() / span));
const double into = std::fmod(at.seconds() * rounds / at.length(), 1.0);
offsets[i] = strips[i].way * std::lround(into * span);
}
}
Image canvas(width, height);
parallelFor(strips.size(), [&](std::size_t begin, std::size_t end) {
for (std::size_t i = begin; i < end; ++i) {
const Strip& strip = strips[i];
const Image& src = sources[strip.sourceIndex];
const long down = settings.horizontal ? 0 : offsets[i];
const long across = settings.horizontal ? offsets[i] : 0;
const int startX = wrap(strip.sx + across, src.width());
for (int line = 0; line < strip.height; ++line) {
const std::uint8_t* in = src.row(wrap(strip.sy + line + down, src.height()));
std::uint8_t* out = canvas.row(strip.dstY + line);
// Round the image's width as often as the canvas needs.
int x = 0;
int from = startX;
while (x < width) {
const int run = std::min(width - x, src.width() - from);
std::memcpy(out + static_cast<std::size_t>(x) * 3,
in + static_cast<std::size_t>(from) * 3,
static_cast<std::size_t>(run) * 3);
x += run;
from = 0;
}
}
}
});
return canvas;
}
} // namespace effector