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>
136 lines
5.7 KiB
C++
136 lines
5.7 KiB
C++
#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
|