#include "effects/tileswap.hpp" #include #include #include #include "effects/source_fill.hpp" #include "effects/tile_plan.hpp" namespace effector { const std::vector& TileSwapEffect::params() const { static const std::vector specs = { ParamSpec::makeChoice("tileswap_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("tileswap_width", "Tile width", 96, 1, 2048, "Width in pixels of each cut-out tile."), ParamSpec::makeInt("tileswap_height", "Tile height", 96, 1, 2048, "Height in pixels of each cut-out tile."), ParamSpec::makeInt("tileswap_jitter", "Size jitter", 0, 0, 512, "Random +/- variation added to both tile sizes (0 = uniform)."), ParamSpec::makeInt("tileswap_count", "Tile count", 64, 1, 4096, "How many tiles are dropped on the canvas; scattered layout only."), ParamSpec::makeBool("tileswap_keep_order", "Cycle images in order", false, "Take tiles from the images in order instead of picking randomly."), ParamSpec::makeDouble("tileswap_rate", "Swap rate", 1.0, 0.0, 5.0, "Swaps per second of each tile, on average; 0 for none. Below two " "swaps in the clip only some of the tiles swap, fewer the lower it " "goes, so that the clip still loops without a jump.", 0.01, 2) .slider(3.0), }; return specs; } namespace { struct Settings { std::string layout; int tileWidth; int tileHeight; int jitter; long count; bool keepOrder; double rate; explicit Settings(const ParamValues& values) : layout(paramChoice(values, "tileswap_layout", "grid")), tileWidth(static_cast(std::max(1L, paramInt(values, "tileswap_width", 96)))), tileHeight(static_cast(std::max(1L, paramInt(values, "tileswap_height", 96)))), jitter(static_cast(std::max(0L, paramInt(values, "tileswap_jitter", 0)))), count(std::max(1L, paramInt(values, "tileswap_count", 64))), keepOrder(paramBool(values, "tileswap_keep_order", false)), rate(std::max(0.0, paramDouble(values, "tileswap_rate", 1.0))) {} }; double unit(Rng& rng) { return static_cast(rng.next() >> 11) * 0x1.0p-53; } // Where a tile sits for the whole clip, when in its swaps it turns over, the // seed its cuts come from, and the draw that settles how often it swaps. struct Cell { int x; int y; int w; int h; double phase; std::uint64_t seed; double share; }; } // namespace Image TileSwapEffect::compose(const std::vector& sources, int width, int height, const ParamValues& values, const Moment& at) const { const Settings settings(values); const bool scatter = settings.layout == "scatter"; Image canvas(width, height); // What stays put for the whole clip: the cells, and for loose tiles the // backdrop they lie on. Rng steady = at.steady(); std::vector cells; auto addCell = [&](int x, int y, int w, int h) { cells.push_back(Cell{x, y, w, h, 0.0, 0, 0.0}); }; if (scatter) { const Image& src = sources[steady.below(sources.size() - 1)]; const int sx = static_cast(steady.inRange(0, std::max(0, src.width() - width))); const int sy = static_cast(steady.inRange(0, std::max(0, src.height() - height))); fillFromSource(canvas, src, sx, sy); cells.reserve(static_cast(settings.count)); for (long i = 0; i < settings.count; ++i) { int w = settings.tileWidth; int h = settings.tileHeight; if (settings.jitter > 0) { w += static_cast(steady.inRange(-settings.jitter, settings.jitter)); h += static_cast(steady.inRange(-settings.jitter, settings.jitter)); } const int x = static_cast(steady.inRange(0, width - 1)); const int y = static_cast(steady.inRange(0, height - 1)); addCell(x, y, std::max(1, std::min(w, width - x)), std::max(1, std::min(h, height - y))); } } else { const bool brick = settings.layout == "brick"; const std::vector rows = planSpans(steady, settings.tileHeight, settings.jitter, height); std::vector columns; if (!brick) columns = planSpans(steady, settings.tileWidth, settings.jitter, width); int y = 0; for (const int rowHeight : rows) { if (brick) columns = planSpans(steady, settings.tileWidth, settings.jitter, width); int x = 0; for (const int cellWidth : columns) { addCell(x, y, cellWidth, rowHeight); x += cellWidth; } y += rowHeight; } } for (Cell& cell : cells) { cell.phase = unit(steady); cell.seed = steady.next(); cell.share = unit(steady); } // How often each tile swaps in one pass of the clip, `each` on average. A // tile has to come back to the cut it started with, so one that swaps at // all swaps at least twice; below that a share of the tiles swap twice and // the rest hold still, so a low rate thins out the tiles that move instead // of moving every one of them. Above it, each tile rounds `each` up or // down by its own draw. const double length = at.length(); const double each = settings.rate * length; auto swapsOf = [&](const Cell& cell) -> long { if (each <= 0.0) return 0; if (each < 2.0) return cell.share < each / 2.0 ? 2 : 0; const double whole = std::floor(each); return static_cast(whole) + (cell.share < each - whole ? 1 : 0); }; // What a tile shows depends only on the tile and how many times it has // swapped, counted round the clip so that the frame after the last shows // what the first does; so it holds still between swaps and any frame can // be drawn on its own. std::vector plan; plan.reserve(cells.size()); for (std::size_t i = 0; i < cells.size(); ++i) { const Cell& cell = cells[i]; const long swaps = swapsOf(cell); std::uint64_t turn = 0; if (swaps > 0) { const auto passed = static_cast(std::floor(at.seconds() * swaps / length + cell.phase)); turn = static_cast(passed % swaps); } Rng cut(cell.seed + turn * 0x9e3779b97f4a7c15ULL); const std::size_t sourceIndex = settings.keepOrder ? (i + turn) % sources.size() : cut.below(sources.size() - 1); const Image& src = sources[sourceIndex]; const int sx = static_cast(cut.inRange(0, std::max(0, src.width() - cell.w))); const int sy = static_cast(cut.inRange(0, std::max(0, src.height() - cell.h))); plan.push_back(Tile{cell.x, cell.y, cell.w, cell.h, static_cast(sourceIndex), sx, sy}); } paintTiles(canvas, sources, plan, scatter); return canvas; } } // namespace effector