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:
@@ -16,6 +16,7 @@
|
||||
#include "effects/tiles.hpp"
|
||||
#include "effects/tileswap.hpp"
|
||||
#include "effects/tracking.hpp"
|
||||
#include "effects/tunnel.hpp"
|
||||
#include "effects/warp.hpp"
|
||||
|
||||
namespace effector {
|
||||
@@ -41,6 +42,7 @@ const std::vector<EffectPtr>& effects() {
|
||||
// Video effects, offered only in a video project.
|
||||
std::make_shared<TrackingEffect>(),
|
||||
std::make_shared<TileSwapEffect>(),
|
||||
std::make_shared<TunnelEffect>(),
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
#include "effects/tunnel.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
#include "core/parallel.hpp"
|
||||
#include "effects/sampling.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
const std::vector<ParamSpec>& TunnelEffect::params() const {
|
||||
static const std::vector<ParamSpec> specs = {
|
||||
ParamSpec::makeInt("tunnel_count", "Echoes", 8, 2, 32,
|
||||
"How many echoes are nested inside the picture at once."),
|
||||
ParamSpec::makeDouble("tunnel_scale", "Scale", 0.8, 0.3, 0.95,
|
||||
"Size of each echo against the one around it.", 0.01),
|
||||
ParamSpec::makeDouble("tunnel_rotate", "Rotation", 6.0, -180.0, 180.0,
|
||||
"Turn in degrees of each echo against the one around it.", 0.5, 1),
|
||||
ParamSpec::makeInt("tunnel_drift", "Drift", 0, 0, 4096,
|
||||
"How far in pixels each echo sits off the centre of the one around "
|
||||
"it, all in one direction of the seed's own."),
|
||||
ParamSpec::makeDouble("tunnel_fade", "Opacity", 0.8, 0.0, 1.0,
|
||||
"How strongly each echo covers what lies beneath it."),
|
||||
ParamSpec::makeDouble("tunnel_speed", "Speed", 0.5, 0.0, 10.0,
|
||||
"Echoes passed per second, rounded so that the clip loops without "
|
||||
"a jump; 0 holds still.",
|
||||
0.1, 1),
|
||||
ParamSpec::makeChoice("tunnel_direction", "Direction", "in", {"in", "out"},
|
||||
"in flies into the tunnel, the echoes growing towards you; out "
|
||||
"backs away from it, the echoes shrinking into the distance."),
|
||||
};
|
||||
return specs;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct Settings {
|
||||
int count;
|
||||
double scale;
|
||||
double rotate;
|
||||
int drift;
|
||||
double fade;
|
||||
double speed;
|
||||
bool out;
|
||||
|
||||
explicit Settings(const ParamValues& values)
|
||||
: count(static_cast<int>(std::clamp(paramInt(values, "tunnel_count", 8), 2L, 32L))),
|
||||
scale(std::clamp(paramDouble(values, "tunnel_scale", 0.8), 0.3, 0.95)),
|
||||
rotate(std::clamp(paramDouble(values, "tunnel_rotate", 6.0), -180.0, 180.0)),
|
||||
drift(static_cast<int>(std::max(0L, paramInt(values, "tunnel_drift", 0)))),
|
||||
fade(std::clamp(paramDouble(values, "tunnel_fade", 0.8), 0.0, 1.0)),
|
||||
speed(std::max(0.0, paramDouble(values, "tunnel_speed", 0.5))),
|
||||
out(paramChoice(values, "tunnel_direction", "in") == "out") {}
|
||||
};
|
||||
|
||||
// One echo as it lies in this frame: how a pixel of the canvas finds the
|
||||
// pixel of the input it shows, and how strongly it covers what is beneath.
|
||||
struct Echo {
|
||||
double sine;
|
||||
double cosine;
|
||||
double shrink; // 1 / size
|
||||
double dx;
|
||||
double dy;
|
||||
int mix; // out of 256
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Image TunnelEffect::animate(Image input, const ParamValues& values, const Moment& at) const {
|
||||
const Settings settings(values);
|
||||
const int width = input.width();
|
||||
const int height = input.height();
|
||||
|
||||
// What stays put for the whole clip: the direction the echoes drift in.
|
||||
Rng steady = at.steady();
|
||||
double driftSine = 0.0;
|
||||
double driftCosine = 1.0;
|
||||
sinCosDegrees(unitDouble(steady.next()) * 360.0, driftSine, driftCosine);
|
||||
|
||||
// Where this frame is in time: how far each echo has moved on from its
|
||||
// place at the start of the step. Echo k sits at depth k + 1 - t flying
|
||||
// in, k + t backing out; at depth 0 it is the picture itself, so it can
|
||||
// melt into it, and a new one fades in at the far end as the old leaves.
|
||||
const double t = settings.speed > 0.0 ? at.cycle(settings.speed) : 0.0;
|
||||
std::vector<Echo> echoes;
|
||||
echoes.reserve(static_cast<std::size_t>(settings.count));
|
||||
for (int k = 0; k < settings.count; ++k) {
|
||||
const double depth = settings.out ? k + t : k + 1.0 - t;
|
||||
const double weight = settings.fade * std::clamp(depth, 0.0, 1.0) *
|
||||
std::clamp(settings.count - depth, 0.0, 1.0);
|
||||
const int mix = static_cast<int>(weight * 256.0 + 0.5);
|
||||
if (mix <= 0) continue;
|
||||
Echo echo{};
|
||||
sinCosDegrees(settings.rotate * depth, echo.sine, echo.cosine);
|
||||
echo.shrink = 1.0 / std::pow(settings.scale, depth);
|
||||
echo.dx = settings.drift * depth * driftCosine;
|
||||
echo.dy = settings.drift * depth * driftSine;
|
||||
echo.mix = mix;
|
||||
echoes.push_back(echo);
|
||||
}
|
||||
if (echoes.empty()) return input;
|
||||
|
||||
// The echoes are laid largest first, so the deeper ones nest inside.
|
||||
const double cx = (width - 1) * 0.5;
|
||||
const double cy = (height - 1) * 0.5;
|
||||
Image frame(width, height);
|
||||
parallelFor(static_cast<std::size_t>(height), [&](std::size_t begin, std::size_t end) {
|
||||
for (std::size_t row = begin; row < end; ++row) {
|
||||
const std::uint8_t* base = input.row(static_cast<int>(row));
|
||||
std::uint8_t* out = frame.row(static_cast<int>(row));
|
||||
for (int col = 0; col < width; ++col) {
|
||||
const std::size_t px = static_cast<std::size_t>(col) * 3;
|
||||
int colour[3] = {base[px], base[px + 1], base[px + 2]};
|
||||
for (const Echo& echo : echoes) {
|
||||
const double rx = col - cx - echo.dx;
|
||||
const double ry = static_cast<double>(row) - cy - echo.dy;
|
||||
const double sx = cx + (rx * echo.cosine + ry * echo.sine) * echo.shrink;
|
||||
const double sy = cy + (-rx * echo.sine + ry * echo.cosine) * echo.shrink;
|
||||
if (sx < 0.0 || sy < 0.0 || sx > width - 1 || sy > height - 1) continue;
|
||||
std::uint8_t echoed[3];
|
||||
sampleBilinear(input, sx, sy, echoed);
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
colour[c] += (echoed[c] - colour[c]) * echo.mix / 256;
|
||||
}
|
||||
}
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
out[px + static_cast<std::size_t>(c)] = static_cast<std::uint8_t>(colour[c]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return frame;
|
||||
}
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "effects/video_effect.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
// The feedback effect in motion: the picture nested inside itself, each copy
|
||||
// smaller, turned and drifted further than the one around it, and the whole
|
||||
// stack flying inwards (or outwards) without end.
|
||||
//
|
||||
// A copy sits at a depth, and its size, turn and drift follow from that depth
|
||||
// alone. Every frame moves each copy on by part of one step, so after a whole
|
||||
// cycle each copy lies exactly where its neighbour started: a new one fades
|
||||
// in at the far end while the nearest fades out into the picture itself, and
|
||||
// the clip loops without a jump.
|
||||
class TunnelEffect : public VideoEffect {
|
||||
public:
|
||||
std::string name() const override { return "tunnel"; }
|
||||
std::string title() const override { return "Echo tunnel"; }
|
||||
std::string description() const override {
|
||||
return "Nests the picture inside itself, as Feedback does, and flies through the echoes "
|
||||
"without end.";
|
||||
}
|
||||
const std::vector<ParamSpec>& params() const override;
|
||||
|
||||
protected:
|
||||
Image animate(Image input, const ParamValues& values, const Moment& at) const override;
|
||||
};
|
||||
|
||||
} // namespace effector
|
||||
Reference in New Issue
Block a user