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
+33
View File
@@ -4,8 +4,10 @@
#include <array>
#include <atomic>
#include <clocale>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
@@ -2079,6 +2081,37 @@ int main() {
frozenSwap.values["tileswap_rate"] = 0.0;
check(sameBytes(frameOf(frozenSwap, 0), frameOf(frozenSwap, 5)),
"at a rate of 0 it holds still");
// How many pixels ever show something other than frame 0.
auto moving = [&](const ChainStage& at) {
const auto first = frameOf(at, 0);
std::vector<bool> moved(static_cast<std::size_t>(80 * 48), false);
for (int index = 1; index < 8; ++index) {
const auto frame = frameOf(at, index);
for (int y = 0; y < 48; ++y) {
for (int x = 0; x < 80; ++x) {
if (std::memcmp(frame.row(y) + x * 3, first.row(y) + x * 3, 3) != 0) {
moved[static_cast<std::size_t>(y * 80 + x)] = true;
}
}
}
}
return std::count(moved.begin(), moved.end(), true);
};
ChainStage slowSwap = swapStage;
slowSwap.values["tileswap_rate"] = 0.3;
const auto slowMoving = moving(slowSwap);
check(slowMoving > 0 && slowMoving < moving(swapStage),
"a rate below two swaps a clip moves only some of the tiles");
const auto& rate = swap->params().back();
check(rate.onSlider && rate.curve > 1.0, "its swap rate is a curved slider");
check(rate.sliderValue(0.0) == 0.0 && rate.sliderValue(1.0) == 5.0 &&
rate.sliderValue(0.5) < 2.5,
"which spans the whole range and gives the low end more travel");
check(std::abs(rate.sliderValue(rate.sliderPosition(0.37)) - 0.37) < 1e-9 &&
rate.sliderValue(0.123456) * 100.0 ==
std::round(rate.sliderValue(0.123456) * 100.0),
"a value finds its place on the slider, and a place rounds to the step");
ChainStage scattered = swapStage;
scattered.values["tileswap_layout"] = std::string("scatter");
ChainStage bricks = swapStage;
+6 -5
View File
@@ -658,17 +658,18 @@ void testSlider(Gtk::Window& window, const std::filesystem::path& dir) {
check(scales.size() == 1, "a param marked as a slider is shown as one");
if (scales.size() != 1) return;
auto* scale = scales.front();
auto adjustment = scale->get_adjustment();
const auto& spec = effector::effectByName("tileswap")->params().back();
// Written the way the locale writes numbers, as the interface does.
const auto mark = [](double at) {
return std::string(Glib::ustring::format(std::fixed, std::setprecision(2), at));
};
check(adjustment->get_lower() == 0.0 && adjustment->get_upper() == 5.0 &&
showsText(*scale, mark(0.0)) && showsText(*scale, mark(5.0)),
check(showsText(*scale, mark(0.0)) && showsText(*scale, mark(5.0)),
"with its range marked at both ends");
check(scale->get_value() == 0.5, "and shows the value saved");
check(spec.sliderValue(scale->get_value()) == 0.5, "and shows the value saved");
check(scale->get_value() > 0.5 / 5.0 + 0.3,
"a curved slider gives the low end of its range more of its travel");
scale->set_value(0.05);
scale->set_value(spec.sliderPosition(0.05));
pump(100);
window.activate_action("win.save-project");
pump(100);