Files
effector/src/effects/tileswap.cpp
T
simonandClaude Opus 5.5 3c8c5bed8c Make Tile swap slow at a low Swap rate, on a curved slider
A tile has to come back to the cut it started with for the clip to
loop, so every tile swapped at least twice a clip, and below that
rounding left every tile still: the bottom of the slider was either
frozen or busy. Swap rate is now the average per tile: below two swaps
a clip only a share of the tiles swap (twice each), fewer the lower it
goes, and above it each tile rounds up or down by its own draw.

ParamSpec::slider() takes an optional curve: the scale runs over the
slider's travel and sliderValue/sliderPosition map it onto the number
as position^curve. Swap rate uses a curve of 3, so the low end gets
most of the slider; the command line and project file are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:11:28 +02:00

172 lines
7.4 KiB
C++

#include "effects/tileswap.hpp"
#include <algorithm>
#include <cmath>
#include <vector>
#include "effects/source_fill.hpp"
#include "effects/tile_plan.hpp"
namespace effector {
const std::vector<ParamSpec>& TileSwapEffect::params() const {
static const std::vector<ParamSpec> 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<int>(std::max(1L, paramInt(values, "tileswap_width", 96)))),
tileHeight(static_cast<int>(std::max(1L, paramInt(values, "tileswap_height", 96)))),
jitter(static_cast<int>(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<double>(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<Image>& 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<Cell> 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<int>(steady.inRange(0, std::max(0, src.width() - width)));
const int sy = static_cast<int>(steady.inRange(0, std::max(0, src.height() - height)));
fillFromSource(canvas, src, sx, sy);
cells.reserve(static_cast<std::size_t>(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<int>(steady.inRange(-settings.jitter, settings.jitter));
h += static_cast<int>(steady.inRange(-settings.jitter, settings.jitter));
}
const int x = static_cast<int>(steady.inRange(0, width - 1));
const int y = static_cast<int>(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<int> rows = planSpans(steady, settings.tileHeight, settings.jitter, height);
std::vector<int> 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<long>(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<Tile> 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<long>(std::floor(at.seconds() * swaps / length + cell.phase));
turn = static_cast<std::uint64_t>(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<int>(cut.inRange(0, std::max(0, src.width() - cell.w)));
const int sy = static_cast<int>(cut.inRange(0, std::max(0, src.height() - cell.h)));
plan.push_back(Tile{cell.x, cell.y, cell.w, cell.h, static_cast<int>(sourceIndex), sx, sy});
}
paintTiles(canvas, sources, plan, scatter);
return canvas;
}
} // namespace effector