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>
This commit is contained in:
simon
2026-10-02 01:11:28 +02:00
co-authored by Claude Opus 5.5
parent ef8f728126
commit 3c8c5bed8c
8 changed files with 123 additions and 38 deletions
+30 -18
View File
@@ -26,10 +26,11 @@ const std::vector<ParamSpec>& TileSwapEffect::params() const {
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, rounded so that the clip loops "
"without a jump; 0 for none.",
"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(),
.slider(3.0),
};
return specs;
}
@@ -57,8 +58,8 @@ struct Settings {
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, and
// the seed its cuts come from.
// 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;
@@ -66,6 +67,7 @@ struct Cell {
int h;
double phase;
std::uint64_t seed;
double share;
};
} // namespace
@@ -81,7 +83,7 @@ Image TileSwapEffect::compose(const std::vector<Image>& sources, int width, int
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});
cells.push_back(Cell{x, y, w, h, 0.0, 0, 0.0});
};
if (scatter) {
const Image& src = sources[steady.below(sources.size() - 1)];
@@ -120,27 +122,37 @@ Image TileSwapEffect::compose(const std::vector<Image>& sources, int width, int
for (Cell& cell : cells) {
cell.phase = unit(steady);
cell.seed = steady.next();
cell.share = unit(steady);
}
// Where this frame is in time: each tile has swapped a whole number of
// times, counted round the clip so that the frame after the last shows
// what the first does.
long swaps = 0;
double perSecond = 0.0;
if (settings.rate > 0.0) {
perSecond = at.loopRate(settings.rate);
swaps = std::max(1L, std::lround(perSecond * at.length()));
}
// 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 its swap, so it holds
// still between swaps and any frame can be drawn on its own.
// 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() * perSecond + cell.phase));
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);