Files
effector/src/effects/feedback.cpp
T
SiandClaude Opus 5.5 ce3a422f6a Add eight effects that throw something chaotic into the chain
Slit shift, channel split, pixel sort, corruption, shatter, warp, bit
crush and feedback: tearing bands sideways, knocking the colour planes
out of register, sorting runs of pixels, damaging blocks the way a broken
stream does, breaking the picture into shards, pushing it along a noise
field, mangling its bytes and laying it over itself again and again.

All eight are pure transforms. None needs sources, so any of them can
lead a chain, and like blur they start from a crop of a random source
when they do and photographs are loaded. canvasFromSources holds that
start once for them.

Each draws its whole plan from the rng before touching a pixel, and
pixel sort hands every line a seed of its own, so the parallel work
never decides anything. The rotations in shatter and feedback take their
sine and cosine from a short series of plain multiplications rather than
libm, so a seed lands on the same pixels whatever the machine's maths
library makes of std::sin.

The tests hold every one of them to the same things -- a seed
reproduces it, it runs without sources and as a later stage, a sliver
of a canvas does not trip it, turned right down it leaves its input
alone -- plus a check of what each actually does, and the registry now
checks that no two effects declare the same param key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:36:09 +02:00

104 lines
4.4 KiB
C++

#include "effects/feedback.hpp"
#include <algorithm>
#include <cmath>
#include <vector>
#include "core/parallel.hpp"
#include "effects/sampling.hpp"
#include "effects/source_fill.hpp"
namespace effector {
const std::vector<ParamSpec>& FeedbackEffect::params() const {
static const std::vector<ParamSpec> specs = {
ParamSpec::makeInt("echo_count", "Echoes", 6, 1, 64,
"How many times the image is laid back over itself."),
ParamSpec::makeDouble("echo_scale", "Scale", 0.85, 0.1, 2.0,
"Size of each echo against the one before; below 1 the echoes "
"tunnel inwards, above 1 they burst outwards.",
0.01),
ParamSpec::makeDouble("echo_rotate", "Rotation", 4.0, -180.0, 180.0,
"Turn in degrees of each echo against the one before.", 0.5, 1),
ParamSpec::makeInt("echo_drift", "Drift", 20, 0, 4096,
"Furthest in pixels each echo may wander off centre, either way."),
ParamSpec::makeDouble("echo_fade", "Opacity", 0.8, 0.0, 1.0,
"How strongly each echo covers what lies beneath it."),
};
return specs;
}
namespace {
struct Echo {
double dx = 0.0;
double dy = 0.0;
};
} // namespace
Image FeedbackEffect::render(const std::vector<Image>& sources, int width, int height, Rng& rng,
const ParamValues& values) const {
if (width < 1 || height < 1) throw EffectError("Canvas size must be at least 1x1 pixels.");
const long count = std::clamp(paramInt(values, "echo_count", 6), 1L, 64L);
const double scale = std::clamp(paramDouble(values, "echo_scale", 0.85), 0.1, 2.0);
const double rotate = std::clamp(paramDouble(values, "echo_rotate", 4.0), -180.0, 180.0);
const int drift = static_cast<int>(std::max(0L, paramInt(values, "echo_drift", 20)));
const double fade = std::clamp(paramDouble(values, "echo_fade", 0.8), 0.0, 1.0);
const Image input = canvasFromSources(sources, width, height, rng);
std::vector<Echo> echoes(static_cast<std::size_t>(count));
for (auto& echo : echoes) {
echo.dx = static_cast<double>(rng.inRange(-drift, drift));
echo.dy = static_cast<double>(rng.inRange(-drift, drift));
}
double sine = 0.0;
double cosine = 1.0;
sinCosDegrees(rotate, sine, cosine);
const double cx = (width - 1) * 0.5;
const double cy = (height - 1) * 0.5;
const int mix = static_cast<int>(fade * 256.0 + 0.5);
// A camera filming its own monitor: every pass lays the whole picture so
// far, shrunk (or grown), turned and drifted, over the untouched input, so
// the older echoes nest inside the newer ones and fade as they go.
Image frame = input;
Image next(width, height);
for (const Echo& echo : echoes) {
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 = next.row(static_cast<int>(row));
const double ry = static_cast<double>(row) - cy - echo.dy;
for (int col = 0; col < width; ++col) {
const std::size_t at = static_cast<std::size_t>(col) * 3;
const double rx = col - cx - echo.dx;
const double sx = cx + (rx * cosine + ry * sine) / scale;
const double sy = cy + (-rx * sine + ry * cosine) / scale;
if (sx < 0.0 || sy < 0.0 || sx > width - 1 || sy > height - 1) {
out[at] = base[at];
out[at + 1] = base[at + 1];
out[at + 2] = base[at + 2];
continue;
}
std::uint8_t echoed[3];
sampleBilinear(frame, sx, sy, echoed);
for (int c = 0; c < 3; ++c) {
const int under = base[at + static_cast<std::size_t>(c)];
out[at + static_cast<std::size_t>(c)] =
static_cast<std::uint8_t>(under + (echoed[c] - under) * mix / 256);
}
}
}
});
std::swap(frame, next);
}
return frame;
}
} // namespace effector