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>
57 lines
2.3 KiB
C++
57 lines
2.3 KiB
C++
#pragma once
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
#include "core/image.hpp"
|
|
#include "core/parallel.hpp"
|
|
#include "core/rng.hpp"
|
|
|
|
namespace effector {
|
|
|
|
// Copies `src` into the canvas starting at (sx, sy), repeating it as needed so
|
|
// that every pixel is covered even when the source is smaller than the canvas.
|
|
// Shared by the effects that do not build an image of their own but still want
|
|
// something to work on when they lead a chain.
|
|
inline void fillFromSource(Image& canvas, const Image& src, int sx, int sy) {
|
|
const int width = canvas.width();
|
|
const std::size_t total = static_cast<std::size_t>(width) * 3;
|
|
const int copyWidth = std::min(width, src.width() - sx);
|
|
const std::size_t copyBytes = static_cast<std::size_t>(copyWidth) * 3;
|
|
|
|
parallelFor(static_cast<std::size_t>(canvas.height()), [&](std::size_t begin, std::size_t end) {
|
|
for (std::size_t y = begin; y < end; ++y) {
|
|
const int srcY = sy + (static_cast<int>(y) % std::max(1, src.height() - sy));
|
|
const std::uint8_t* in = src.row(srcY) + static_cast<std::size_t>(sx) * 3;
|
|
std::uint8_t* out = canvas.row(static_cast<int>(y));
|
|
|
|
std::memcpy(out, in, copyBytes);
|
|
std::size_t filled = copyBytes;
|
|
while (filled < total) {
|
|
const std::size_t chunk = std::min(filled, total - filled);
|
|
std::memcpy(out + filled, out, chunk);
|
|
filled += chunk;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// The canvas a pure transform starts from: a crop of a randomly chosen source,
|
|
// or black when there is none. Inside a chain the only source is the stage
|
|
// before, which has the canvas size, so no random value is drawn at all; only a
|
|
// transform leading a chain with photographs loaded spends any of the rng here.
|
|
inline Image canvasFromSources(const std::vector<Image>& sources, int width, int height, Rng& rng) {
|
|
Image canvas(width, height);
|
|
if (!sources.empty()) {
|
|
const std::size_t index = rng.below(sources.size() - 1);
|
|
const Image& src = sources[index];
|
|
const int sx = static_cast<int>(rng.inRange(0, std::max(0, src.width() - width)));
|
|
const int sy = static_cast<int>(rng.inRange(0, std::max(0, src.height() - height)));
|
|
fillFromSource(canvas, src, sx, sy);
|
|
}
|
|
return canvas;
|
|
}
|
|
|
|
} // namespace effector
|