Merge branch 'chaos-effects'
This commit is contained in:
@@ -32,13 +32,21 @@ add_library(effector_core STATIC
|
|||||||
src/core/registry.cpp
|
src/core/registry.cpp
|
||||||
src/core/rng.cpp
|
src/core/rng.cpp
|
||||||
src/core/style.cpp
|
src/core/style.cpp
|
||||||
|
src/effects/bitcrush.cpp
|
||||||
src/effects/blur.cpp
|
src/effects/blur.cpp
|
||||||
|
src/effects/channelsplit.cpp
|
||||||
|
src/effects/corrupt.cpp
|
||||||
|
src/effects/feedback.cpp
|
||||||
src/effects/grain.cpp
|
src/effects/grain.cpp
|
||||||
src/effects/monochrome.cpp
|
src/effects/monochrome.cpp
|
||||||
src/effects/overlay.cpp
|
src/effects/overlay.cpp
|
||||||
|
src/effects/pixelsort.cpp
|
||||||
src/effects/scanlines.cpp
|
src/effects/scanlines.cpp
|
||||||
|
src/effects/shatter.cpp
|
||||||
|
src/effects/slitshift.cpp
|
||||||
src/effects/text.cpp
|
src/effects/text.cpp
|
||||||
src/effects/tiles.cpp
|
src/effects/tiles.cpp
|
||||||
|
src/effects/warp.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(effector_core PUBLIC src)
|
target_include_directories(effector_core PUBLIC src)
|
||||||
# The core is GUI-free: it only needs gdk-pixbuf for decoding and encoding,
|
# The core is GUI-free: it only needs gdk-pixbuf for decoding and encoding,
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
#include "core/registry.hpp"
|
#include "core/registry.hpp"
|
||||||
|
|
||||||
|
#include "effects/bitcrush.hpp"
|
||||||
#include "effects/blur.hpp"
|
#include "effects/blur.hpp"
|
||||||
|
#include "effects/channelsplit.hpp"
|
||||||
|
#include "effects/corrupt.hpp"
|
||||||
|
#include "effects/feedback.hpp"
|
||||||
#include "effects/grain.hpp"
|
#include "effects/grain.hpp"
|
||||||
#include "effects/monochrome.hpp"
|
#include "effects/monochrome.hpp"
|
||||||
#include "effects/overlay.hpp"
|
#include "effects/overlay.hpp"
|
||||||
|
#include "effects/pixelsort.hpp"
|
||||||
#include "effects/scanlines.hpp"
|
#include "effects/scanlines.hpp"
|
||||||
|
#include "effects/shatter.hpp"
|
||||||
|
#include "effects/slitshift.hpp"
|
||||||
#include "effects/text.hpp"
|
#include "effects/text.hpp"
|
||||||
#include "effects/tiles.hpp"
|
#include "effects/tiles.hpp"
|
||||||
|
#include "effects/warp.hpp"
|
||||||
|
|
||||||
namespace effector {
|
namespace effector {
|
||||||
|
|
||||||
@@ -20,6 +28,14 @@ const std::vector<EffectPtr>& effects() {
|
|||||||
std::make_shared<OverlayEffect>(),
|
std::make_shared<OverlayEffect>(),
|
||||||
std::make_shared<TextEffect>(),
|
std::make_shared<TextEffect>(),
|
||||||
std::make_shared<TilesEffect>(),
|
std::make_shared<TilesEffect>(),
|
||||||
|
std::make_shared<SlitShiftEffect>(),
|
||||||
|
std::make_shared<ChannelSplitEffect>(),
|
||||||
|
std::make_shared<PixelSortEffect>(),
|
||||||
|
std::make_shared<CorruptEffect>(),
|
||||||
|
std::make_shared<ShatterEffect>(),
|
||||||
|
std::make_shared<WarpEffect>(),
|
||||||
|
std::make_shared<BitCrushEffect>(),
|
||||||
|
std::make_shared<FeedbackEffect>(),
|
||||||
};
|
};
|
||||||
return registry;
|
return registry;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
#include "effects/bitcrush.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/parallel.hpp"
|
||||||
|
#include "effects/sampling.hpp"
|
||||||
|
#include "effects/source_fill.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
const std::vector<ParamSpec>& BitCrushEffect::params() const {
|
||||||
|
static const std::vector<ParamSpec> specs = {
|
||||||
|
ParamSpec::makeChoice("crush_op", "Operation", "mixed",
|
||||||
|
{"mixed", "posterize", "xor", "bitrotate", "channel_swap"},
|
||||||
|
"posterize drops the low bits, xor flips random low bits, "
|
||||||
|
"bitrotate turns each byte's bits round, channel_swap shuffles "
|
||||||
|
"red, green and blue; mixed picks one for every band."),
|
||||||
|
ParamSpec::makeInt("crush_bits", "Bits", 3, 1, 7,
|
||||||
|
"How many bits the operation reaches: dropped by posterize, flipped "
|
||||||
|
"at most by xor, turned at most by bitrotate."),
|
||||||
|
ParamSpec::makeDouble("crush_coverage", "Coverage", 0.5, 0.0, 1.0,
|
||||||
|
"Chance of each band being crushed."),
|
||||||
|
ParamSpec::makeInt("crush_band_height", "Band height", 32, 1, 4096,
|
||||||
|
"Thickness in pixels of each band."),
|
||||||
|
};
|
||||||
|
return specs;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
enum class Op : std::uint8_t { None, Posterize, Xor, Rotate, Swap };
|
||||||
|
|
||||||
|
struct Band {
|
||||||
|
Op op = Op::None;
|
||||||
|
int rotate = 0;
|
||||||
|
std::array<std::uint8_t, 3> mask{};
|
||||||
|
std::array<int, 3> order{0, 1, 2};
|
||||||
|
};
|
||||||
|
|
||||||
|
Op opNamed(const std::string& name) {
|
||||||
|
if (name == "posterize") return Op::Posterize;
|
||||||
|
if (name == "xor") return Op::Xor;
|
||||||
|
if (name == "bitrotate") return Op::Rotate;
|
||||||
|
if (name == "channel_swap") return Op::Swap;
|
||||||
|
return Op::None; // mixed: drawn per band
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Image BitCrushEffect::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 Op fixed = opNamed(paramChoice(values, "crush_op", "mixed"));
|
||||||
|
const int bits = static_cast<int>(std::clamp(paramInt(values, "crush_bits", 3), 1L, 7L));
|
||||||
|
const double coverage = std::clamp(paramDouble(values, "crush_coverage", 0.5), 0.0, 1.0);
|
||||||
|
const int bandHeight = static_cast<int>(std::max(1L, paramInt(values, "crush_band_height", 32)));
|
||||||
|
|
||||||
|
Image canvas = canvasFromSources(sources, width, height, rng);
|
||||||
|
|
||||||
|
const int bandCount = (height + bandHeight - 1) / bandHeight;
|
||||||
|
std::vector<Band> plan(static_cast<std::size_t>(bandCount));
|
||||||
|
for (auto& band : plan) {
|
||||||
|
if (!chance(rng.next(), coverage)) continue;
|
||||||
|
band.op = fixed != Op::None ? fixed : static_cast<Op>(1 + rng.below(3));
|
||||||
|
switch (band.op) {
|
||||||
|
case Op::Xor:
|
||||||
|
for (auto& mask : band.mask) {
|
||||||
|
mask = static_cast<std::uint8_t>(rng.inRange(1, (1L << bits) - 1));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Op::Rotate:
|
||||||
|
band.rotate = static_cast<int>(rng.inRange(1, bits));
|
||||||
|
break;
|
||||||
|
case Op::Swap:
|
||||||
|
// Fisher-Yates, never leaving the order as it was.
|
||||||
|
do {
|
||||||
|
band.order = {0, 1, 2};
|
||||||
|
for (int i = 2; i > 0; --i) {
|
||||||
|
std::swap(band.order[static_cast<std::size_t>(i)],
|
||||||
|
band.order[rng.below(static_cast<std::uint64_t>(i))]);
|
||||||
|
}
|
||||||
|
} while (band.order[0] == 0 && band.order[1] == 1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::uint8_t keep = static_cast<std::uint8_t>(0xFF << bits);
|
||||||
|
|
||||||
|
parallelFor(static_cast<std::size_t>(height), [&](std::size_t begin, std::size_t end) {
|
||||||
|
for (std::size_t y = begin; y < end; ++y) {
|
||||||
|
const Band& band = plan[y / static_cast<std::size_t>(bandHeight)];
|
||||||
|
if (band.op == Op::None) continue;
|
||||||
|
std::uint8_t* row = canvas.row(static_cast<int>(y));
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
std::uint8_t* px = row + static_cast<std::size_t>(x) * 3;
|
||||||
|
switch (band.op) {
|
||||||
|
case Op::Posterize:
|
||||||
|
for (int c = 0; c < 3; ++c) px[c] &= keep;
|
||||||
|
break;
|
||||||
|
case Op::Xor:
|
||||||
|
for (int c = 0; c < 3; ++c) px[c] ^= band.mask[static_cast<std::size_t>(c)];
|
||||||
|
break;
|
||||||
|
case Op::Rotate:
|
||||||
|
for (int c = 0; c < 3; ++c) {
|
||||||
|
px[c] = static_cast<std::uint8_t>((px[c] << band.rotate) |
|
||||||
|
(px[c] >> (8 - band.rotate)));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Op::Swap: {
|
||||||
|
const std::uint8_t in[3] = {px[0], px[1], px[2]};
|
||||||
|
for (int c = 0; c < 3; ++c) px[c] = in[band.order[static_cast<std::size_t>(c)]];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Op::None:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/effect.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
// Mangles the bytes of its input band by band: posterising, xoring with a
|
||||||
|
// random mask, rotating the bits or shuffling the channels.
|
||||||
|
// A pure transform: it needs no source images, and leading a chain with
|
||||||
|
// sources loaded it starts from a crop of a randomly chosen one.
|
||||||
|
class BitCrushEffect : public Effect {
|
||||||
|
public:
|
||||||
|
std::string name() const override { return "bitcrush"; }
|
||||||
|
std::string title() const override { return "Bit crush"; }
|
||||||
|
std::string description() const override {
|
||||||
|
return "Mangles the bytes of random bands: posterises them, xors them with a random "
|
||||||
|
"mask, rotates their bits or shuffles their channels.";
|
||||||
|
}
|
||||||
|
const std::vector<ParamSpec>& params() const override;
|
||||||
|
bool needsSources() const override { return false; }
|
||||||
|
|
||||||
|
Image render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||||
|
const ParamValues& values) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#include "effects/channelsplit.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/parallel.hpp"
|
||||||
|
#include "effects/source_fill.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
const std::vector<ParamSpec>& ChannelSplitEffect::params() const {
|
||||||
|
static const std::vector<ParamSpec> specs = {
|
||||||
|
ParamSpec::makeInt("split_amount", "Amount", 12, 0, 1024,
|
||||||
|
"Furthest in pixels a channel may be moved, along either axis."),
|
||||||
|
ParamSpec::makeChoice("split_mode", "Mode", "global", {"global", "per_band"},
|
||||||
|
"global moves each channel once over the whole image; per_band "
|
||||||
|
"draws new offsets for every band."),
|
||||||
|
ParamSpec::makeInt("split_band_height", "Band height", 24, 1, 2048,
|
||||||
|
"Thickness in pixels of each band; per_band mode only."),
|
||||||
|
ParamSpec::makeChoice("split_channels", "Channels", "rgb", {"rgb", "red_cyan", "random"},
|
||||||
|
"rgb moves all three channels apart; red_cyan moves red one way and "
|
||||||
|
"green and blue the other, as 3D glasses would; random moves one "
|
||||||
|
"channel picked at random."),
|
||||||
|
};
|
||||||
|
return specs;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct Offset {
|
||||||
|
int dx = 0;
|
||||||
|
int dy = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
using ChannelOffsets = std::array<Offset, 3>;
|
||||||
|
|
||||||
|
ChannelOffsets drawOffsets(Rng& rng, int amount, const std::string& channels) {
|
||||||
|
ChannelOffsets offsets{};
|
||||||
|
auto draw = [&] {
|
||||||
|
return Offset{static_cast<int>(rng.inRange(-amount, amount)),
|
||||||
|
static_cast<int>(rng.inRange(-amount, amount))};
|
||||||
|
};
|
||||||
|
if (channels == "red_cyan") {
|
||||||
|
const Offset red = draw();
|
||||||
|
offsets[0] = red;
|
||||||
|
offsets[1] = offsets[2] = Offset{-red.dx, -red.dy};
|
||||||
|
} else if (channels == "random") {
|
||||||
|
offsets[rng.below(2)] = draw();
|
||||||
|
} else {
|
||||||
|
for (auto& offset : offsets) offset = draw();
|
||||||
|
}
|
||||||
|
return offsets;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Image ChannelSplitEffect::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 int amount = static_cast<int>(std::max(0L, paramInt(values, "split_amount", 12)));
|
||||||
|
const bool perBand = paramChoice(values, "split_mode", "global") == "per_band";
|
||||||
|
const int bandHeight = static_cast<int>(std::max(1L, paramInt(values, "split_band_height", 24)));
|
||||||
|
const std::string channels = paramChoice(values, "split_channels", "rgb");
|
||||||
|
|
||||||
|
const Image input = canvasFromSources(sources, width, height, rng);
|
||||||
|
|
||||||
|
// One set of offsets per band, a single band covering everything in global
|
||||||
|
// mode; all of them drawn before the pixels are touched.
|
||||||
|
const int band = perBand ? bandHeight : height;
|
||||||
|
const int bandCount = (height + band - 1) / band;
|
||||||
|
std::vector<ChannelOffsets> plan;
|
||||||
|
plan.reserve(static_cast<std::size_t>(bandCount));
|
||||||
|
for (int i = 0; i < bandCount; ++i) plan.push_back(drawOffsets(rng, amount, channels));
|
||||||
|
|
||||||
|
Image canvas(width, height);
|
||||||
|
parallelFor(static_cast<std::size_t>(height), [&](std::size_t begin, std::size_t end) {
|
||||||
|
for (std::size_t y = begin; y < end; ++y) {
|
||||||
|
const ChannelOffsets& offsets = plan[y / static_cast<std::size_t>(band)];
|
||||||
|
std::uint8_t* out = canvas.row(static_cast<int>(y));
|
||||||
|
for (int c = 0; c < 3; ++c) {
|
||||||
|
const Offset& offset = offsets[static_cast<std::size_t>(c)];
|
||||||
|
const int sy = std::clamp(static_cast<int>(y) - offset.dy, 0, height - 1);
|
||||||
|
const std::uint8_t* in = input.row(sy);
|
||||||
|
for (int x = 0; x < width; ++x) {
|
||||||
|
const int sx = std::clamp(x - offset.dx, 0, width - 1);
|
||||||
|
out[static_cast<std::size_t>(x) * 3 + c] = in[static_cast<std::size_t>(sx) * 3 + c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/effect.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
// Knocks the red, green and blue planes out of register, either the whole image
|
||||||
|
// at once or band by band, each band with offsets of its own.
|
||||||
|
// A pure transform: it needs no source images, and leading a chain with
|
||||||
|
// sources loaded it starts from a crop of a randomly chosen one.
|
||||||
|
class ChannelSplitEffect : public Effect {
|
||||||
|
public:
|
||||||
|
std::string name() const override { return "channelsplit"; }
|
||||||
|
std::string title() const override { return "Channel split"; }
|
||||||
|
std::string description() const override {
|
||||||
|
return "Knocks the red, green and blue planes out of register, over the whole image or "
|
||||||
|
"band by band.";
|
||||||
|
}
|
||||||
|
const std::vector<ParamSpec>& params() const override;
|
||||||
|
bool needsSources() const override { return false; }
|
||||||
|
|
||||||
|
Image render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||||
|
const ParamValues& values) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
#include "effects/corrupt.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/parallel.hpp"
|
||||||
|
#include "effects/sampling.hpp"
|
||||||
|
#include "effects/source_fill.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
const std::vector<ParamSpec>& CorruptEffect::params() const {
|
||||||
|
static const std::vector<ParamSpec> specs = {
|
||||||
|
ParamSpec::makeInt("corrupt_block", "Block size", 16, 2, 256,
|
||||||
|
"Width and height in pixels of one block, as a codec would cut them."),
|
||||||
|
ParamSpec::makeDouble("corrupt_rate", "Rate", 0.08, 0.0, 1.0,
|
||||||
|
"Chance of each block being damaged."),
|
||||||
|
ParamSpec::makeChoice("corrupt_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::makeBool("corrupt_bleed", "Bleed", false,
|
||||||
|
"Carry each damaged block's fault on to the end of its row, the way "
|
||||||
|
"one broken bit spoils the rest of a stream."),
|
||||||
|
};
|
||||||
|
return specs;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
enum class Damage : std::uint8_t { None, Repeat, Displace, Colour };
|
||||||
|
|
||||||
|
struct Block {
|
||||||
|
Damage damage = Damage::None;
|
||||||
|
int sx = 0; // displace: where the block is read from
|
||||||
|
int sy = 0;
|
||||||
|
int shift[3] = {0, 0, 0}; // colour: added to each channel
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Image CorruptEffect::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 int size = static_cast<int>(std::max(2L, paramInt(values, "corrupt_block", 16)));
|
||||||
|
const double rate = std::clamp(paramDouble(values, "corrupt_rate", 0.08), 0.0, 1.0);
|
||||||
|
const std::string kinds = paramChoice(values, "corrupt_kinds", "mixed");
|
||||||
|
const bool bleed = paramBool(values, "corrupt_bleed", false);
|
||||||
|
|
||||||
|
const Image input = canvasFromSources(sources, width, height, rng);
|
||||||
|
|
||||||
|
const int cols = (width + size - 1) / size;
|
||||||
|
const int rows = (height + size - 1) / size;
|
||||||
|
|
||||||
|
auto drawBlock = [&](int bx) {
|
||||||
|
Block block;
|
||||||
|
if (kinds == "repeat") block.damage = Damage::Repeat;
|
||||||
|
else if (kinds == "displace") block.damage = Damage::Displace;
|
||||||
|
else if (kinds == "colour") block.damage = Damage::Colour;
|
||||||
|
else block.damage = static_cast<Damage>(1 + rng.below(2));
|
||||||
|
// The first block of a row has nothing to its left to repeat.
|
||||||
|
if (block.damage == Damage::Repeat && bx == 0) block.damage = Damage::Displace;
|
||||||
|
if (block.damage == Damage::Displace) {
|
||||||
|
block.sx = static_cast<int>(rng.inRange(0, std::max(0, width - size)));
|
||||||
|
block.sy = static_cast<int>(rng.inRange(0, std::max(0, height - size)));
|
||||||
|
} else if (block.damage == Damage::Colour) {
|
||||||
|
for (int& shift : block.shift) shift = static_cast<int>(rng.inRange(-128, 128));
|
||||||
|
}
|
||||||
|
return block;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The whole damage map is drawn up front, block by block in reading order.
|
||||||
|
std::vector<Block> plan(static_cast<std::size_t>(cols) * rows);
|
||||||
|
for (int by = 0; by < rows; ++by) {
|
||||||
|
const Block* carried = nullptr;
|
||||||
|
for (int bx = 0; bx < cols; ++bx) {
|
||||||
|
Block& block = plan[static_cast<std::size_t>(by) * cols + bx];
|
||||||
|
if (carried != nullptr) {
|
||||||
|
block = *carried;
|
||||||
|
// A displaced run keeps moving along with the row, so the
|
||||||
|
// stretch reads as one shifted strip rather than one block
|
||||||
|
// over and over.
|
||||||
|
if (block.damage == Damage::Displace) block.sx += size;
|
||||||
|
carried = █
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!chance(rng.next(), rate)) continue;
|
||||||
|
block = drawBlock(bx);
|
||||||
|
if (bleed) carried = █
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Image canvas = input;
|
||||||
|
// Block rows are independent (a repeat only ever reads to its left), so
|
||||||
|
// they run in parallel and each walks its row in order.
|
||||||
|
parallelFor(static_cast<std::size_t>(rows), [&](std::size_t begin, std::size_t end) {
|
||||||
|
for (std::size_t by = begin; by < end; ++by) {
|
||||||
|
const int y0 = static_cast<int>(by) * size;
|
||||||
|
const int h = std::min(size, height - y0);
|
||||||
|
for (int bx = 0; bx < cols; ++bx) {
|
||||||
|
const Block& block = plan[by * static_cast<std::size_t>(cols) + bx];
|
||||||
|
if (block.damage == Damage::None) continue;
|
||||||
|
const int x0 = bx * size;
|
||||||
|
const int w = std::min(size, width - x0);
|
||||||
|
const std::size_t bytes = static_cast<std::size_t>(w) * 3;
|
||||||
|
|
||||||
|
for (int line = 0; line < h; ++line) {
|
||||||
|
std::uint8_t* out = canvas.row(y0 + line) + static_cast<std::size_t>(x0) * 3;
|
||||||
|
switch (block.damage) {
|
||||||
|
case Damage::Repeat:
|
||||||
|
std::memmove(out, out - static_cast<std::size_t>(size) * 3, bytes);
|
||||||
|
break;
|
||||||
|
case Damage::Displace: {
|
||||||
|
const int sy = std::min(block.sy + line, height - 1);
|
||||||
|
const std::uint8_t* in = input.row(sy);
|
||||||
|
for (int x = 0; x < w; ++x) {
|
||||||
|
const int sx = (block.sx + x) % width;
|
||||||
|
std::memcpy(out + static_cast<std::size_t>(x) * 3,
|
||||||
|
in + static_cast<std::size_t>(sx) * 3, 3);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Damage::Colour:
|
||||||
|
for (std::size_t i = 0; i < bytes; ++i) {
|
||||||
|
out[i] = static_cast<std::uint8_t>(
|
||||||
|
std::clamp(out[i] + block.shift[i % 3], 0, 255));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Damage::None:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/effect.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
// Imitates a damaged video stream: blocks of the image are frozen into a copy of
|
||||||
|
// their neighbour, pulled in from elsewhere or knocked off colour, and with
|
||||||
|
// bleeding switched on the damage runs on to the end of the row.
|
||||||
|
// A pure transform: it needs no source images, and leading a chain with
|
||||||
|
// sources loaded it starts from a crop of a randomly chosen one.
|
||||||
|
class CorruptEffect : public Effect {
|
||||||
|
public:
|
||||||
|
std::string name() const override { return "corrupt"; }
|
||||||
|
std::string title() const override { return "Corruption"; }
|
||||||
|
std::string description() const override {
|
||||||
|
return "Imitates a damaged video stream: random blocks are smeared, displaced or knocked "
|
||||||
|
"off colour, optionally bleeding on to the end of their row.";
|
||||||
|
}
|
||||||
|
const std::vector<ParamSpec>& params() const override;
|
||||||
|
bool needsSources() const override { return false; }
|
||||||
|
|
||||||
|
Image render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||||
|
const ParamValues& values) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
#include "effects/feedback.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/parallel.hpp"
|
||||||
|
#include "effects/sampling.hpp"
|
||||||
|
#include "effects/source_fill.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
const std::vector<ParamSpec>& FeedbackEffect::params() const {
|
||||||
|
static const std::vector<ParamSpec> specs = {
|
||||||
|
ParamSpec::makeInt("echo_count", "Echoes", 6, 1, 64,
|
||||||
|
"How many times the image is laid back over itself."),
|
||||||
|
ParamSpec::makeDouble("echo_scale", "Scale", 0.85, 0.1, 2.0,
|
||||||
|
"Size of each echo against the one before; below 1 the echoes "
|
||||||
|
"tunnel inwards, above 1 they burst outwards.",
|
||||||
|
0.01),
|
||||||
|
ParamSpec::makeDouble("echo_rotate", "Rotation", 4.0, -180.0, 180.0,
|
||||||
|
"Turn in degrees of each echo against the one before.", 0.5, 1),
|
||||||
|
ParamSpec::makeInt("echo_drift", "Drift", 20, 0, 4096,
|
||||||
|
"Furthest in pixels each echo may wander off centre, either way."),
|
||||||
|
ParamSpec::makeDouble("echo_fade", "Opacity", 0.8, 0.0, 1.0,
|
||||||
|
"How strongly each echo covers what lies beneath it."),
|
||||||
|
};
|
||||||
|
return specs;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct Echo {
|
||||||
|
double dx = 0.0;
|
||||||
|
double dy = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Image FeedbackEffect::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 count = std::clamp(paramInt(values, "echo_count", 6), 1L, 64L);
|
||||||
|
const double scale = std::clamp(paramDouble(values, "echo_scale", 0.85), 0.1, 2.0);
|
||||||
|
const double rotate = std::clamp(paramDouble(values, "echo_rotate", 4.0), -180.0, 180.0);
|
||||||
|
const int drift = static_cast<int>(std::max(0L, paramInt(values, "echo_drift", 20)));
|
||||||
|
const double fade = std::clamp(paramDouble(values, "echo_fade", 0.8), 0.0, 1.0);
|
||||||
|
|
||||||
|
const Image input = canvasFromSources(sources, width, height, rng);
|
||||||
|
|
||||||
|
std::vector<Echo> echoes(static_cast<std::size_t>(count));
|
||||||
|
for (auto& echo : echoes) {
|
||||||
|
echo.dx = static_cast<double>(rng.inRange(-drift, drift));
|
||||||
|
echo.dy = static_cast<double>(rng.inRange(-drift, drift));
|
||||||
|
}
|
||||||
|
|
||||||
|
double sine = 0.0;
|
||||||
|
double cosine = 1.0;
|
||||||
|
sinCosDegrees(rotate, sine, cosine);
|
||||||
|
const double cx = (width - 1) * 0.5;
|
||||||
|
const double cy = (height - 1) * 0.5;
|
||||||
|
const int mix = static_cast<int>(fade * 256.0 + 0.5);
|
||||||
|
|
||||||
|
// A camera filming its own monitor: every pass lays the whole picture so
|
||||||
|
// far, shrunk (or grown), turned and drifted, over the untouched input, so
|
||||||
|
// the older echoes nest inside the newer ones and fade as they go.
|
||||||
|
Image frame = input;
|
||||||
|
Image next(width, height);
|
||||||
|
for (const Echo& echo : echoes) {
|
||||||
|
parallelFor(static_cast<std::size_t>(height), [&](std::size_t begin, std::size_t end) {
|
||||||
|
for (std::size_t row = begin; row < end; ++row) {
|
||||||
|
const std::uint8_t* base = input.row(static_cast<int>(row));
|
||||||
|
std::uint8_t* out = next.row(static_cast<int>(row));
|
||||||
|
const double ry = static_cast<double>(row) - cy - echo.dy;
|
||||||
|
for (int col = 0; col < width; ++col) {
|
||||||
|
const std::size_t at = static_cast<std::size_t>(col) * 3;
|
||||||
|
const double rx = col - cx - echo.dx;
|
||||||
|
const double sx = cx + (rx * cosine + ry * sine) / scale;
|
||||||
|
const double sy = cy + (-rx * sine + ry * cosine) / scale;
|
||||||
|
if (sx < 0.0 || sy < 0.0 || sx > width - 1 || sy > height - 1) {
|
||||||
|
out[at] = base[at];
|
||||||
|
out[at + 1] = base[at + 1];
|
||||||
|
out[at + 2] = base[at + 2];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::uint8_t echoed[3];
|
||||||
|
sampleBilinear(frame, sx, sy, echoed);
|
||||||
|
for (int c = 0; c < 3; ++c) {
|
||||||
|
const int under = base[at + static_cast<std::size_t>(c)];
|
||||||
|
out[at + static_cast<std::size_t>(c)] =
|
||||||
|
static_cast<std::uint8_t>(under + (echoed[c] - under) * mix / 256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
std::swap(frame, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/effect.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
// Lays the image over itself again and again, each copy zoomed, turned and
|
||||||
|
// drifted a little further and fading as it goes, like a camera filming its
|
||||||
|
// own monitor.
|
||||||
|
// A pure transform: it needs no source images, and leading a chain with
|
||||||
|
// sources loaded it starts from a crop of a randomly chosen one.
|
||||||
|
class FeedbackEffect : public Effect {
|
||||||
|
public:
|
||||||
|
std::string name() const override { return "feedback"; }
|
||||||
|
std::string title() const override { return "Feedback"; }
|
||||||
|
std::string description() const override {
|
||||||
|
return "Lays the image over itself again and again, each copy zoomed, turned and drifted "
|
||||||
|
"further and fading, like a camera filming its own monitor.";
|
||||||
|
}
|
||||||
|
const std::vector<ParamSpec>& params() const override;
|
||||||
|
bool needsSources() const override { return false; }
|
||||||
|
|
||||||
|
Image render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||||
|
const ParamValues& values) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
#include "effects/pixelsort.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstring>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/parallel.hpp"
|
||||||
|
#include "effects/sampling.hpp"
|
||||||
|
#include "effects/source_fill.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
const std::vector<ParamSpec>& PixelSortEffect::params() const {
|
||||||
|
static const std::vector<ParamSpec> specs = {
|
||||||
|
ParamSpec::makeDouble("sort_low", "Lower threshold", 0.25, 0.0, 1.0,
|
||||||
|
"Pixels darker than this break a run and stay where they are."),
|
||||||
|
ParamSpec::makeDouble("sort_high", "Upper threshold", 0.8, 0.0, 1.0,
|
||||||
|
"Pixels brighter than this break a run and stay where they are."),
|
||||||
|
ParamSpec::makeChoice("sort_key", "Sort by", "luma", {"luma", "hue", "saturation"},
|
||||||
|
"What orders the pixels inside a run."),
|
||||||
|
ParamSpec::makeChoice("sort_direction", "Direction", "horizontal",
|
||||||
|
{"horizontal", "vertical"}, "Whether rows or columns are sorted."),
|
||||||
|
ParamSpec::makeBool("sort_reverse", "Reverse", false,
|
||||||
|
"Sort from high to low instead of from low to high."),
|
||||||
|
ParamSpec::makeDouble("sort_line_chance", "Line chance", 1.0, 0.0, 1.0,
|
||||||
|
"Chance of each row (or column) being sorted at all."),
|
||||||
|
ParamSpec::makeInt("sort_max_run", "Longest run", 0, 0, 8192,
|
||||||
|
"Each run is cut after a random length up to this many pixels, which "
|
||||||
|
"frays the edges of the sorted streaks (0 = no limit)."),
|
||||||
|
};
|
||||||
|
return specs;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int luma(const std::uint8_t* px) { return (px[0] * 54 + px[1] * 183 + px[2] * 19) >> 8; }
|
||||||
|
|
||||||
|
// Hue on a 0..1535 scale (six sectors of 256), in integers.
|
||||||
|
int hue(const std::uint8_t* px) {
|
||||||
|
const int r = px[0], g = px[1], b = px[2];
|
||||||
|
const int hi = std::max({r, g, b});
|
||||||
|
const int lo = std::min({r, g, b});
|
||||||
|
const int range = hi - lo;
|
||||||
|
if (range == 0) return 0;
|
||||||
|
if (hi == r) return g >= b ? (g - b) * 255 / range : 1535 - (b - g) * 255 / range;
|
||||||
|
if (hi == g) return 512 + (b - r) * 255 / range;
|
||||||
|
return 1024 + (r - g) * 255 / range;
|
||||||
|
}
|
||||||
|
|
||||||
|
int saturation(const std::uint8_t* px) {
|
||||||
|
return std::max({px[0], px[1], px[2]}) - std::min({px[0], px[1], px[2]});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The image with rows and columns swapped, so vertical sorting is horizontal
|
||||||
|
// sorting of the transpose.
|
||||||
|
Image transposed(const Image& src) {
|
||||||
|
Image out(src.height(), src.width());
|
||||||
|
parallelFor(static_cast<std::size_t>(src.width()), [&](std::size_t begin, std::size_t end) {
|
||||||
|
for (std::size_t x = begin; x < end; ++x) {
|
||||||
|
std::uint8_t* row = out.row(static_cast<int>(x));
|
||||||
|
for (int y = 0; y < src.height(); ++y) {
|
||||||
|
std::memcpy(row + static_cast<std::size_t>(y) * 3, src.row(y) + x * 3, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LinePlan {
|
||||||
|
bool sorted = false;
|
||||||
|
std::uint64_t seed = 0; // for the run lengths, when they are limited
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Image PixelSortEffect::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 int low = static_cast<int>(std::clamp(paramDouble(values, "sort_low", 0.25), 0.0, 1.0) * 255.0 + 0.5);
|
||||||
|
const int high = static_cast<int>(std::clamp(paramDouble(values, "sort_high", 0.8), 0.0, 1.0) * 255.0 + 0.5);
|
||||||
|
const std::string keyName = paramChoice(values, "sort_key", "luma");
|
||||||
|
const bool vertical = paramChoice(values, "sort_direction", "horizontal") == "vertical";
|
||||||
|
const bool reverse = paramBool(values, "sort_reverse", false);
|
||||||
|
const double lineChance = std::clamp(paramDouble(values, "sort_line_chance", 1.0), 0.0, 1.0);
|
||||||
|
const int maxRun = static_cast<int>(std::max(0L, paramInt(values, "sort_max_run", 0)));
|
||||||
|
|
||||||
|
Image canvas = canvasFromSources(sources, width, height, rng);
|
||||||
|
if (vertical) canvas = transposed(canvas);
|
||||||
|
const int lines = canvas.height();
|
||||||
|
const int span = canvas.width();
|
||||||
|
|
||||||
|
// Which lines are sorted, and a seed for each line's run lengths, are drawn
|
||||||
|
// serially; the lines then sort in parallel, each from its own generator.
|
||||||
|
std::vector<LinePlan> plan(static_cast<std::size_t>(lines));
|
||||||
|
for (auto& line : plan) {
|
||||||
|
line.sorted = chance(rng.next(), lineChance);
|
||||||
|
if (maxRun > 0) line.seed = rng.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto keyOf = [&](const std::uint8_t* px) {
|
||||||
|
if (keyName == "hue") return hue(px);
|
||||||
|
if (keyName == "saturation") return saturation(px);
|
||||||
|
return luma(px);
|
||||||
|
};
|
||||||
|
|
||||||
|
parallelFor(static_cast<std::size_t>(lines), [&](std::size_t begin, std::size_t end) {
|
||||||
|
std::vector<std::pair<int, std::array<std::uint8_t, 3>>> run;
|
||||||
|
for (std::size_t line = begin; line < end; ++line) {
|
||||||
|
if (!plan[line].sorted) continue;
|
||||||
|
Rng runRng(plan[line].seed);
|
||||||
|
std::uint8_t* row = canvas.row(static_cast<int>(line));
|
||||||
|
|
||||||
|
auto flush = [&](int start) {
|
||||||
|
// A stable sort, so equal keys keep their order on every
|
||||||
|
// standard library.
|
||||||
|
std::stable_sort(run.begin(), run.end(), [&](const auto& a, const auto& b) {
|
||||||
|
return reverse ? a.first > b.first : a.first < b.first;
|
||||||
|
});
|
||||||
|
for (std::size_t i = 0; i < run.size(); ++i) {
|
||||||
|
std::memcpy(row + (static_cast<std::size_t>(start) + i) * 3, run[i].second.data(), 3);
|
||||||
|
}
|
||||||
|
run.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
int start = 0;
|
||||||
|
int limit = maxRun > 0 ? static_cast<int>(runRng.inRange(1, maxRun)) : span;
|
||||||
|
for (int x = 0; x < span; ++x) {
|
||||||
|
const std::uint8_t* px = row + static_cast<std::size_t>(x) * 3;
|
||||||
|
const int brightness = luma(px);
|
||||||
|
if (brightness < low || brightness > high) {
|
||||||
|
flush(start);
|
||||||
|
start = x + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (static_cast<int>(run.size()) >= limit) {
|
||||||
|
flush(start);
|
||||||
|
start = x;
|
||||||
|
if (maxRun > 0) limit = static_cast<int>(runRng.inRange(1, maxRun));
|
||||||
|
}
|
||||||
|
run.push_back({keyOf(px), {px[0], px[1], px[2]}});
|
||||||
|
}
|
||||||
|
flush(start);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return vertical ? transposed(canvas) : canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/effect.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
// Finds runs of pixels whose brightness lies between two thresholds and sorts
|
||||||
|
// each run by brightness, hue or saturation, which makes the image drip.
|
||||||
|
// A pure transform: it needs no source images, and leading a chain with
|
||||||
|
// sources loaded it starts from a crop of a randomly chosen one.
|
||||||
|
class PixelSortEffect : public Effect {
|
||||||
|
public:
|
||||||
|
std::string name() const override { return "pixelsort"; }
|
||||||
|
std::string title() const override { return "Pixel sort"; }
|
||||||
|
std::string description() const override {
|
||||||
|
return "Sorts the runs of pixels whose brightness lies between two thresholds, along "
|
||||||
|
"rows or columns, so the image melts and drips.";
|
||||||
|
}
|
||||||
|
const std::vector<ParamSpec>& params() const override;
|
||||||
|
bool needsSources() const override { return false; }
|
||||||
|
|
||||||
|
Image render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||||
|
const ParamValues& values) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "core/image.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
// Reads the colour at a fractional position, mixing the four neighbouring
|
||||||
|
// pixels; positions off the image take the nearest edge pixel. The weights are
|
||||||
|
// cut to 1/256 steps so the mix itself is integer arithmetic.
|
||||||
|
inline void sampleBilinear(const Image& image, double x, double y, std::uint8_t out[3]) {
|
||||||
|
const int maxX = image.width() - 1;
|
||||||
|
const int maxY = image.height() - 1;
|
||||||
|
x = std::clamp(x, 0.0, static_cast<double>(maxX));
|
||||||
|
y = std::clamp(y, 0.0, static_cast<double>(maxY));
|
||||||
|
|
||||||
|
const int x0 = static_cast<int>(x);
|
||||||
|
const int y0 = static_cast<int>(y);
|
||||||
|
const int x1 = std::min(x0 + 1, maxX);
|
||||||
|
const int y1 = std::min(y0 + 1, maxY);
|
||||||
|
const int fx = static_cast<int>((x - x0) * 256.0);
|
||||||
|
const int fy = static_cast<int>((y - y0) * 256.0);
|
||||||
|
|
||||||
|
const std::uint8_t* a = image.row(y0) + static_cast<std::size_t>(x0) * 3;
|
||||||
|
const std::uint8_t* b = image.row(y0) + static_cast<std::size_t>(x1) * 3;
|
||||||
|
const std::uint8_t* c = image.row(y1) + static_cast<std::size_t>(x0) * 3;
|
||||||
|
const std::uint8_t* d = image.row(y1) + static_cast<std::size_t>(x1) * 3;
|
||||||
|
for (int ch = 0; ch < 3; ++ch) {
|
||||||
|
const int top = a[ch] * (256 - fx) + b[ch] * fx;
|
||||||
|
const int bottom = c[ch] * (256 - fx) + d[ch] * fx;
|
||||||
|
out[ch] = static_cast<std::uint8_t>((top * (256 - fy) + bottom * fy + 32768) >> 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sine and cosine of an angle in degrees from nothing but additions and
|
||||||
|
// multiplications, so a rotation lands on the same pixels whatever the
|
||||||
|
// machine's libm makes of std::sin. The angle is folded into one octant and a
|
||||||
|
// short Taylor series does the rest, far below a pixel's worth of error.
|
||||||
|
inline void sinCosDegrees(double degrees, double& sine, double& cosine) {
|
||||||
|
const double turns = degrees / 90.0;
|
||||||
|
const double quadrant = std::floor(turns + 0.5);
|
||||||
|
const double x = (turns - quadrant) * 1.5707963267948966; // [-pi/4, pi/4]
|
||||||
|
const double x2 = x * x;
|
||||||
|
const double s =
|
||||||
|
x * (1.0 - x2 / 6.0 * (1.0 - x2 / 20.0 * (1.0 - x2 / 42.0 * (1.0 - x2 / 72.0))));
|
||||||
|
const double c =
|
||||||
|
1.0 - x2 / 2.0 * (1.0 - x2 / 12.0 * (1.0 - x2 / 30.0 * (1.0 - x2 / 56.0 * (1.0 - x2 / 90.0))));
|
||||||
|
switch (static_cast<int>(quadrant - 4.0 * std::floor(quadrant / 4.0))) {
|
||||||
|
case 0: sine = s; cosine = c; break;
|
||||||
|
case 1: sine = c; cosine = -s; break;
|
||||||
|
case 2: sine = -s; cosine = -c; break;
|
||||||
|
default: sine = -c; cosine = s; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A uniform double in [0, 1) from the top 53 bits of one rng draw.
|
||||||
|
inline double unitDouble(std::uint64_t bits) {
|
||||||
|
return static_cast<double>(bits >> 11) * (1.0 / 9007199254740992.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether an event with the given probability happens, from one rng draw.
|
||||||
|
inline bool chance(std::uint64_t bits, double probability) {
|
||||||
|
return unitDouble(bits) < probability;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
#include "effects/shatter.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <limits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/parallel.hpp"
|
||||||
|
#include "effects/sampling.hpp"
|
||||||
|
#include "effects/source_fill.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
const std::vector<ParamSpec>& ShatterEffect::params() const {
|
||||||
|
static const std::vector<ParamSpec> specs = {
|
||||||
|
ParamSpec::makeInt("shatter_cells", "Shards", 40, 2, 5000,
|
||||||
|
"How many shards the image breaks into."),
|
||||||
|
ParamSpec::makeInt("shatter_displace", "Displacement", 40, 0, 4096,
|
||||||
|
"Furthest in pixels a shard's picture may slide, along either axis."),
|
||||||
|
ParamSpec::makeDouble("shatter_rotate", "Rotation", 8.0, 0.0, 180.0,
|
||||||
|
"Largest turn in degrees of a shard's picture, either way.", 0.5, 1),
|
||||||
|
ParamSpec::makeDouble("shatter_zoom", "Zoom", 0.05, 0.0, 0.9,
|
||||||
|
"Largest zoom in or out of a shard's picture, as a fraction.", 0.01),
|
||||||
|
ParamSpec::makeInt("shatter_crack_width", "Crack width", 2, 0, 64,
|
||||||
|
"Width in pixels of the cracks between the shards (0 = none)."),
|
||||||
|
ParamSpec::makeColor("shatter_crack_colour", "Crack colour", "#101010",
|
||||||
|
"Colour the cracks are drawn in."),
|
||||||
|
};
|
||||||
|
return specs;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct Shard {
|
||||||
|
double x = 0.0; // the point the shard grows around
|
||||||
|
double y = 0.0;
|
||||||
|
double dx = 0.0;
|
||||||
|
double dy = 0.0;
|
||||||
|
double sine = 0.0;
|
||||||
|
double cosine = 1.0;
|
||||||
|
double zoom = 1.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The shard points bucketed on a coarse grid, so finding the two nearest to a
|
||||||
|
// pixel looks at a few buckets instead of every point.
|
||||||
|
class ShardGrid {
|
||||||
|
public:
|
||||||
|
ShardGrid(const std::vector<Shard>& shards, int width, int height) : shards_(shards) {
|
||||||
|
cell_ = std::max(1.0, std::sqrt(static_cast<double>(width) * height /
|
||||||
|
static_cast<double>(shards.size())));
|
||||||
|
cols_ = static_cast<int>(width / cell_) + 1;
|
||||||
|
rows_ = static_cast<int>(height / cell_) + 1;
|
||||||
|
buckets_.resize(static_cast<std::size_t>(cols_) * rows_);
|
||||||
|
for (std::size_t i = 0; i < shards.size(); ++i) {
|
||||||
|
buckets_[bucketOf(shards[i].x, shards[i].y)].push_back(static_cast<int>(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The nearest and second nearest shard to (x, y).
|
||||||
|
void nearest(double x, double y, int& first, int& second) const {
|
||||||
|
const int bx = std::clamp(static_cast<int>(x / cell_), 0, cols_ - 1);
|
||||||
|
const int by = std::clamp(static_cast<int>(y / cell_), 0, rows_ - 1);
|
||||||
|
double best = std::numeric_limits<double>::max();
|
||||||
|
double next = best;
|
||||||
|
first = second = -1;
|
||||||
|
const int maxRing = std::max(cols_, rows_);
|
||||||
|
for (int ring = 0; ring <= maxRing; ++ring) {
|
||||||
|
// Anything in this ring is at least (ring - 1) cells away, so once
|
||||||
|
// both answers are closer than that the search is over.
|
||||||
|
const double reach = std::max(0, ring - 1) * cell_;
|
||||||
|
if (second >= 0 && next < reach * reach) break;
|
||||||
|
for (int gy = by - ring; gy <= by + ring; ++gy) {
|
||||||
|
if (gy < 0 || gy >= rows_) continue;
|
||||||
|
const bool edgeRow = gy == by - ring || gy == by + ring;
|
||||||
|
for (int gx = bx - ring; gx <= bx + ring; gx += edgeRow ? 1 : 2 * std::max(ring, 1)) {
|
||||||
|
if (gx < 0 || gx >= cols_) continue;
|
||||||
|
for (int index : buckets_[static_cast<std::size_t>(gy) * cols_ + gx]) {
|
||||||
|
const Shard& shard = shards_[static_cast<std::size_t>(index)];
|
||||||
|
const double d = (shard.x - x) * (shard.x - x) + (shard.y - y) * (shard.y - y);
|
||||||
|
if (d < best || (d == best && index < first)) {
|
||||||
|
next = best;
|
||||||
|
second = first;
|
||||||
|
best = d;
|
||||||
|
first = index;
|
||||||
|
} else if (d < next || (d == next && index < second)) {
|
||||||
|
next = d;
|
||||||
|
second = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ring == 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::size_t bucketOf(double x, double y) const {
|
||||||
|
const int bx = std::clamp(static_cast<int>(x / cell_), 0, cols_ - 1);
|
||||||
|
const int by = std::clamp(static_cast<int>(y / cell_), 0, rows_ - 1);
|
||||||
|
return static_cast<std::size_t>(by) * cols_ + bx;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<Shard>& shards_;
|
||||||
|
double cell_ = 1.0;
|
||||||
|
int cols_ = 1;
|
||||||
|
int rows_ = 1;
|
||||||
|
std::vector<std::vector<int>> buckets_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Image ShatterEffect::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 count = std::max(2L, paramInt(values, "shatter_cells", 40));
|
||||||
|
const int displace = static_cast<int>(std::max(0L, paramInt(values, "shatter_displace", 40)));
|
||||||
|
const double rotate = std::clamp(paramDouble(values, "shatter_rotate", 8.0), 0.0, 180.0);
|
||||||
|
const double zoom = std::clamp(paramDouble(values, "shatter_zoom", 0.05), 0.0, 0.9);
|
||||||
|
const double crackWidth = static_cast<double>(std::max(0L, paramInt(values, "shatter_crack_width", 2)));
|
||||||
|
std::uint8_t crack[3];
|
||||||
|
paramColor(values, "shatter_crack_colour", "#101010", crack);
|
||||||
|
|
||||||
|
const Image input = canvasFromSources(sources, width, height, rng);
|
||||||
|
|
||||||
|
std::vector<Shard> shards(static_cast<std::size_t>(count));
|
||||||
|
for (auto& shard : shards) {
|
||||||
|
shard.x = unitDouble(rng.next()) * width;
|
||||||
|
shard.y = unitDouble(rng.next()) * height;
|
||||||
|
shard.dx = static_cast<double>(rng.inRange(-displace, displace));
|
||||||
|
shard.dy = static_cast<double>(rng.inRange(-displace, displace));
|
||||||
|
sinCosDegrees((unitDouble(rng.next()) * 2.0 - 1.0) * rotate, shard.sine, shard.cosine);
|
||||||
|
shard.zoom = 1.0 + (unitDouble(rng.next()) * 2.0 - 1.0) * zoom;
|
||||||
|
}
|
||||||
|
const ShardGrid grid(shards, width, height);
|
||||||
|
|
||||||
|
Image canvas(width, height);
|
||||||
|
parallelFor(static_cast<std::size_t>(height), [&](std::size_t begin, std::size_t end) {
|
||||||
|
for (std::size_t row = begin; row < end; ++row) {
|
||||||
|
std::uint8_t* out = canvas.row(static_cast<int>(row));
|
||||||
|
const double y = static_cast<double>(row) + 0.5;
|
||||||
|
for (int col = 0; col < width; ++col) {
|
||||||
|
std::uint8_t* px = out + static_cast<std::size_t>(col) * 3;
|
||||||
|
const double x = col + 0.5;
|
||||||
|
int first = 0;
|
||||||
|
int second = 0;
|
||||||
|
grid.nearest(x, y, first, second);
|
||||||
|
const Shard& a = shards[static_cast<std::size_t>(first)];
|
||||||
|
|
||||||
|
if (crackWidth > 0.0 && second >= 0) {
|
||||||
|
// Distance to the edge between the two nearest shards,
|
||||||
|
// which is the perpendicular bisector of their points.
|
||||||
|
const Shard& b = shards[static_cast<std::size_t>(second)];
|
||||||
|
const double da = (x - a.x) * (x - a.x) + (y - a.y) * (y - a.y);
|
||||||
|
const double db = (x - b.x) * (x - b.x) + (y - b.y) * (y - b.y);
|
||||||
|
const double gap = std::sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
|
||||||
|
if (gap > 0.0 && (db - da) / (2.0 * gap) < crackWidth * 0.5) {
|
||||||
|
px[0] = crack[0];
|
||||||
|
px[1] = crack[1];
|
||||||
|
px[2] = crack[2];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo the shard's slide, then its turn and zoom about its point.
|
||||||
|
const double rx = x - a.dx - a.x;
|
||||||
|
const double ry = y - a.dy - a.y;
|
||||||
|
const double sx = a.x + (rx * a.cosine + ry * a.sine) / a.zoom;
|
||||||
|
const double sy = a.y + (-rx * a.sine + ry * a.cosine) / a.zoom;
|
||||||
|
sampleBilinear(input, sx - 0.5, sy - 0.5, px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/effect.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
// Breaks its input into random glass shards (the cells around scattered points),
|
||||||
|
// each showing the image shifted, turned and zoomed a little on its own, with
|
||||||
|
// optional cracks drawn along the edges.
|
||||||
|
// A pure transform: it needs no source images, and leading a chain with
|
||||||
|
// sources loaded it starts from a crop of a randomly chosen one.
|
||||||
|
class ShatterEffect : public Effect {
|
||||||
|
public:
|
||||||
|
std::string name() const override { return "shatter"; }
|
||||||
|
std::string title() const override { return "Shatter"; }
|
||||||
|
std::string description() const override {
|
||||||
|
return "Breaks the image into random shards, each shifted, turned and zoomed on its own, "
|
||||||
|
"with optional cracks between them.";
|
||||||
|
}
|
||||||
|
const std::vector<ParamSpec>& params() const override;
|
||||||
|
bool needsSources() const override { return false; }
|
||||||
|
|
||||||
|
Image render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||||
|
const ParamValues& values) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
#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
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/effect.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
// Tears its input apart: random bands of rows (or columns) are pushed sideways by a
|
||||||
|
// random distance each, the way a video signal loses its sync.
|
||||||
|
// A pure transform: it needs no source images, and leading a chain with
|
||||||
|
// sources loaded it starts from a crop of a randomly chosen one.
|
||||||
|
class SlitShiftEffect : public Effect {
|
||||||
|
public:
|
||||||
|
std::string name() const override { return "slitshift"; }
|
||||||
|
std::string title() const override { return "Slit shift"; }
|
||||||
|
std::string description() const override {
|
||||||
|
return "Tears random bands of its input sideways, like a video signal losing sync; "
|
||||||
|
"overlapping bands add their shifts up.";
|
||||||
|
}
|
||||||
|
const std::vector<ParamSpec>& params() const override;
|
||||||
|
bool needsSources() const override { return false; }
|
||||||
|
|
||||||
|
Image render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||||
|
const ParamValues& values) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "core/image.hpp"
|
#include "core/image.hpp"
|
||||||
#include "core/parallel.hpp"
|
#include "core/parallel.hpp"
|
||||||
|
#include "core/rng.hpp"
|
||||||
|
|
||||||
namespace effector {
|
namespace effector {
|
||||||
|
|
||||||
@@ -35,4 +37,20 @@ inline void fillFromSource(Image& canvas, const Image& src, int sx, int sy) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
} // namespace effector
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
#include "effects/warp.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/parallel.hpp"
|
||||||
|
#include "effects/sampling.hpp"
|
||||||
|
#include "effects/source_fill.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
const std::vector<ParamSpec>& WarpEffect::params() const {
|
||||||
|
static const std::vector<ParamSpec> specs = {
|
||||||
|
ParamSpec::makeInt("warp_strength", "Strength", 40, 0, 4096,
|
||||||
|
"How far in pixels the field pushes the image at its strongest."),
|
||||||
|
ParamSpec::makeInt("warp_scale", "Scale", 160, 4, 8192,
|
||||||
|
"Size in pixels of the largest features in the field."),
|
||||||
|
ParamSpec::makeInt("warp_octaves", "Octaves", 3, 1, 6,
|
||||||
|
"Finer layers of noise laid over the first; more give a turbulent "
|
||||||
|
"edge to the flow."),
|
||||||
|
ParamSpec::makeChoice("warp_mode", "Mode", "displace", {"displace", "swirl"},
|
||||||
|
"displace pushes along the field; swirl flows along its contour "
|
||||||
|
"lines, which curls the image into eddies."),
|
||||||
|
};
|
||||||
|
return specs;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// One layer of value noise: random heights on a lattice `spacing` pixels
|
||||||
|
// apart, blended smoothly between the lattice points.
|
||||||
|
class NoiseLayer {
|
||||||
|
public:
|
||||||
|
NoiseLayer(Rng& rng, int width, int height, double spacing)
|
||||||
|
: spacing_(spacing),
|
||||||
|
cols_(static_cast<int>(width / spacing) + 2),
|
||||||
|
rows_(static_cast<int>(height / spacing) + 2),
|
||||||
|
lattice_(static_cast<std::size_t>(cols_) * rows_) {
|
||||||
|
for (auto& value : lattice_) value = unitDouble(rng.next()) * 2.0 - 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The height at (x, y) in [-1, 1], and its slope per pixel along x and y.
|
||||||
|
void sample(double x, double y, double& value, double& slopeX, double& slopeY) const {
|
||||||
|
const double gx = x / spacing_;
|
||||||
|
const double gy = y / spacing_;
|
||||||
|
const int ix = std::min(static_cast<int>(gx), cols_ - 2);
|
||||||
|
const int iy = std::min(static_cast<int>(gy), rows_ - 2);
|
||||||
|
const double fx = gx - ix;
|
||||||
|
const double fy = gy - iy;
|
||||||
|
const double sx = fx * fx * (3.0 - 2.0 * fx);
|
||||||
|
const double sy = fy * fy * (3.0 - 2.0 * fy);
|
||||||
|
|
||||||
|
const double a = at(ix, iy);
|
||||||
|
const double b = at(ix + 1, iy);
|
||||||
|
const double c = at(ix, iy + 1);
|
||||||
|
const double d = at(ix + 1, iy + 1);
|
||||||
|
const double cross = a - b - c + d;
|
||||||
|
value = a + (b - a) * sx + (c - a) * sy + cross * sx * sy;
|
||||||
|
slopeX = 6.0 * fx * (1.0 - fx) * ((b - a) + cross * sy) / spacing_;
|
||||||
|
slopeY = 6.0 * fy * (1.0 - fy) * ((c - a) + cross * sx) / spacing_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
double at(int x, int y) const { return lattice_[static_cast<std::size_t>(y) * cols_ + x]; }
|
||||||
|
|
||||||
|
double spacing_;
|
||||||
|
int cols_;
|
||||||
|
int rows_;
|
||||||
|
std::vector<double> lattice_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Octaves of noise summed, each twice as fine and half as strong as the one
|
||||||
|
// before, and scaled back so the total stays in [-1, 1].
|
||||||
|
class NoiseField {
|
||||||
|
public:
|
||||||
|
NoiseField(Rng& rng, int width, int height, double scale, int octaves) {
|
||||||
|
double spacing = scale;
|
||||||
|
double weight = 1.0;
|
||||||
|
for (int i = 0; i < octaves; ++i) {
|
||||||
|
layers_.emplace_back(rng, width, height, std::max(1.0, spacing));
|
||||||
|
weights_.push_back(weight);
|
||||||
|
total_ += weight;
|
||||||
|
spacing *= 0.5;
|
||||||
|
weight *= 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sample(double x, double y, double& value, double& slopeX, double& slopeY) const {
|
||||||
|
value = slopeX = slopeY = 0.0;
|
||||||
|
for (std::size_t i = 0; i < layers_.size(); ++i) {
|
||||||
|
double v = 0.0, sx = 0.0, sy = 0.0;
|
||||||
|
layers_[i].sample(x, y, v, sx, sy);
|
||||||
|
value += v * weights_[i];
|
||||||
|
slopeX += sx * weights_[i];
|
||||||
|
slopeY += sy * weights_[i];
|
||||||
|
}
|
||||||
|
value /= total_;
|
||||||
|
slopeX /= total_;
|
||||||
|
slopeY /= total_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<NoiseLayer> layers_;
|
||||||
|
std::vector<double> weights_;
|
||||||
|
double total_ = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Image WarpEffect::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 double strength = static_cast<double>(std::max(0L, paramInt(values, "warp_strength", 40)));
|
||||||
|
const double scale = static_cast<double>(std::max(4L, paramInt(values, "warp_scale", 160)));
|
||||||
|
const int octaves = static_cast<int>(std::clamp(paramInt(values, "warp_octaves", 3), 1L, 6L));
|
||||||
|
const bool swirl = paramChoice(values, "warp_mode", "displace") == "swirl";
|
||||||
|
|
||||||
|
const Image input = canvasFromSources(sources, width, height, rng);
|
||||||
|
|
||||||
|
// Lattices are drawn in full before any pixel moves: one field for the
|
||||||
|
// swirl, which flows along its contours, two for a push along x and y.
|
||||||
|
const NoiseField first(rng, width, height, scale, octaves);
|
||||||
|
std::vector<NoiseField> extra;
|
||||||
|
if (!swirl) extra.emplace_back(rng, width, height, scale, octaves);
|
||||||
|
|
||||||
|
Image canvas(width, height);
|
||||||
|
parallelFor(static_cast<std::size_t>(height), [&](std::size_t begin, std::size_t end) {
|
||||||
|
for (std::size_t row = begin; row < end; ++row) {
|
||||||
|
std::uint8_t* out = canvas.row(static_cast<int>(row));
|
||||||
|
const double y = static_cast<double>(row);
|
||||||
|
for (int col = 0; col < width; ++col) {
|
||||||
|
const double x = col;
|
||||||
|
double value = 0.0, slopeX = 0.0, slopeY = 0.0;
|
||||||
|
first.sample(x, y, value, slopeX, slopeY);
|
||||||
|
double dx = 0.0;
|
||||||
|
double dy = 0.0;
|
||||||
|
if (swirl) {
|
||||||
|
// Along the contour: the slope turned a quarter, which
|
||||||
|
// never piles pixels up or tears them apart. The slope of
|
||||||
|
// a field `scale` wide is about 1/scale, hence the factor.
|
||||||
|
dx = slopeY * scale * strength * 0.5;
|
||||||
|
dy = -slopeX * scale * strength * 0.5;
|
||||||
|
} else {
|
||||||
|
double other = 0.0;
|
||||||
|
extra.front().sample(x, y, other, slopeX, slopeY);
|
||||||
|
dx = value * strength;
|
||||||
|
dy = other * strength;
|
||||||
|
}
|
||||||
|
sampleBilinear(input, x + dx, y + dy, out + static_cast<std::size_t>(col) * 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "core/effect.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
// Pushes every pixel along a smooth random noise field: a haze at low strength,
|
||||||
|
// melted paint at high. The swirl mode follows the field's contour lines,
|
||||||
|
// which turns the image into eddies instead of ripples.
|
||||||
|
// A pure transform: it needs no source images, and leading a chain with
|
||||||
|
// sources loaded it starts from a crop of a randomly chosen one.
|
||||||
|
class WarpEffect : public Effect {
|
||||||
|
public:
|
||||||
|
std::string name() const override { return "warp"; }
|
||||||
|
std::string title() const override { return "Warp"; }
|
||||||
|
std::string description() const override {
|
||||||
|
return "Pushes every pixel along a smooth random noise field, from a gentle haze to "
|
||||||
|
"molten eddies.";
|
||||||
|
}
|
||||||
|
const std::vector<ParamSpec>& params() const override;
|
||||||
|
bool needsSources() const override { return false; }
|
||||||
|
|
||||||
|
Image render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||||
|
const ParamValues& values) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <locale>
|
#include <locale>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -201,6 +202,18 @@ effector::Image flat(int width, int height, std::uint8_t shade) {
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Renders any registered effect by name with its defaults, overridden by
|
||||||
|
// `values`, from a generator seeded with `seed`.
|
||||||
|
effector::Image renderNamed(const std::string& name, const std::vector<effector::Image>& sources,
|
||||||
|
int width, int height, std::uint64_t seed,
|
||||||
|
const effector::ParamValues& values) {
|
||||||
|
const auto effect = effector::effectByName(name);
|
||||||
|
effector::Rng rng(seed);
|
||||||
|
auto merged = effect->defaults();
|
||||||
|
for (const auto& [key, value] : values) merged[key] = value;
|
||||||
|
return effect->render(sources, width, height, rng, merged);
|
||||||
|
}
|
||||||
|
|
||||||
effector::Image render(const std::vector<effector::Image>& sources, int width, int height,
|
effector::Image render(const std::vector<effector::Image>& sources, int width, int height,
|
||||||
std::uint64_t seed, const effector::ParamValues& values) {
|
std::uint64_t seed, const effector::ParamValues& values) {
|
||||||
effector::ScanlinesEffect effect;
|
effector::ScanlinesEffect effect;
|
||||||
@@ -243,6 +256,15 @@ int main() {
|
|||||||
threw = true;
|
threw = true;
|
||||||
}
|
}
|
||||||
check(threw, "unknown effect throws");
|
check(threw, "unknown effect throws");
|
||||||
|
|
||||||
|
// Flags share one namespace, so a key two effects declared would be
|
||||||
|
// handed to whichever came first.
|
||||||
|
std::set<std::string> keys;
|
||||||
|
bool unique = true;
|
||||||
|
for (const auto& effect : effects()) {
|
||||||
|
for (const auto& spec : effect->params()) unique = keys.insert(spec.key).second && unique;
|
||||||
|
}
|
||||||
|
check(unique, "no two effects declare the same param key");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "scanlines\n";
|
std::cout << "scanlines\n";
|
||||||
@@ -933,6 +955,150 @@ int main() {
|
|||||||
"and so does a chain handed no styles at all");
|
"and so does a chain handed no styles at all");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::cout << "chaos transforms\n";
|
||||||
|
{
|
||||||
|
const std::vector<Image> sources = {gradient(1200, 800, 10), gradient(640, 480, 90)};
|
||||||
|
const std::vector<Image> input = {gradient(400, 300, 40)};
|
||||||
|
|
||||||
|
// What every one of them must do, and what switches each one off.
|
||||||
|
const std::vector<std::pair<std::string, ParamValues>> transforms = {
|
||||||
|
{"slitshift", {{"shift_max", 0L}}},
|
||||||
|
{"channelsplit", {{"split_amount", 0L}}},
|
||||||
|
{"pixelsort", {{"sort_line_chance", 0.0}}},
|
||||||
|
{"corrupt", {{"corrupt_rate", 0.0}}},
|
||||||
|
{"shatter",
|
||||||
|
{{"shatter_displace", 0L}, {"shatter_rotate", 0.0}, {"shatter_zoom", 0.0},
|
||||||
|
{"shatter_crack_width", 0L}}},
|
||||||
|
{"warp", {{"warp_strength", 0L}}},
|
||||||
|
{"bitcrush", {{"crush_coverage", 0.0}}},
|
||||||
|
{"feedback", {{"echo_fade", 0.0}}},
|
||||||
|
};
|
||||||
|
for (const auto& [name, off] : transforms) {
|
||||||
|
check(!effectByName(name)->needsSources(), name + " needs no source images");
|
||||||
|
|
||||||
|
const auto first = renderNamed(name, sources, 400, 300, 4, {});
|
||||||
|
const auto again = renderNamed(name, sources, 400, 300, 4, {});
|
||||||
|
const auto other = renderNamed(name, sources, 400, 300, 11, {});
|
||||||
|
check(first.width() == 400 && first.height() == 300,
|
||||||
|
name + " renders the requested size");
|
||||||
|
check(sameBytes(first, again), name + " reproduces itself from a seed");
|
||||||
|
check(!sameBytes(first, other), name + " changes with the seed");
|
||||||
|
check(!sameBytes(renderNamed(name, input, 400, 300, 4, {}), input[0]),
|
||||||
|
name + " changes its input");
|
||||||
|
check(sameBytes(renderNamed(name, input, 400, 300, 4, off), input[0]),
|
||||||
|
name + " switched right down leaves its input alone");
|
||||||
|
|
||||||
|
const auto blank = renderNamed(name, {}, 120, 80, 1, {});
|
||||||
|
check(blank.width() == 120 && blank.height() == 80,
|
||||||
|
name + " renders without sources");
|
||||||
|
|
||||||
|
ChainStage scanlines{effectByName("scanlines"), {{"strip_height", 6L}}};
|
||||||
|
ChainStage stage{effectByName(name), {}};
|
||||||
|
Rng a(9);
|
||||||
|
Rng b(9);
|
||||||
|
check(sameBytes(renderChain({scanlines, stage}, sources, 300, 200, a),
|
||||||
|
renderChain({scanlines, stage}, sources, 300, 200, b)),
|
||||||
|
name + " cascades reproducibly");
|
||||||
|
check(!chainNeedsSources({stage, scanlines}), "a chain led by " + name + " needs no sources");
|
||||||
|
|
||||||
|
// A canvas one pixel wide or high must not trip any of them up.
|
||||||
|
check(renderNamed(name, {}, 1, 1, 2, {}).width() == 1 &&
|
||||||
|
renderNamed(name, input, 1, 57, 2, {}).height() == 57 &&
|
||||||
|
renderNamed(name, input, 57, 1, 2, {}).width() == 57,
|
||||||
|
name + " copes with a sliver of a canvas");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapped slit shifts only rotate each row, so every row keeps its
|
||||||
|
// pixels, just not their places.
|
||||||
|
const auto wrapped = renderNamed("slitshift", input, 400, 300, 5, {{"shift_bands", 200L}});
|
||||||
|
bool rotated = true;
|
||||||
|
for (int y = 0; y < 300; ++y) {
|
||||||
|
std::vector<std::uint8_t> before(input[0].row(y), input[0].row(y) + 1200);
|
||||||
|
std::vector<std::uint8_t> after(wrapped.row(y), wrapped.row(y) + 1200);
|
||||||
|
std::sort(before.begin(), before.end());
|
||||||
|
std::sort(after.begin(), after.end());
|
||||||
|
rotated = rotated && before == after;
|
||||||
|
}
|
||||||
|
check(rotated, "a wrapped slit shift keeps every row's pixels");
|
||||||
|
const auto upright = renderNamed("slitshift", input, 400, 300, 5,
|
||||||
|
{{"shift_axis", std::string("vertical")}});
|
||||||
|
check(!sameBytes(upright, wrapped), "the vertical axis tears columns instead");
|
||||||
|
|
||||||
|
// Moving a plane of one colour moves nothing that can be seen.
|
||||||
|
const auto split = renderNamed("channelsplit", {solid(80, 60, 200, 90, 30)}, 80, 60, 3,
|
||||||
|
{{"split_mode", std::string("per_band")},
|
||||||
|
{"split_band_height", 7L}});
|
||||||
|
check(sameBytes(split, solid(80, 60, 200, 90, 30)), "a channel split keeps a flat colour");
|
||||||
|
const auto rc = renderNamed("channelsplit", input, 400, 300, 3,
|
||||||
|
{{"split_channels", std::string("red_cyan")}});
|
||||||
|
check(pixel(rc, 200, 150)[2] == pixel(input[0], 200, 150)[2],
|
||||||
|
"red_cyan leaves blue where a flat plane has it");
|
||||||
|
|
||||||
|
// With the thresholds wide open every row comes out sorted.
|
||||||
|
const auto sorted = renderNamed("pixelsort", {stripes(64, 16)}, 64, 16, 1,
|
||||||
|
{{"sort_low", 0.0}, {"sort_high", 1.0}});
|
||||||
|
bool ordered = true;
|
||||||
|
for (int y = 0; y < 16; ++y) {
|
||||||
|
for (int x = 1; x < 64; ++x) ordered = ordered && pixel(sorted, x - 1, y)[0] <= pixel(sorted, x, y)[0];
|
||||||
|
}
|
||||||
|
check(ordered, "an open pixel sort orders every row");
|
||||||
|
const auto downward = renderNamed("pixelsort", {stripes(64, 16)}, 64, 16, 1,
|
||||||
|
{{"sort_low", 0.0}, {"sort_high", 1.0},
|
||||||
|
{"sort_direction", std::string("vertical")}});
|
||||||
|
check(sameBytes(downward, stripes(64, 16)), "sorting uniform columns changes nothing");
|
||||||
|
|
||||||
|
// Every block repeated is the first block smeared across the row.
|
||||||
|
const auto smeared = renderNamed("corrupt", input, 64, 32, 2,
|
||||||
|
{{"corrupt_rate", 1.0},
|
||||||
|
{"corrupt_block", 16L},
|
||||||
|
{"corrupt_kinds", std::string("repeat")}});
|
||||||
|
bool periodic = true;
|
||||||
|
for (int y = 0; y < 32; ++y) {
|
||||||
|
for (int x = 16; x < 64; ++x) periodic = periodic && pixel(smeared, x, y) == pixel(smeared, x - 16, y);
|
||||||
|
}
|
||||||
|
check(periodic, "repeated blocks copy the block to their left");
|
||||||
|
const auto bled = renderNamed("corrupt", input, 400, 300, 2,
|
||||||
|
{{"corrupt_rate", 0.02}, {"corrupt_bleed", true}});
|
||||||
|
check(!sameBytes(bled, renderNamed("corrupt", input, 400, 300, 2, {{"corrupt_rate", 0.02}})),
|
||||||
|
"bleeding carries the damage further");
|
||||||
|
|
||||||
|
const auto cracked = renderNamed("shatter", {flat(200, 150, 128)}, 200, 150, 6,
|
||||||
|
{{"shatter_crack_colour", std::string("#ff0000")}});
|
||||||
|
bool red = false;
|
||||||
|
for (int y = 0; y < 150; ++y) {
|
||||||
|
for (int x = 0; x < 200; ++x) red = red || isRed(pixel(cracked, x, y));
|
||||||
|
}
|
||||||
|
check(red, "shatter draws its cracks in the crack colour");
|
||||||
|
|
||||||
|
check(!sameBytes(renderNamed("warp", input, 400, 300, 4, {{"warp_mode", std::string("swirl")}}),
|
||||||
|
renderNamed("warp", input, 400, 300, 4, {})),
|
||||||
|
"swirl flows differently from a plain push");
|
||||||
|
|
||||||
|
const auto poster = renderNamed("bitcrush", input, 400, 300, 3,
|
||||||
|
{{"crush_op", std::string("posterize")},
|
||||||
|
{"crush_coverage", 1.0}});
|
||||||
|
bool coarse = true;
|
||||||
|
for (std::size_t i = 0; i < poster.byteSize(); ++i) coarse = coarse && poster.data()[i] % 8 == 0;
|
||||||
|
check(coarse, "posterising three bits leaves multiples of eight");
|
||||||
|
const auto swapped = renderNamed("bitcrush", {solid(40, 40, 250, 120, 5)}, 40, 40, 3,
|
||||||
|
{{"crush_op", std::string("channel_swap")},
|
||||||
|
{"crush_coverage", 1.0}});
|
||||||
|
check(pixel(swapped, 20, 20) != std::array<int, 3>{250, 120, 5},
|
||||||
|
"a channel swap never leaves the order as it was");
|
||||||
|
|
||||||
|
// A shrunk, fully opaque echo lands the image's corner inside the
|
||||||
|
// canvas, while the border outside every echo stays the input's.
|
||||||
|
const auto tunnel = renderNamed("feedback", input, 400, 300, 1,
|
||||||
|
{{"echo_scale", 0.5}, {"echo_rotate", 0.0},
|
||||||
|
{"echo_drift", 0L}, {"echo_fade", 1.0}});
|
||||||
|
check(pixel(tunnel, 0, 0) == pixel(input[0], 0, 0), "the border outside the echoes is the input");
|
||||||
|
// Halfway to the corner reads the corner of the image at half size.
|
||||||
|
const auto near = pixel(tunnel, 100, 75);
|
||||||
|
check(std::abs(near[0] - pixel(input[0], 0, 0)[0]) < 4 &&
|
||||||
|
std::abs(near[1] - pixel(input[0], 0, 0)[1]) < 4,
|
||||||
|
"the echoes nest inside it");
|
||||||
|
}
|
||||||
|
|
||||||
std::cout << "chain\n";
|
std::cout << "chain\n";
|
||||||
{
|
{
|
||||||
std::vector<Image> sources = {gradient(1200, 800, 10), gradient(640, 480, 90)};
|
std::vector<Image> sources = {gradient(1200, 800, 10), gradient(640, 480, 90)};
|
||||||
|
|||||||
Reference in New Issue
Block a user