Add a tiles effect
The two-dimensional counterpart of scanlines: rectangular tiles cut at random positions from the source images, in three arrangements. A grid draws its column widths and row heights once so the cells stay aligned even when jittered; brick repacks every row on its own so the vertical seams do not meet; scatter fills the canvas from one of the images and drops loose tiles over it. The plan is drawn serially from the rng as scanlines does, but scattered tiles overlap, so they are painted in plan order on one thread: letting threads race for the same pixels would leave the output depending on their scheduling. A source smaller than a tile is repeated to fill the cell, sideways and then downwards, which keeps grid and brick gapless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
#include "effects/overlay.hpp"
|
||||
#include "effects/scanlines.hpp"
|
||||
#include "effects/text.hpp"
|
||||
#include "effects/tiles.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
@@ -18,6 +19,7 @@ const std::vector<EffectPtr>& effects() {
|
||||
std::make_shared<MonochromeEffect>(),
|
||||
std::make_shared<OverlayEffect>(),
|
||||
std::make_shared<TextEffect>(),
|
||||
std::make_shared<TilesEffect>(),
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
#include "effects/tiles.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "core/parallel.hpp"
|
||||
#include "effects/source_fill.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
const std::vector<ParamSpec>& TilesEffect::params() const {
|
||||
static const std::vector<ParamSpec> specs = {
|
||||
ParamSpec::makeChoice("tile_layout", "Layout", "grid", {"grid", "brick", "scatter"},
|
||||
"grid lines the tiles up in rows and columns; brick packs every row "
|
||||
"on its own so the seams do not meet; scatter drops loose tiles over "
|
||||
"a filled background, overlapping."),
|
||||
ParamSpec::makeInt("tile_width", "Tile width", 96, 1, 2048,
|
||||
"Width in pixels of each cut-out tile."),
|
||||
ParamSpec::makeInt("tile_height", "Tile height", 96, 1, 2048,
|
||||
"Height in pixels of each cut-out tile."),
|
||||
ParamSpec::makeInt("tile_jitter", "Size jitter", 0, 0, 512,
|
||||
"Random +/- variation added to both tile sizes (0 = uniform)."),
|
||||
ParamSpec::makeInt("tile_count", "Tile count", 64, 1, 4096,
|
||||
"How many tiles are dropped on the canvas; scattered layout only."),
|
||||
ParamSpec::makeBool("tile_keep_order", "Cycle images in order", false,
|
||||
"Take tiles from the images in order instead of picking randomly."),
|
||||
};
|
||||
return specs;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// One cut-out: `w` by `h` pixels taken from source `sourceIndex` at (sx, sy)
|
||||
// and laid down at (dstX, dstY).
|
||||
struct Tile {
|
||||
int dstX;
|
||||
int dstY;
|
||||
int w;
|
||||
int h;
|
||||
int sourceIndex;
|
||||
int sx;
|
||||
int sy;
|
||||
};
|
||||
|
||||
// Splits `span` into consecutive pieces of `base` pixels give or take `jitter`,
|
||||
// the last one clipped so that the pieces cover the span exactly.
|
||||
std::vector<int> planSpans(Rng& rng, int base, int jitter, int span) {
|
||||
std::vector<int> spans;
|
||||
spans.reserve(static_cast<std::size_t>(span / base) + 8);
|
||||
int used = 0;
|
||||
while (used < span) {
|
||||
int size = base;
|
||||
if (jitter > 0) size += static_cast<int>(rng.inRange(-jitter, jitter));
|
||||
size = std::max(1, std::min(size, span - used));
|
||||
spans.push_back(size);
|
||||
used += size;
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
void paintTile(Image& canvas, const Image& src, const Tile& tile) {
|
||||
const int copyWidth = std::min(tile.w, src.width() - tile.sx);
|
||||
const int copyHeight = std::min(tile.h, src.height() - tile.sy);
|
||||
const std::size_t copyBytes = static_cast<std::size_t>(copyWidth) * 3;
|
||||
const std::size_t total = static_cast<std::size_t>(tile.w) * 3;
|
||||
const std::size_t offset = static_cast<std::size_t>(tile.dstX) * 3;
|
||||
|
||||
for (int line = 0; line < copyHeight; ++line) {
|
||||
const std::uint8_t* in = src.row(tile.sy + line) + static_cast<std::size_t>(tile.sx) * 3;
|
||||
std::uint8_t* out = canvas.row(tile.dstY + line) + offset;
|
||||
|
||||
std::memcpy(out, in, copyBytes);
|
||||
// Repeat the cut sideways when the source is narrower than the tile,
|
||||
// doubling the filled span on each pass.
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Same again downwards, out of the lines already written.
|
||||
for (int line = copyHeight; line < tile.h; ++line) {
|
||||
std::memcpy(canvas.row(tile.dstY + line) + offset,
|
||||
canvas.row(tile.dstY + (line % copyHeight)) + offset, total);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Image TilesEffect::render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||
const ParamValues& values) const {
|
||||
if (sources.empty()) throw EffectError("No source images.");
|
||||
if (width < 1 || height < 1) throw EffectError("Canvas size must be at least 1x1 pixels.");
|
||||
|
||||
const std::string layout = paramChoice(values, "tile_layout", "grid");
|
||||
const int tileWidth = static_cast<int>(std::max(1L, paramInt(values, "tile_width", 96)));
|
||||
const int tileHeight = static_cast<int>(std::max(1L, paramInt(values, "tile_height", 96)));
|
||||
const int jitter = static_cast<int>(std::max(0L, paramInt(values, "tile_jitter", 0)));
|
||||
const long count = std::max(1L, paramInt(values, "tile_count", 64));
|
||||
const bool keepOrder = paramBool(values, "tile_keep_order", false);
|
||||
|
||||
Image canvas(width, height);
|
||||
|
||||
// The whole plan is drawn sequentially so that a seed always gives the same
|
||||
// layout; only the pixel copying afterwards is parallelised.
|
||||
std::size_t index = 0;
|
||||
auto cut = [&](int dstX, int dstY, int w, int h) {
|
||||
const int sourceIndex = keepOrder ? static_cast<int>(index++ % sources.size())
|
||||
: static_cast<int>(rng.below(sources.size() - 1));
|
||||
const Image& src = sources[static_cast<std::size_t>(sourceIndex)];
|
||||
const int sx = static_cast<int>(rng.inRange(0, std::max(0, src.width() - w)));
|
||||
const int sy = static_cast<int>(rng.inRange(0, std::max(0, src.height() - h)));
|
||||
return Tile{dstX, dstY, w, h, sourceIndex, sx, sy};
|
||||
};
|
||||
|
||||
std::vector<Tile> plan;
|
||||
if (layout == "scatter") {
|
||||
// Loose tiles leave gaps, so the canvas starts as one of the images
|
||||
// rather than as black.
|
||||
const std::size_t backdrop = rng.below(sources.size() - 1);
|
||||
const Image& src = sources[backdrop];
|
||||
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);
|
||||
|
||||
plan.reserve(static_cast<std::size_t>(count));
|
||||
for (long i = 0; i < count; ++i) {
|
||||
int w = tileWidth;
|
||||
int h = tileHeight;
|
||||
if (jitter > 0) {
|
||||
w += static_cast<int>(rng.inRange(-jitter, jitter));
|
||||
h += static_cast<int>(rng.inRange(-jitter, jitter));
|
||||
}
|
||||
const int dstX = static_cast<int>(rng.inRange(0, width - 1));
|
||||
const int dstY = static_cast<int>(rng.inRange(0, height - 1));
|
||||
w = std::max(1, std::min(w, width - dstX));
|
||||
h = std::max(1, std::min(h, height - dstY));
|
||||
plan.push_back(cut(dstX, dstY, w, h));
|
||||
}
|
||||
} else {
|
||||
const std::vector<int> rows = planSpans(rng, tileHeight, jitter, height);
|
||||
// In a grid every row is cut at the same columns; in the brick layout
|
||||
// each row is packed on its own, so the vertical seams do not line up.
|
||||
std::vector<int> columns;
|
||||
if (layout != "brick") columns = planSpans(rng, tileWidth, jitter, width);
|
||||
|
||||
plan.reserve(rows.size() * (columns.empty() ? 8 : columns.size()));
|
||||
int y = 0;
|
||||
for (const int rowHeight : rows) {
|
||||
if (layout == "brick") columns = planSpans(rng, tileWidth, jitter, width);
|
||||
int x = 0;
|
||||
for (const int cellWidth : columns) {
|
||||
plan.push_back(cut(x, y, cellWidth, rowHeight));
|
||||
x += cellWidth;
|
||||
}
|
||||
y += rowHeight;
|
||||
}
|
||||
}
|
||||
|
||||
if (layout == "scatter") {
|
||||
// Scattered tiles overlap, so they are painted in plan order on one
|
||||
// thread: letting threads race for the same pixels would make the
|
||||
// output depend on their scheduling.
|
||||
for (const Tile& tile : plan) {
|
||||
paintTile(canvas, sources[static_cast<std::size_t>(tile.sourceIndex)], tile);
|
||||
}
|
||||
} else {
|
||||
parallelFor(plan.size(), [&](std::size_t begin, std::size_t end) {
|
||||
for (std::size_t i = begin; i < end; ++i) {
|
||||
paintTile(canvas, sources[static_cast<std::size_t>(plan[i].sourceIndex)], plan[i]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/effect.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
// Covers the canvas with rectangular tiles cut at random positions from the
|
||||
// source images: the two-dimensional counterpart of the scanlines effect.
|
||||
class TilesEffect : public Effect {
|
||||
public:
|
||||
std::string name() const override { return "tiles"; }
|
||||
std::string title() const override { return "Tiles"; }
|
||||
std::string description() const override {
|
||||
return "Cuts rectangular tiles at random positions from the source images and lays "
|
||||
"them over the canvas, in a grid, in offset rows or scattered about.";
|
||||
}
|
||||
const std::vector<ParamSpec>& params() const override;
|
||||
|
||||
Image render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||
const ParamValues& values) const override;
|
||||
};
|
||||
|
||||
} // namespace effector
|
||||
Reference in New Issue
Block a user