The version is the project() line of CMakeLists.txt and nowhere else:
CMake writes it, with the text of CHANGELOG.md, into a generated
core/version.hpp, so the number and the release notes the binary shows
are the ones it was built from. effector --version (-V) prints it, and
the header-bar menu gains a section with Changelog, a window laying the
Markdown out as headings and bullets, and About Effector.
The core tests hold the newest changelog entry to the current version,
so a bump cannot go in without its notes. Releases are tagged vX.Y.Z;
this is 0.5.0, and the changelog reconstructs 0.1.0-0.4.0 from the
history.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Projects opened or saved go into GTK's shared recent list, signed with the
effector application name, and the project menu lists the newest ten of
them. The GUI tests point XDG_DATA_HOME at a temporary folder so they leave
the user's own list alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The split between the chain list and the params sat at a fixed 300 px,
wrong for a short chain and for a long one alike. It now rests under the
chain's rows, growing with the chain up to eight stages before the list
scrolls. Once the user drags it, it stays put; a double-click on the
handle fits it to the rows again.
GtkPaned sizes the fixed pane from its minimum height, which is only the
drag floor here, so the fit is done by hand from the natural height.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Effect::label(values) names a stage in a list, defaulting to the title;
the text effect answers "Text (words)", and the chain rows are renamed
in place as params change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sidebar grew crowded as the chain got longer. The chain and the params
of the selected stage now share a Chain tab through a vertical split the
user drags, and the sources, canvas and seed move to a Setup tab. Stage
rows are compact, with their move and remove buttons shown only on the
hovered, selected or focused row.
Add effect had stopped opening: its menu of fifteen effects no longer fit
below the button, and GTK does not show a popover that does not fit. The
menu now scrolls past 480 pixels, and the button sits beside the chain
heading, where the split cannot push it down the screen.
The window code becomes an effector_gui library so a new GUI test binary
can drive the real window under Xvfb; ctest skips it when xvfb-run is
missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A picture with a title, a caption and a credit meant typing the same
font, size and colour into every text stage, and changing the look meant
typing them all again. A project now holds named styles: a stage follows
one, and editing it moves every stage that follows it at once.
The front-ends learn nothing effect-specific. A ParamSpec can be marked
.styled(), saying a style carries it rather than the stage, and
Effect::styleKind() names the pool it comes from -- so styleSpecs(kind)
is to a style what params() is to a stage, and a second text-ish effect
would slot into the styles that already exist. The look is what a style
carries; the words and the placement stay on the stage, which is what
lets two stages share a typography in two different corners.
stageValues() resolves one in a single place, and a stage keeps a full
set of values of its own beside the style, so switching it to Custom,
dropping the style, or opening the project in a build that does not know
it renders exactly the same picture. A style moves stages together; it
never holds the only copy of anything.
The command line builds no styles and passes none, so it renders what it
always did.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trying a stage out meant removing it and building it again from scratch,
settings and all, to see what the picture looked like without it. A stage now
carries an `enabled` flag and can simply be switched off: it keeps its place
in the chain and everything it was set to.
`enabledStages` is the one place that reads the flag, and renderChain,
chainNeedsSources, chainTitle and chainSlug all go through it. A disabled
stage is skipped whole — handed no image and drawing nothing from the shared
rng — so switching one off renders byte for byte what the chain rendered
before that stage was added, which is the only thing that makes the switch
worth anything while comparing. Whichever stage runs first is the one handed
the sources, so disabling a leading scanlines lets blur lead and the chain
needs no images at all. The status line and the generated file name follow
too: they name what was rendered, not what sits in the sidebar. A chain with
every stage disabled throws rather than quietly rendering nothing.
In the interface it is a check on the chain row: the row dims, the params
heading says "(off)" and the settings stay editable — a stage being worked on
is not the same as one being deleted. On the command line it is
`--disable NAME`, which picks its stage by effect name exactly as the params
do. A project writes `"enabled": false` only for a stage that is off, so
existing project files are unchanged and an ordinary one stays uncluttered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The window used to hand the user a Scanlines stage before they had asked for
anything, which also meant the first thing on screen was "Add source images
to start." — an instruction about an effect nobody chose. It now opens on "No
effects yet" and waits: the chain is the subject of the interface, so the
first stage is the user's to pick.
The default effect was injected by the option parser, which would have put
Scanlines straight back on every launch. It moves to renderHeadless, where a
chain really is mandatory — a headless run must render something, while the
interface has no such need. `-o out.png` with no -e is unchanged.
The sources section follows the same question the render gate already asks,
chainNeedsSources(): only the first stage is ever handed the sources, so a
chain led by blur or text makes the whole section beside the point and it is
hidden rather than left there to be filled in vain. Today that shows it for
scanlines and tiles, but no effect is named in the GUI — a new one declaring
needsSources() gets this for free. rebuildChainRows() is the one funnel every
chain change goes through, so that is where the visibility is set.
New in the project menu now starts empty too, matching what the window opens
with.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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/.