Files
effector/README.md
T
hansandClaude Opus 5 7e98357070 Load source images in the background with a progress window
Decoding a batch of photographs took seconds on the main thread, so the
window froze with no sign of what was happening. loadImages now takes an
optional LoadProgress (per-file callback plus a cancel flag) and hands files
out from a shared cursor, so one large file cannot stall the queue behind it.

The GUI decodes off the main thread and shows a progress window with the
count, the current file and a Cancel button; it only appears if the batch is
still running after 250 ms. Headless mode keeps a "reading n/total" line on
stderr when it is attached to a terminal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 14:39:07 +02:00

127 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.
```bash
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`.
## Run
```bash
build/effector # GTK interface
build/effector --list-effects
build/effector --help
```
Headless rendering, no display needed:
```bash
build/effector -o out.png -s 1920x1080 \
--strip-height 8 --jitter 4 --seed 42 photo1.jpg photo2.png
```
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.
## 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`:
```cpp
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
}
};
```
2. 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.