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>
100 lines
4.5 KiB
C++
100 lines
4.5 KiB
C++
#include "effects/slitshift.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
#include "core/parallel.hpp"
|
|
#include "effects/source_fill.hpp"
|
|
|
|
namespace effector {
|
|
|
|
const std::vector<ParamSpec>& SlitShiftEffect::params() const {
|
|
static const std::vector<ParamSpec> specs = {
|
|
ParamSpec::makeInt("shift_bands", "Bands", 24, 1, 4096,
|
|
"How many bands are torn out of place."),
|
|
ParamSpec::makeInt("shift_max", "Largest shift", 120, 0, 8192,
|
|
"Furthest in pixels a band may be pushed, either way."),
|
|
ParamSpec::makeInt("shift_band_height", "Band height", 16, 1, 2048,
|
|
"Thickness in pixels of each band."),
|
|
ParamSpec::makeInt("shift_height_jitter", "Height jitter", 12, 0, 2048,
|
|
"Random +/- variation added to the band thickness (0 = uniform)."),
|
|
ParamSpec::makeChoice("shift_edge", "Edges", "wrap", {"wrap", "smear", "black"},
|
|
"What fills the gap a band leaves: wrap brings in what was pushed "
|
|
"off the other side, smear stretches the last pixel, black leaves "
|
|
"it empty."),
|
|
ParamSpec::makeChoice("shift_axis", "Axis", "horizontal", {"horizontal", "vertical"},
|
|
"horizontal tears bands of rows sideways, vertical tears bands of "
|
|
"columns up and down."),
|
|
};
|
|
return specs;
|
|
}
|
|
|
|
namespace {
|
|
|
|
// Where the pixel at `index` of a line of `count` is read from once the line is
|
|
// pushed by `offset`, or -1 for an empty (black) pixel.
|
|
int shiftedIndex(int index, int offset, int count, char edge) {
|
|
const int from = index - offset;
|
|
if (from >= 0 && from < count) return from;
|
|
if (edge == 'w') return ((from % count) + count) % count;
|
|
if (edge == 's') return std::clamp(from, 0, count - 1);
|
|
return -1;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Image SlitShiftEffect::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 bands = std::max(1L, paramInt(values, "shift_bands", 24));
|
|
const int maxShift = static_cast<int>(std::max(0L, paramInt(values, "shift_max", 120)));
|
|
const int bandHeight = static_cast<int>(std::max(1L, paramInt(values, "shift_band_height", 16)));
|
|
const int jitter = static_cast<int>(std::max(0L, paramInt(values, "shift_height_jitter", 12)));
|
|
const char edge = paramChoice(values, "shift_edge", "wrap").front();
|
|
const bool vertical = paramChoice(values, "shift_axis", "horizontal") == "vertical";
|
|
|
|
const Image input = canvasFromSources(sources, width, height, rng);
|
|
|
|
// Every band is drawn up front, and overlapping bands simply add their
|
|
// pushes, so what the pixels need is one offset per line.
|
|
const int lines = vertical ? width : height;
|
|
const int span = vertical ? height : width;
|
|
std::vector<int> offsets(static_cast<std::size_t>(lines), 0);
|
|
for (long band = 0; band < bands; ++band) {
|
|
int thickness = bandHeight;
|
|
if (jitter > 0) thickness += static_cast<int>(rng.inRange(-jitter, jitter));
|
|
thickness = std::clamp(thickness, 1, lines);
|
|
const int start = static_cast<int>(rng.inRange(0, lines - thickness));
|
|
const int offset = static_cast<int>(rng.inRange(-maxShift, maxShift));
|
|
for (int line = start; line < start + thickness; ++line) {
|
|
offsets[static_cast<std::size_t>(line)] += offset;
|
|
}
|
|
}
|
|
|
|
Image canvas(width, height);
|
|
parallelFor(static_cast<std::size_t>(lines), [&](std::size_t begin, std::size_t end) {
|
|
for (std::size_t line = begin; line < end; ++line) {
|
|
const int offset = offsets[line];
|
|
for (int i = 0; i < span; ++i) {
|
|
const int from = shiftedIndex(i, offset, span, edge);
|
|
const int x = vertical ? static_cast<int>(line) : i;
|
|
const int y = vertical ? i : static_cast<int>(line);
|
|
std::uint8_t* out = canvas.row(y) + static_cast<std::size_t>(x) * 3;
|
|
if (from < 0) {
|
|
out[0] = out[1] = out[2] = 0;
|
|
continue;
|
|
}
|
|
const int fx = vertical ? x : from;
|
|
const int fy = vertical ? from : y;
|
|
std::memcpy(out, input.row(fy) + static_cast<std::size_t>(fx) * 3, 3);
|
|
}
|
|
}
|
|
});
|
|
|
|
return canvas;
|
|
}
|
|
|
|
} // namespace effector
|