#include "effects/slitshift.hpp" #include #include #include #include "core/parallel.hpp" #include "effects/source_fill.hpp" namespace effector { const std::vector& SlitShiftEffect::params() const { static const std::vector 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& 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(std::max(0L, paramInt(values, "shift_max", 120))); const int bandHeight = static_cast(std::max(1L, paramInt(values, "shift_band_height", 16))); const int jitter = static_cast(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 offsets(static_cast(lines), 0); for (long band = 0; band < bands; ++band) { int thickness = bandHeight; if (jitter > 0) thickness += static_cast(rng.inRange(-jitter, jitter)); thickness = std::clamp(thickness, 1, lines); const int start = static_cast(rng.inRange(0, lines - thickness)); const int offset = static_cast(rng.inRange(-maxShift, maxShift)); for (int line = start; line < start + thickness; ++line) { offsets[static_cast(line)] += offset; } } Image canvas(width, height); parallelFor(static_cast(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(line) : i; const int y = vertical ? i : static_cast(line); std::uint8_t* out = canvas.row(y) + static_cast(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(fx) * 3, 3); } } }); return canvas; } } // namespace effector