#include "effects/tracking.hpp" #include #include #include #include #include #include "core/parallel.hpp" namespace effector { const std::vector& TrackingEffect::params() const { static const std::vector 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(std::max(0L, paramInt(values, "tracking_wobble", 4)))), wobbleSpeed(std::max(0.0, paramDouble(values, "tracking_wobble_speed", 0.5))), jitter(static_cast(std::max(0L, paramInt(values, "tracking_jitter", 2)))), band(static_cast(std::max(1L, paramInt(values, "tracking_band_height", 24)))), bar(static_cast(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(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 dx{}; std::array 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 phase{}; std::array 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 swayX{}; std::array 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 rows(static_cast(height)); std::vector tear(static_cast(height), 0); for (int top = 0; top < height; top += settings.band) { std::array jitterX{}; std::array jitterY{}; if (settings.jitter > 0) { for (std::size_t c = 0; c < 3; ++c) { jitterX[c] = static_cast(fresh.inRange(-settings.jitter, settings.jitter)); jitterY[c] = static_cast(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 apart = {1.0, 0.0, -1.0}; auto& row = rows[static_cast(y)]; for (std::size_t c = 0; c < 3; ++c) { row.dx[c] = static_cast(std::lround(swayX[c] + jitterX[c] + pull * apart[c])); row.dy[c] = static_cast(std::lround(swayY[c] + jitterY[c])); } tear[static_cast(y)] = static_cast(std::lround(pull / 2.0)); } } Image frame(width, height); parallelFor(static_cast(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(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(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(x) * 3 + c] = in[static_cast(sx) * 3 + c]; } } } }); return frame; } } // namespace effector