#include "effects/tiles.hpp" #include #include #include #include "core/parallel.hpp" #include "effects/source_fill.hpp" namespace effector { const std::vector& TilesEffect::params() const { static const std::vector 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 planSpans(Rng& rng, int base, int jitter, int span) { std::vector spans; spans.reserve(static_cast(span / base) + 8); int used = 0; while (used < span) { int size = base; if (jitter > 0) size += static_cast(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(copyWidth) * 3; const std::size_t total = static_cast(tile.w) * 3; const std::size_t offset = static_cast(tile.dstX) * 3; for (int line = 0; line < copyHeight; ++line) { const std::uint8_t* in = src.row(tile.sy + line) + static_cast(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& 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(std::max(1L, paramInt(values, "tile_width", 96))); const int tileHeight = static_cast(std::max(1L, paramInt(values, "tile_height", 96))); const int jitter = static_cast(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(index++ % sources.size()) : static_cast(rng.below(sources.size() - 1)); const Image& src = sources[static_cast(sourceIndex)]; const int sx = static_cast(rng.inRange(0, std::max(0, src.width() - w))); const int sy = static_cast(rng.inRange(0, std::max(0, src.height() - h))); return Tile{dstX, dstY, w, h, sourceIndex, sx, sy}; }; std::vector 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(rng.inRange(0, std::max(0, src.width() - width))); const int sy = static_cast(rng.inRange(0, std::max(0, src.height() - height))); fillFromSource(canvas, src, sx, sy); plan.reserve(static_cast(count)); for (long i = 0; i < count; ++i) { int w = tileWidth; int h = tileHeight; if (jitter > 0) { w += static_cast(rng.inRange(-jitter, jitter)); h += static_cast(rng.inRange(-jitter, jitter)); } const int dstX = static_cast(rng.inRange(0, width - 1)); const int dstY = static_cast(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 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 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(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(plan[i].sourceIndex)], plan[i]); } }); } return canvas; } } // namespace effector