Show Tile swap's Swap rate as a slider, and let it go slower
A new ParamSpec::slider() marks an Int or Double param to be shown in the interface as a Gtk::Scale with its minimum and maximum marked under the ends, rather than a spin button; the command line and the project file are unchanged. Swap rate uses it, and now runs from 0 to 5 in steps of 0.01 (was 0 to 30 in steps of 0.1): with every tile swapping on its own, even 0.1 swapped the canvas many times a second. Version 0.8.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,13 @@ Every release of Effector, newest first. The version lives in one place, the
|
|||||||
`project()` line of `CMakeLists.txt`, and each release is a `vX.Y.Z` tag on the
|
`project()` line of `CMakeLists.txt`, and each release is a `vX.Y.Z` tag on the
|
||||||
commit that made it. The interface shows this file under Changelog in its menu.
|
commit that made it. The interface shows this file under Changelog in its menu.
|
||||||
|
|
||||||
|
## [0.8.2] - 2026-09-30
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- The Swap rate of Tile swap is a slider marked with its range, and goes
|
||||||
|
down in steps of 0.01 rather than 0.1, so a clip can swap its tiles much
|
||||||
|
more slowly. Its top end is now 5 swaps a second (it was 30).
|
||||||
|
|
||||||
## [0.8.1] - 2026-09-26
|
## [0.8.1] - 2026-09-26
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ travel as `ParamValues` (a
|
|||||||
`map<string, variant<long,double,bool,string>>`), read back with
|
`map<string, variant<long,double,bool,string>>`), read back with
|
||||||
`paramInt`/`paramDouble`/`paramBool`/`paramChoice`.
|
`paramInt`/`paramDouble`/`paramBool`/`paramChoice`.
|
||||||
|
|
||||||
|
An Int or Double param marked `.slider()` is shown in
|
||||||
|
the interface as a slider with its minimum and maximum marked instead of a spin
|
||||||
|
button; nothing else changes.
|
||||||
|
|
||||||
A param can also be marked `.styled()`, which says a *named style* carries it
|
A param can also be marked `.styled()`, which says a *named style* carries it
|
||||||
rather than the stage it sits on: the font and the colour of a text stage, but
|
rather than the stage it sits on: the font and the colour of a text stage, but
|
||||||
never its words or its placement. The pool a style comes from is named by
|
never its words or its placement. The pool a style comes from is named by
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.20)
|
cmake_minimum_required(VERSION 3.20)
|
||||||
project(effector VERSION 0.8.1 LANGUAGES CXX)
|
project(effector VERSION 0.8.2 LANGUAGES CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# so pkgver must follow the project() line of CMakeLists.txt (the version core
|
# so pkgver must follow the project() line of CMakeLists.txt (the version core
|
||||||
# test checks it). From this folder: makepkg -si
|
# test checks it). From this folder: makepkg -si
|
||||||
pkgname=effector
|
pkgname=effector
|
||||||
pkgver=0.8.1
|
pkgver=0.8.2
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc='Image effects app: a GTK4 interface and a headless CLI over pluggable effects'
|
pkgdesc='Image effects app: a GTK4 interface and a headless CLI over pluggable effects'
|
||||||
arch=('x86_64' 'aarch64')
|
arch=('x86_64' 'aarch64')
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ ParamSpec ParamSpec::styled() const {
|
|||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ParamSpec ParamSpec::slider() const {
|
||||||
|
ParamSpec copy = *this;
|
||||||
|
copy.onSlider = true;
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
ParamSpec ParamSpec::makeInt(std::string key, std::string label, long defaultValue, long minimum,
|
ParamSpec ParamSpec::makeInt(std::string key, std::string label, long defaultValue, long minimum,
|
||||||
long maximum, std::string help, long step) {
|
long maximum, std::string help, long step) {
|
||||||
ParamSpec spec;
|
ParamSpec spec;
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ struct ParamSpec {
|
|||||||
// Whether a named style carries this param rather than the stage it sits
|
// Whether a named style carries this param rather than the stage it sits
|
||||||
// on: see core/style.hpp.
|
// on: see core/style.hpp.
|
||||||
bool inStyle = false;
|
bool inStyle = false;
|
||||||
|
// Whether the interface shows this number as a slider marked with its
|
||||||
|
// range rather than as a spin button: see slider().
|
||||||
|
bool onSlider = false;
|
||||||
|
|
||||||
std::string flag() const; // "strip_height" -> "--strip-height"
|
std::string flag() const; // "strip_height" -> "--strip-height"
|
||||||
|
|
||||||
@@ -35,6 +38,11 @@ struct ParamSpec {
|
|||||||
// signatures they have.
|
// signatures they have.
|
||||||
ParamSpec styled() const;
|
ParamSpec styled() const;
|
||||||
|
|
||||||
|
// A copy of this Int or Double spec shown as a slider in the interface, for
|
||||||
|
// a number whose place between its minimum and maximum says more than its
|
||||||
|
// digits do. The command line and the project file are unchanged.
|
||||||
|
ParamSpec slider() const;
|
||||||
|
|
||||||
static ParamSpec makeInt(std::string key, std::string label, long defaultValue, long minimum,
|
static ParamSpec makeInt(std::string key, std::string label, long defaultValue, long minimum,
|
||||||
long maximum, std::string help = {}, long step = 1);
|
long maximum, std::string help = {}, long step = 1);
|
||||||
static ParamSpec makeDouble(std::string key, std::string label, double defaultValue,
|
static ParamSpec makeDouble(std::string key, std::string label, double defaultValue,
|
||||||
|
|||||||
@@ -25,10 +25,11 @@ const std::vector<ParamSpec>& TileSwapEffect::params() const {
|
|||||||
"How many tiles are dropped on the canvas; scattered layout only."),
|
"How many tiles are dropped on the canvas; scattered layout only."),
|
||||||
ParamSpec::makeBool("tileswap_keep_order", "Cycle images in order", false,
|
ParamSpec::makeBool("tileswap_keep_order", "Cycle images in order", false,
|
||||||
"Take tiles from the images in order instead of picking randomly."),
|
"Take tiles from the images in order instead of picking randomly."),
|
||||||
ParamSpec::makeDouble("tileswap_rate", "Swap rate", 1.0, 0.0, 30.0,
|
ParamSpec::makeDouble("tileswap_rate", "Swap rate", 1.0, 0.0, 5.0,
|
||||||
"Swaps per second of each tile, rounded so that the clip loops "
|
"Swaps per second of each tile, rounded so that the clip loops "
|
||||||
"without a jump; 0 for none.",
|
"without a jump; 0 for none.",
|
||||||
0.1, 1),
|
0.01, 2)
|
||||||
|
.slider(),
|
||||||
};
|
};
|
||||||
return specs;
|
return specs;
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-13
@@ -5,6 +5,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <iomanip>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
@@ -74,14 +75,14 @@ Gtk::Label* sectionLabel(const char* text) {
|
|||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A GtkSpinButton takes every wheel turn over it: it grabs the focus and
|
// A GtkSpinButton (or a GtkScale) takes every wheel turn over it: it grabs the
|
||||||
// spins its value, so scrolling down a column of params stops dead at the
|
// focus and spins its value, so scrolling down a column of params stops dead at
|
||||||
// first spin under the pointer and quietly changes it. This hands the wheel to
|
// the first spin under the pointer and quietly changes it. This hands the wheel to
|
||||||
// the view instead unless the spin already has the focus -- the user clicked
|
// the view instead unless the spin already has the focus -- the user clicked
|
||||||
// into it and means to adjust it. It runs in the capture phase, ahead of the
|
// into it and means to adjust it. It runs in the capture phase, ahead of the
|
||||||
// spin's own controller, and so has to scroll the enclosing ScrolledWindow
|
// spin's own controller, and so has to scroll the enclosing ScrolledWindow
|
||||||
// itself: stopping the event there stops it for the window as well.
|
// itself: stopping the event there stops it for the window as well.
|
||||||
void scrollPastUnlessFocused(Gtk::SpinButton& spin) {
|
void scrollPastUnlessFocused(Gtk::Widget& spin) {
|
||||||
auto scroll = Gtk::EventControllerScroll::create();
|
auto scroll = Gtk::EventControllerScroll::create();
|
||||||
scroll->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
|
scroll->set_propagation_phase(Gtk::PropagationPhase::CAPTURE);
|
||||||
scroll->set_flags(Gtk::EventControllerScroll::Flags::VERTICAL);
|
scroll->set_flags(Gtk::EventControllerScroll::Flags::VERTICAL);
|
||||||
@@ -950,14 +951,39 @@ void EffectorWindow::buildParamWidgets() {
|
|||||||
}
|
}
|
||||||
case ParamSpec::Type::Int:
|
case ParamSpec::Type::Int:
|
||||||
case ParamSpec::Type::Double: {
|
case ParamSpec::Type::Double: {
|
||||||
|
const double value =
|
||||||
|
spec.type == ParamSpec::Type::Int
|
||||||
|
? static_cast<double>(
|
||||||
|
paramInt(values, spec.key, static_cast<long>(spec.defaultValue)))
|
||||||
|
: paramDouble(values, spec.key, spec.defaultValue);
|
||||||
|
if (spec.onSlider) {
|
||||||
|
// The ends are marked with their values, so where the
|
||||||
|
// number sits in its range can be read at a glance.
|
||||||
|
const int digits = spec.type == ParamSpec::Type::Int ? 0 : spec.digits;
|
||||||
|
auto scale = std::make_unique<Gtk::Scale>(Gtk::Orientation::HORIZONTAL);
|
||||||
|
scale->set_range(spec.minimum, spec.maximum);
|
||||||
|
scale->set_increments(spec.step, spec.step * 10);
|
||||||
|
scale->set_round_digits(digits);
|
||||||
|
scale->set_digits(digits);
|
||||||
|
scale->set_draw_value(true);
|
||||||
|
scale->set_value_pos(Gtk::PositionType::RIGHT);
|
||||||
|
const auto mark = [digits](double at) {
|
||||||
|
return Glib::ustring::format(std::fixed, std::setprecision(digits), at);
|
||||||
|
};
|
||||||
|
scale->add_mark(spec.minimum, Gtk::PositionType::BOTTOM, mark(spec.minimum));
|
||||||
|
scale->add_mark(spec.maximum, Gtk::PositionType::BOTTOM, mark(spec.maximum));
|
||||||
|
scale->set_value(value);
|
||||||
|
scale->signal_value_changed().connect(
|
||||||
|
sigc::mem_fun(*this, &EffectorWindow::onParamChanged));
|
||||||
|
scrollPastUnlessFocused(*scale);
|
||||||
|
widget = std::move(scale);
|
||||||
|
break;
|
||||||
|
}
|
||||||
auto spin = std::make_unique<Gtk::SpinButton>();
|
auto spin = std::make_unique<Gtk::SpinButton>();
|
||||||
spin->set_range(spec.minimum, spec.maximum);
|
spin->set_range(spec.minimum, spec.maximum);
|
||||||
spin->set_increments(spec.step, spec.step * 10);
|
spin->set_increments(spec.step, spec.step * 10);
|
||||||
spin->set_digits(spec.type == ParamSpec::Type::Int ? 0 : spec.digits);
|
spin->set_digits(spec.type == ParamSpec::Type::Int ? 0 : spec.digits);
|
||||||
spin->set_value(spec.type == ParamSpec::Type::Int
|
spin->set_value(value);
|
||||||
? static_cast<double>(paramInt(values, spec.key,
|
|
||||||
static_cast<long>(spec.defaultValue)))
|
|
||||||
: paramDouble(values, spec.key, spec.defaultValue));
|
|
||||||
spin->signal_value_changed().connect(
|
spin->signal_value_changed().connect(
|
||||||
sigc::mem_fun(*this, &EffectorWindow::onParamChanged));
|
sigc::mem_fun(*this, &EffectorWindow::onParamChanged));
|
||||||
scrollPastUnlessFocused(*spin);
|
scrollPastUnlessFocused(*spin);
|
||||||
@@ -1065,12 +1091,17 @@ ParamValues EffectorWindow::paramValues() const {
|
|||||||
values[entry.spec.key] = static_cast<Gtk::Switch*>(entry.widget)->get_active();
|
values[entry.spec.key] = static_cast<Gtk::Switch*>(entry.widget)->get_active();
|
||||||
break;
|
break;
|
||||||
case ParamSpec::Type::Int:
|
case ParamSpec::Type::Int:
|
||||||
values[entry.spec.key] =
|
case ParamSpec::Type::Double: {
|
||||||
static_cast<long>(static_cast<Gtk::SpinButton*>(entry.widget)->get_value());
|
const double value =
|
||||||
break;
|
entry.spec.onSlider ? static_cast<Gtk::Scale*>(entry.widget)->get_value()
|
||||||
case ParamSpec::Type::Double:
|
: static_cast<Gtk::SpinButton*>(entry.widget)->get_value();
|
||||||
values[entry.spec.key] = static_cast<Gtk::SpinButton*>(entry.widget)->get_value();
|
if (entry.spec.type == ParamSpec::Type::Int) {
|
||||||
|
values[entry.spec.key] = std::lround(value);
|
||||||
|
} else {
|
||||||
|
values[entry.spec.key] = value;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case ParamSpec::Type::Path:
|
case ParamSpec::Type::Path:
|
||||||
case ParamSpec::Type::Text:
|
case ParamSpec::Type::Text:
|
||||||
values[entry.spec.key] =
|
values[entry.spec.key] =
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <iomanip>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -633,6 +634,50 @@ void testFont(Gtk::Window& window, const std::filesystem::path& dir) {
|
|||||||
"a family chosen there is saved with the project");
|
"a family chosen there is saved with the project");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void testSlider(Gtk::Window& window, const std::filesystem::path& dir) {
|
||||||
|
std::cout << "slider\n";
|
||||||
|
std::filesystem::create_directories(dir);
|
||||||
|
const auto path = dir / "swapping.json";
|
||||||
|
effector::Project project;
|
||||||
|
project.video.enabled = true;
|
||||||
|
effector::ChainStage stage{effector::effectByName("tileswap"), {}};
|
||||||
|
stage.values["tileswap_rate"] = 0.5;
|
||||||
|
project.stages.push_back(stage);
|
||||||
|
effector::saveProject(path.string(), project);
|
||||||
|
window.activate_action("win.open-recent",
|
||||||
|
Glib::Variant<Glib::ustring>::create(Glib::filename_to_uri(path.string())));
|
||||||
|
pump(300);
|
||||||
|
|
||||||
|
auto* chainList = named<Gtk::ListBox>(window, "chain-list");
|
||||||
|
if (chainList != nullptr && chainList->get_row_at_index(0) != nullptr) {
|
||||||
|
chainList->select_row(*chainList->get_row_at_index(0));
|
||||||
|
}
|
||||||
|
pump(100);
|
||||||
|
std::vector<Gtk::Scale*> scales;
|
||||||
|
collect<Gtk::Scale>(window, [](Gtk::Scale& s) { return s.is_drawable(); }, scales);
|
||||||
|
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();
|
||||||
|
// 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)),
|
||||||
|
"with its range marked at both ends");
|
||||||
|
check(scale->get_value() == 0.5, "and shows the value saved");
|
||||||
|
|
||||||
|
scale->set_value(0.05);
|
||||||
|
pump(100);
|
||||||
|
window.activate_action("win.save-project");
|
||||||
|
pump(100);
|
||||||
|
const auto saved = effector::loadProject(path.string());
|
||||||
|
check(saved.stages.size() == 1 &&
|
||||||
|
std::abs(effector::paramDouble(saved.stages[0].values, "tileswap_rate", 0.0) - 0.05) < 1e-9,
|
||||||
|
"a value slid to is saved with the project");
|
||||||
|
}
|
||||||
|
|
||||||
bool actionEnabled(Gtk::Window& window, const std::string& name) {
|
bool actionEnabled(Gtk::Window& window, const std::string& name) {
|
||||||
auto action = dynamic_cast<Gtk::ApplicationWindow&>(window).lookup_action(name);
|
auto action = dynamic_cast<Gtk::ApplicationWindow&>(window).lookup_action(name);
|
||||||
return action && action->get_enabled();
|
return action && action->get_enabled();
|
||||||
@@ -822,6 +867,7 @@ int main(int argc, char** argv) {
|
|||||||
testRecentProjects(*window, dataDir / "projects");
|
testRecentProjects(*window, dataDir / "projects");
|
||||||
testVideo(*window, dataDir / "video");
|
testVideo(*window, dataDir / "video");
|
||||||
testFont(*window, dataDir / "font");
|
testFont(*window, dataDir / "font");
|
||||||
|
testSlider(*window, dataDir / "slider");
|
||||||
testHistory(*window, dataDir / "history");
|
testHistory(*window, dataDir / "history");
|
||||||
testMemoryGuard(*window, dataDir / "memory");
|
testMemoryGuard(*window, dataDir / "memory");
|
||||||
app->remove_window(*window);
|
app->remove_window(*window);
|
||||||
|
|||||||
Reference in New Issue
Block a user