Add Datamosh, a video effect whose Corrupt blocks break and heal over the clip

Corrupt's block damage moves to effects/corrupt_blocks.hpp so both share it;
Corrupt renders exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Si
2026-09-26 20:41:52 +02:00
co-authored by Claude Opus 5.5
parent ce0207c41f
commit a7cb2505d7
8 changed files with 273 additions and 75 deletions
+135
View File
@@ -0,0 +1,135 @@
#include "effects/datamosh.hpp"
#include <algorithm>
#include <cmath>
#include <string>
#include <vector>
#include "effects/corrupt_blocks.hpp"
#include "effects/sampling.hpp"
namespace effector {
const std::vector<ParamSpec>& DatamoshEffect::params() const {
static const std::vector<ParamSpec> specs = {
ParamSpec::makeInt("mosh_block", "Block size", 16, 2, 256,
"Width and height in pixels of one block, as a codec would cut them."),
ParamSpec::makeDouble("mosh_rate", "Rate", 0.1, 0.0, 1.0,
"Chance of each block breaking at some moment of the cycle."),
ParamSpec::makeChoice("mosh_kinds", "Damage", "mixed",
{"mixed", "repeat", "displace", "colour"},
"repeat freezes a block into a copy of the one to its left, "
"displace pulls one in from elsewhere in the image, colour knocks "
"it off colour; mixed picks one of them for every block."),
ParamSpec::makeDouble("mosh_hold", "Hold", 0.3, 0.01, 1.0,
"How much of each cycle a broken block stays broken, give or take "
"half; 1 keeps it broken throughout."),
ParamSpec::makeInt("mosh_drift", "Drift", 24, 0, 4096,
"How far in pixels a displaced block's picture slides while it "
"shows."),
ParamSpec::makeDouble("mosh_speed", "Speed", 0.5, 0.0, 10.0,
"Cycles per second, rounded so that the clip loops without a jump; "
"0 holds still.",
0.05, 2),
ParamSpec::makeBool("mosh_bleed", "Bleed", false,
"Carry each broken block's fault on to the end of its row, the way "
"one broken bit spoils the rest of a stream."),
};
return specs;
}
namespace {
struct Settings {
int size;
double rate;
std::string kinds;
double hold;
int drift;
double speed;
bool bleed;
explicit Settings(const ParamValues& values)
: size(static_cast<int>(std::max(2L, paramInt(values, "mosh_block", 16)))),
rate(std::clamp(paramDouble(values, "mosh_rate", 0.1), 0.0, 1.0)),
kinds(paramChoice(values, "mosh_kinds", "mixed")),
hold(std::clamp(paramDouble(values, "mosh_hold", 0.3), 0.01, 1.0)),
drift(static_cast<int>(std::max(0L, paramInt(values, "mosh_drift", 24)))),
speed(std::max(0.0, paramDouble(values, "mosh_speed", 0.5))),
bleed(paramBool(values, "mosh_bleed", false)) {}
};
// A block that breaks at some moment of the cycle: how, from when, for how
// long (both as parts of a cycle), and which way a displaced one smears.
struct Break {
Block block;
double start = 0.0;
double length = 0.0;
double driftX = 0.0;
double driftY = 0.0;
};
} // namespace
Image DatamoshEffect::animate(Image input, const ParamValues& values, const Moment& at) const {
const Settings settings(values);
const int width = input.width();
const int height = input.height();
const int size = settings.size;
const int cols = (width + size - 1) / size;
const int rows = (height + size - 1) / size;
// What stays put for the whole clip: the whole map of breaks, block by
// block in reading order, as Corrupt draws its damage.
Rng steady = at.steady();
std::vector<Break> breaks(static_cast<std::size_t>(cols) * rows);
bool any = false;
for (int by = 0; by < rows; ++by) {
const Break* carried = nullptr;
for (int bx = 0; bx < cols; ++bx) {
Break& piece = breaks[static_cast<std::size_t>(by) * cols + bx];
if (carried != nullptr) {
piece = *carried;
// A displaced run keeps moving along with the row, so the
// stretch reads as one shifted strip.
if (piece.block.damage == Damage::Displace) piece.block.sx += size;
carried = &piece;
continue;
}
if (!chance(steady.next(), settings.rate)) continue;
piece.block = drawDamage(steady, settings.kinds, bx, width, height, size);
piece.start = unitDouble(steady.next());
piece.length = std::min(1.0, settings.hold * (0.5 + unitDouble(steady.next())));
sinCosDegrees(unitDouble(steady.next()) * 360.0, piece.driftY, piece.driftX);
if (settings.bleed) carried = &piece;
any = true;
}
}
if (!any) return input;
// Where this frame is in time: which blocks are broken, and how far
// through its stretch each displaced one is.
const double t = settings.speed > 0.0 ? at.cycle(settings.speed) : 0.0;
std::vector<Block> plan(breaks.size());
for (std::size_t i = 0; i < breaks.size(); ++i) {
const Break& piece = breaks[i];
if (piece.block.damage == Damage::None) continue;
const double into = t - piece.start - std::floor(t - piece.start);
if (piece.length < 1.0 && into >= piece.length) continue;
Block& block = plan[i];
block = piece.block;
if (block.damage == Damage::Displace && settings.drift > 0) {
const double slide = settings.drift * into / piece.length;
const int sx = block.sx + static_cast<int>(std::lround(slide * piece.driftX));
block.sx = (sx % width + width) % width;
block.sy = std::clamp(block.sy + static_cast<int>(std::lround(slide * piece.driftY)), 0,
height - 1);
}
}
Image canvas = input;
paintBlocks(canvas, input, plan, cols, rows, size);
return canvas;
}
} // namespace effector