Split effects into image and video effects, with VHS tracking as the first video effect
Effect::video() says which kind an effect is. The Add effect menu lists the two kinds in sections of their own, each scrolling on its own, and offers video effects only in a video project; a video stage carries a film icon in the chain and, in a still, is dimmed and skipped like a stage switched off. enabledStages takes whether the render is a video and is the one place that leaves video stages out of a still. --help groups the video effects under a heading of their own, --list-effects marks them, and a video effect given for a still is skipped with a warning. VideoEffect is the base every video effect builds on, and keeps the rules a moving stage has to follow: it takes one value from the shared rng, the same on every frame, and hands the effect steady and per-frame generators and loop-rounded time through a Moment, so a subclass writes only animate(). VHS tracking is the first, and the one to copy: the colour planes sway and shake and a tracking bar rolls down the picture. That motion moves out of channel split, which is an image effect again and renders stills exactly as before; forVideo() goes with it. Version 0.7.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
#include "effects/tracking.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <numbers>
|
||||
#include <vector>
|
||||
|
||||
#include "core/parallel.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
const std::vector<ParamSpec>& TrackingEffect::params() const {
|
||||
static const std::vector<ParamSpec> specs = {
|
||||
ParamSpec::makeInt("tracking_wobble", "Wobble", 4, 0, 256,
|
||||
"How far in pixels the colour planes sway apart and back."),
|
||||
ParamSpec::makeDouble("tracking_wobble_speed", "Wobble speed", 0.5, 0.0, 10.0,
|
||||
"Sways per second, rounded so that the clip loops without a jump.",
|
||||
0.1, 1),
|
||||
ParamSpec::makeInt("tracking_jitter", "Jitter", 2, 0, 256,
|
||||
"Furthest in pixels a band shakes, anew on every frame."),
|
||||
ParamSpec::makeInt("tracking_band_height", "Band height", 24, 1, 2048,
|
||||
"Thickness in pixels of the bands that shake on their own."),
|
||||
ParamSpec::makeInt("tracking_bar", "Tracking bar", 16, 0, 512,
|
||||
"How far in pixels the rolling bar tears the rows sideways and the "
|
||||
"colour planes apart; 0 for none."),
|
||||
ParamSpec::makeDouble("tracking_bar_time", "Bar pass", 4.0, 0.5, 60.0,
|
||||
"Seconds the tracking bar takes from top to bottom, rounded so "
|
||||
"that the clip loops without a jump.",
|
||||
0.5, 1),
|
||||
};
|
||||
return specs;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct Settings {
|
||||
int wobble;
|
||||
double wobbleSpeed;
|
||||
int jitter;
|
||||
int band;
|
||||
int bar;
|
||||
double barTime;
|
||||
|
||||
explicit Settings(const ParamValues& values)
|
||||
: wobble(static_cast<int>(std::max(0L, paramInt(values, "tracking_wobble", 4)))),
|
||||
wobbleSpeed(std::max(0.0, paramDouble(values, "tracking_wobble_speed", 0.5))),
|
||||
jitter(static_cast<int>(std::max(0L, paramInt(values, "tracking_jitter", 2)))),
|
||||
band(static_cast<int>(std::max(1L, paramInt(values, "tracking_band_height", 24)))),
|
||||
bar(static_cast<int>(std::max(0L, paramInt(values, "tracking_bar", 16)))),
|
||||
barTime(std::max(0.5, paramDouble(values, "tracking_bar_time", 4.0))) {}
|
||||
};
|
||||
|
||||
double unit(Rng& rng) { return static_cast<double>(rng.next() >> 11) * 0x1.0p-53; }
|
||||
|
||||
// How far each of the red, green and blue planes is moved on one row.
|
||||
struct RowShift {
|
||||
std::array<int, 3> dx{};
|
||||
std::array<int, 3> dy{};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Image TrackingEffect::animate(Image input, const ParamValues& values, const Moment& at) const {
|
||||
const Settings settings(values);
|
||||
const int width = input.width();
|
||||
const int height = input.height();
|
||||
constexpr double tau = 2.0 * std::numbers::pi;
|
||||
|
||||
// What stays put for the whole clip: where each plane's sway starts, and
|
||||
// the direction it sways in.
|
||||
Rng steady = at.steady();
|
||||
std::array<double, 3> phase{};
|
||||
std::array<double, 3> angle{};
|
||||
for (std::size_t c = 0; c < 3; ++c) {
|
||||
phase[c] = unit(steady) * tau;
|
||||
angle[c] = unit(steady) * tau;
|
||||
}
|
||||
|
||||
// Where this frame is in time: the sway of each plane, and the bar -- an
|
||||
// eighth of the picture, soft at both edges, entering above the top and
|
||||
// leaving below the bottom.
|
||||
std::array<double, 3> swayX{};
|
||||
std::array<double, 3> swayY{};
|
||||
if (settings.wobble > 0 && settings.wobbleSpeed > 0.0) {
|
||||
const double turn = at.cycle(settings.wobbleSpeed) * tau;
|
||||
for (std::size_t c = 0; c < 3; ++c) {
|
||||
const double sway = settings.wobble * std::sin(turn + phase[c]);
|
||||
swayX[c] = sway * std::cos(angle[c]);
|
||||
swayY[c] = sway * std::sin(angle[c]);
|
||||
}
|
||||
}
|
||||
const double barHeight = std::max(8.0, height / 8.0);
|
||||
const double barTop = -barHeight + at.cycle(1.0 / settings.barTime) * (height + barHeight);
|
||||
|
||||
// What changes on every frame: each band's shake, mostly sideways, as a
|
||||
// picture losing its horizontal hold. All of it drawn, band by band,
|
||||
// before a pixel is touched.
|
||||
Rng fresh = at.fresh();
|
||||
std::vector<RowShift> rows(static_cast<std::size_t>(height));
|
||||
std::vector<int> tear(static_cast<std::size_t>(height), 0);
|
||||
for (int top = 0; top < height; top += settings.band) {
|
||||
std::array<double, 3> jitterX{};
|
||||
std::array<double, 3> jitterY{};
|
||||
if (settings.jitter > 0) {
|
||||
for (std::size_t c = 0; c < 3; ++c) {
|
||||
jitterX[c] = static_cast<double>(fresh.inRange(-settings.jitter, settings.jitter));
|
||||
jitterY[c] = static_cast<double>(fresh.inRange(-settings.jitter, settings.jitter)) / 3.0;
|
||||
}
|
||||
}
|
||||
const int end = std::min(height, top + settings.band);
|
||||
for (int y = top; y < end; ++y) {
|
||||
// The bar pulls red one way and blue the other, and drags the
|
||||
// whole row half as far.
|
||||
double pull = 0.0;
|
||||
if (settings.bar > 0 && y >= barTop && y < barTop + barHeight) {
|
||||
pull = settings.bar * std::sin(std::numbers::pi * (y - barTop) / barHeight);
|
||||
}
|
||||
constexpr std::array<double, 3> apart = {1.0, 0.0, -1.0};
|
||||
auto& row = rows[static_cast<std::size_t>(y)];
|
||||
for (std::size_t c = 0; c < 3; ++c) {
|
||||
row.dx[c] = static_cast<int>(std::lround(swayX[c] + jitterX[c] + pull * apart[c]));
|
||||
row.dy[c] = static_cast<int>(std::lround(swayY[c] + jitterY[c]));
|
||||
}
|
||||
tear[static_cast<std::size_t>(y)] = static_cast<int>(std::lround(pull / 2.0));
|
||||
}
|
||||
}
|
||||
|
||||
Image frame(width, height);
|
||||
parallelFor(static_cast<std::size_t>(height), [&](std::size_t begin, std::size_t end) {
|
||||
for (std::size_t y = begin; y < end; ++y) {
|
||||
std::uint8_t* out = frame.row(static_cast<int>(y));
|
||||
for (std::size_t c = 0; c < 3; ++c) {
|
||||
const int dx = rows[y].dx[c] + tear[y];
|
||||
const int sy = std::clamp(static_cast<int>(y) - rows[y].dy[c], 0, height - 1);
|
||||
const std::uint8_t* in = input.row(sy);
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const int sx = std::clamp(x - dx, 0, width - 1);
|
||||
out[static_cast<std::size_t>(x) * 3 + c] = in[static_cast<std::size_t>(sx) * 3 + c];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return frame;
|
||||
}
|
||||
|
||||
} // namespace effector
|
||||
Reference in New Issue
Block a user