simonandClaude Opus 5.5 ef8f728126 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>
2026-09-30 23:44:27 +02:00
2026-09-26 10:38:49 +02:00

Effector

An image effects app: a GTK4 interface and a headless CLI over a set of pluggable effects. Effects are self-contained classes that declare their own parameters; the interface builds its widgets and the CLI builds its flags from those declarations, so a new effect needs no UI or CLI code.

Build

Requirements: a C++20 compiler (GCC 13+), CMake 3.20+, gtkmm-4.0 and gdk-pixbuf-2.0 development packages. On Arch: gcc cmake ninja gtkmm-4.0. Writing videos needs the ffmpeg program at run time (on Arch: ffmpeg); without it everything else works and the video tests are skipped.

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failure

The binary lands at build/effector; cmake --install build --prefix ~/.local installs it to ~/.local/bin.

On Arch, packaging/arch/PKGBUILD builds the release tagged v<version> from this repository and installs it with pacman, desktop entry included:

cd packaging/arch && makepkg -si

Run

build/effector                  # GTK interface
build/effector --list-effects
build/effector --help
build/effector --version

Headless rendering, no display needed:

build/effector -o out.png -s 1920x1080 \
    --strip-height 8 --jitter 4 --seed 42 photo1.jpg photo2.png

The canvas is either a size in pixels (-s 1920x1080, the Custom entry in the interface) or one of the standard formats, which --help lists in full:

build/effector -o post.png --format instagram-post -e tiles
build/effector -o sheet.png --format a4 --dpi 300 -e scanlines

A web format — the Instagram sizes, Full HD, 4K UHD — is a fixed pixel size. A print format — A5, A4, A3, US Letter — is a sheet of paper, and becomes pixels at --dpi (36–1200, 300 by default), so A4 at 300 dpi is 2480×3508. Picking a format in the interface fills the width and height in and greys them out; Custom hands them back, keeping whatever the format last worked out to. A format is saved with the project, so an A4 project reopens as A4 rather than as the pixels it happened to be.

Video

A project can be a video clip instead of a still: switch Video on under Setup and give it a length and a frame rate, or pick a preset that sets the canvas too — Instagram story (1080×1920, 15 s), Instagram reel (30 s), Instagram feed video, TikTok / YouTube Short, Square loop (6 s) and Full HD clip. The preview renders the whole clip first, shrunk to fit in memory, and then loops it, with play/pause and a scale to look at any one frame. Save As… then writes the video at full size through ffmpeg, which has to be installed: .mp4, .mov and .mkv as H.264, .webm as VP9, .gif as a palette GIF.

build/effector -o story.mp4 --video-preset instagram-story -e channelsplit -e tracking --seed 1 photo.jpg
build/effector -o loop.gif -s 480x480 --duration 3 --fps 15 -e tracking --tracking-bar 40 photo.jpg

--duration SECONDS and --fps N make any run a video; a preset fills in whatever was not given alongside it.

Effects come in two kinds. Image effects make a picture and hold still in a clip. Video effects are motion: the Add effect menu lists them under a heading of their own, and they can only be added once the project is a video. In a still project a video stage is marked as skipped and the render leaves it out, as it would a stage that is switched off. For now there is one video effect, VHS tracking: the colour planes sway and shake, and a tracking bar rolls down the picture, like a worn tape on an old tube. Put it after the image effects it should shake. Every frame starts from the same seed, so the image effects hold still while a video effect moves them, and a clip renders frame for frame the same again.

Any option can also be passed when launching the interface; it becomes the starting state (images, canvas size, effect settings, seed).

Reading a batch of large photographs takes seconds, so it never blocks the interface: decoding runs in the background and a progress window shows how many files are done, which one is being read, and a Cancel button that stops the batch. Small batches finish before the window would be drawn, so it only appears if the decode is still going after 250 ms. On a terminal the headless mode keeps a single reading n/total line updated on stderr; redirected output stays clean.

Versions

Releases are listed in CHANGELOG.md and tagged vX.Y.Z (git tag -l 'v*'). effector --version prints the version, and the menu in the interface has Changelog and About Effector.

To cut a release: bump the project() version in CMakeLists.txt, move the changes into a new ## [X.Y.Z] - date entry at the top of CHANGELOG.md (the core tests fail until the two agree), commit, and tag it: git tag -a vX.Y.Z -m "Effector X.Y.Z".

Effects

scanlines

Fills the canvas top to bottom with horizontal strips cut at random positions from the source images. Strips are taken at native scale, cropped at a random horizontal offset, and tiled when the source is narrower than the canvas.

Parameter Flag Meaning
strip_height --strip-height Height in pixels of each extracted line
jitter --jitter Random ± variation of that height (0 = uniform)
keep_order --keep-order Cycle the images in order instead of picking randomly

The seed makes a result reproducible: same sources, settings and seed give the same image, on any machine — the generator (src/core/rng.hpp) is a hand-rolled xoshiro256** rather than <random>, whose distributions are not portable. Recreate draws a new seed.

Layout

src/core/      image buffer and IO, param specs, effect base, registry, rng, thread helper
src/effects/   one file per effect
src/cli/       argument parsing (flags generated from the registry)
src/gui/       gtkmm 4 interface
tests/         core checks, run by ctest
python-reference/  the original Python prototype, kept for reference only

effector_core is a GUI-free static library that links only gdk-pixbuf, so headless rendering never touches GTK or a display. The GTK front end links on top of it.

Adding an effect

  1. Create src/effects/myeffect.{hpp,cpp} with a subclass of Effect:
class MyEffect : public Effect {
    std::string name() const override { return "myeffect"; }
    std::string title() const override { return "My Effect"; }
    std::string description() const override { return "..."; }

    const std::vector<ParamSpec>& params() const override {
        static const std::vector<ParamSpec> specs = {
            ParamSpec::makeInt("amount", "Amount", 4, 1, 64, "How much."),
        };
        return specs;
    }

    Image render(const std::vector<Image>& sources, int width, int height, Rng& rng,
                 const ParamValues& values) const override {
        const long amount = paramInt(values, "amount", 4);
        ...  // return a width x height RGB image
    }
};
  1. Add it to _EFFECT_CLASSES-style list in src/core/registry.cpp and to the source list in CMakeLists.txt.

Widgets and flags follow from params() automatically. Draw everything random from the Rng you are handed so results stay reproducible, and use parallelFor (src/core/parallel.hpp) for the pixel work.

Performance notes

The render loop first draws the whole strip plan from the seeded generator, then copies the pixels in parallel across the hardware threads; each row is a memcpy, and horizontal tiling doubles the filled span per pass. Source images are decoded in parallel too, which is what dominates a headless run.

A 7680×4320 canvas at --strip-height 2 from three multi-megapixel JPEGs renders in ~35 ms, against ~73 ms for the Python prototype; the interface reports the render time of the current preview in its status line.

Decoding threads take the next file from a shared cursor rather than a fixed slice of the list, so one very large file cannot stall the files queued behind it and progress advances at a steady rate.

License

Copyright © 2026 Si.

Effector is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; see LICENSE for the full text. Images you render with it are yours and are not covered by the license.

S
Description
No description provided
Readme
1.8 MiB
Languages
C++ 82.5%
Python 16.2%
CMake 1.3%