The canvas was two spin buttons, so "an Instagram story" or "A4 at 300 dpi"
was a number the user had to work out. src/core/canvas.cpp now holds one
table of standard sizes both front-ends read: a web format is a fixed pixel
size, a print format a sheet in millimetres that becomes pixels at a dpi.
Adding a format is a line in canvasFormats() — the dropdown, --format and
--help all walk the table.
The pixels stay the truth everywhere; renderChain never hears about formats.
A format is the presentation of a canvas, carried alongside the pixels in
Options and Project and empty for a custom size, and where one is named it
decides the pixels — so a project saved as A4 reopens as A4 rather than as
whatever it happened to measure.
In the interface a format fills the width and height in and greys them out
rather than hiding them: the size a sheet works out to is worth seeing. A
print format also shows a DPI row, and Custom hands the spins back keeping
the last size, which makes starting from a format and then tweaking it the
ordinary path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Lays a single picture from disk over its input: fitted, filled, stretched,
left at its own size or tiled, centred or pinned to the top left corner,
nudged by an offset and mixed in by its opacity.
The file it needs is a new kind of param, Path, so the interface shows a
chooser and the CLI takes a file name, both without knowing an overlay
exists. Projects write it relative to their own folder, as they do a
source, and Image::load can now hand back the alpha channel instead of
flattening it onto white, so a cut-out PNG keeps its cut-out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The source images took the top of the sidebar and most of its height,
which made the list of files look like the subject of the window rather
than its input. The chain is what the interface is about, so it now comes
first with the selected stage's params right below it, taking the free
space; the sources sit at the bottom in a collapsed expander whose label
carries the count, with their own "Add images" button instead of the one
that used to be in the header bar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Whether an effect works from the loaded photographs is now part of the
declarative plug-in contract, next to params(): Effect::needsSources()
defaults to true, and blur — which only softens what it is handed — says
no and blurs an empty canvas when given nothing.
Only the first stage of a cascade ever sees the sources, so
chainNeedsSources() just asks the first stage. Both front-ends gate on
that instead of on "are there any images": the CLI only refuses an empty
batch when the chain wants one, and the interface renders a blur-led
chain with no images loaded, saying whichever thing is actually missing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A project is the whole setup as a small text file: the source paths, the
effect chain with its params, the canvas and the seed. Reading and writing
are driven by the effects' ParamSpecs rather than by whatever sits in a
stage's values, so a new effect is saveable without touching this code, a
key an effect does not declare is never written or read back, and an old
project keeps the defaults for params that did not exist when it was
saved. Paths under the project's own folder are written relative, so a
folder of images and their project can be moved whole.
src/core/project.cpp carries its own small JSON reader instead of a
dependency, and names the line when a hand-edited file is wrong.
The interface gets a header-bar menu: New, Open, Save, Save As, with
Ctrl+N/O/S. The title shows the project name. Opening a project replaces
the sources, discarding a decode still in flight rather than mixing it
into the new project.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
renderChain runs a list of stages in order: stage 0 works on the loaded
sources, every later one on the output of the stage before it, so blur
after scanlines softens the scanlines rather than the photographs. The
effects themselves are untouched — none of them knows whether its input
came from disk. All stages share one Rng and run in sequence, so a seed
still reproduces the cascade byte for byte.
The sidebar grows an "Effect chain" list: add a stage from a menu built
from the registry, reorder it, remove it, and edit the selected stage's
params underneath. Each stage keeps its own values. The CLI takes the
same chain from repeated -e, with every flag going to the stage whose
effect declares it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Port of the Python prototype to C++20 with a CMake build:
- effector_core: GUI-free static library (effects, image IO via gdk-pixbuf,
declarative param specs, registry, portable seeded RNG, thread helper)
- scanlines effect: plans strips from the seeded RNG, then copies pixels in
parallel with memcpy and doubling horizontal tiling
- gtkmm 4 interface: native file dialogs, widgets generated from the param
specs, coalesced background renders, preview as a GDK memory texture
- CLI: flags generated from the effect registry, headless rendering with no
display required
- ctest suite covering the RNG, registry, effect geometry and image round trip
The Python prototype is kept under python-reference/.