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:
@@ -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
|
||||
Reference in New Issue
Block a user