#include "effects/feedback.hpp" #include #include #include #include "core/parallel.hpp" #include "effects/sampling.hpp" #include "effects/source_fill.hpp" namespace effector { const std::vector& FeedbackEffect::params() const { static const std::vector 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& 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(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 echoes(static_cast(count)); for (auto& echo : echoes) { echo.dx = static_cast(rng.inRange(-drift, drift)); echo.dy = static_cast(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(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(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(row)); std::uint8_t* out = next.row(static_cast(row)); const double ry = static_cast(row) - cy - echo.dy; for (int col = 0; col < width; ++col) { const std::size_t at = static_cast(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(c)]; out[at + static_cast(c)] = static_cast(under + (echoed[c] - under) * mix / 256); } } } }); std::swap(frame, next); } return frame; } } // namespace effector