Commit Graph
57 Commits
Author SHA1 Message Date
SiandClaude Opus 5.5 4b407281e2 Add Strip roll, a video effect that rolls the picture in Scanlines' strips
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:43:47 +02:00
SiandClaude Opus 5.5 a7cb2505d7 Add Datamosh, a video effect whose Corrupt blocks break and heal over the clip
Corrupt's block damage moves to effects/corrupt_blocks.hpp so both share it;
Corrupt renders exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:41:52 +02:00
SiandClaude Opus 5.5 ce0207c41f Add Film gate, a video effect of film running through a projector
Film grain's field moves to effects/grain_field.hpp so both share it; Film
grain renders exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:39:57 +02:00
SiandClaude Opus 5.5 37c02a8a7b Add Sort sweep, a video effect that sweeps Pixel sort's window over the brightness
The run sorting moves to effects/sort_runs.hpp so both share it; Pixel sort
renders exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:37:30 +02:00
SiandClaude Opus 5.5 0377cd4abb Add Warp flow, a video effect that sets Warp's noise field drifting
Warp's noise field moves to effects/noise_field.hpp so both share it; Warp
renders exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:34:55 +02:00
SiandClaude Opus 5.5 370d3e550e Add Echo tunnel, a video effect that flies through Feedback's nested echoes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:32:49 +02:00
SiandClaude Opus 5.5 8537a3cf0a Open Save in the project folder and offer the last name with a number counted on
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:24:10 +02:00
SiandClaude Opus 5.5 ac8b62208c Render the video preview draft first: every third frame, then the rest
The preview plays a draft of every third frame, each held for three, as
soon as those are in, and fills in the frames between while it plays.
renderFrames takes an optional frame order (draftFirstOrder builds this
one); a frame is the same whatever order it is rendered in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:49:59 +02:00
SiandClaude Opus 5.5 6df4bcb079 Add a video export quality choice: Small file, Standard or High
A Quality dropdown under Video on Setup, --video-quality on the command
line, and "quality" in a project's video settings (written only when it is
not Standard). One table, videoQualities(), holds the ffmpeg settings per
codec; Standard is what every video was written with before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:41:00 +02:00
SiandClaude Opus 5.5 1b30a079e6 Add Tile swap, a video effect built from the source images
Tile swap lays tiles cut from the photographs over the canvas as Tiles
does, in a grid, in bricks or scattered, and swaps each one for another
cut at a moment of its own, so the mosaic keeps changing piece by piece.
The layout stays put for the whole clip; what a tile shows depends only
on the tile and how many times it has swapped, counted round the clip, so
it holds still between swaps and the clip loops without a jump. On the
command line it is -e tileswap with --tileswap-rate and the rest.

VideoEffect learns a second kind: an effect that answers needsSources()
writes compose() instead of animate(), and the base then takes no crop,
only the one seed, so it sees the photographs when it leads the chain and
the stage before it otherwise. Transforms render exactly as before. The
tile cutting and painting move out of tiles.cpp into tile_plan.hpp for
both effects to share; Tiles renders byte for byte as before. Version
0.8.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:22:26 +02:00
SiandClaude Opus 5.5 cdaadffc89 Split effects into image and video effects, with VHS tracking as the first video effect
Effect::video() says which kind an effect is. The Add effect menu lists the
two kinds in sections of their own, each scrolling on its own, and offers
video effects only in a video project; a video stage carries a film icon in
the chain and, in a still, is dimmed and skipped like a stage switched off.
enabledStages takes whether the render is a video and is the one place that
leaves video stages out of a still. --help groups the video effects under a
heading of their own, --list-effects marks them, and a video effect given
for a still is skipped with a warning.

VideoEffect is the base every video effect builds on, and keeps the rules a
moving stage has to follow: it takes one value from the shared rng, the same
on every frame, and hands the effect steady and per-frame generators and
loop-rounded time through a Moment, so a subclass writes only animate().
VHS tracking is the first, and the one to copy: the colour planes sway and
shake and a tracking bar rolls down the picture.

That motion moves out of channel split, which is an image effect again and
renders stills exactly as before; forVideo() goes with it. Version 0.7.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:03:39 +02:00
SiandClaude Opus 5.5 ea59db5acd Refuse loads and renders that would not fit in memory
Opening a project of ninety 17 MP photos got Effector killed by the
kernel's OOM killer: 4.6 GB of decoded RGB on a machine with 3.6 GB free,
and no core dump to show for it. Bulk allocations are now sized first and
refused with an OutOfMemoryError past three quarters of the free memory
(MemAvailable, or less under a cgroup memory.max). loadImages sizes the
batch from the file headers and refuses it before decoding anything, and
holds the decodes in flight to what is left; renderChain and renderFrames
refuse a canvas at 24 bytes a pixel, plus any picture a Path param loads.
The interface keeps a refused batch's paths, so saving the project never
drops them. Version 0.6.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:31:53 +02:00
SiandClaude Opus 5.5 385617781e Make video projects, with a moving channel split as the first video effect
A project can now be a clip: Setup has a Video switch with a length, a
frame rate and presets that set the canvas too (Instagram story, reel and
feed video, TikTok / YouTube Short, Square loop, Full HD clip). The preview
renders the whole clip, shrunk to a memory budget, and loops it with
play/pause and a scale; Save As... renders it again at full size and pipes
the frames into ffmpeg as .mp4, .mov, .mkv, .webm or .gif, with a progress
window that can cancel. --duration, --fps and --video-preset do the same
from the command line.

Effects learn about time through FrameTime and Effect::renderFrame, which
falls back to render, so nothing that holds still changes. Channel split
now sways, shakes and rolls a tracking bar down the picture in a video; its
motion params are marked forVideo() and hidden in a still project, and a
still renders byte for byte as before. renderFrames starts every frame
from the same seed and renders the still stages ahead of the first moving
one only once. The version moves to 0.6.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.6.0
2026-09-26 11:22:01 +02:00
SiandClaude Opus 5.5 c069de3566 Give Effector its own icon
An E split into its colour channels, with two slices slipped sideways,
replaces the generic applications-graphics icon in the desktop entry and
About Effector, with a one-colour symbolic version for panels. Both are
installed into the hicolor theme; a build run straight from the build tree
falls back to the copy in the sources. The version moves to 0.5.3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 10:38:49 +02:00
SiandClaude Opus 5.5 708b6bdb1b License Effector under the GPL, version 3 or later
LICENSE holds the text, the README and About Effector state it, and the
Arch package declares it. The version moves to 0.5.2 for the release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.5.2
2026-09-26 10:26:28 +02:00
SiandClaude Opus 5.5 520c7c45fb Package Effector for Arch and give it a desktop entry
packaging/arch/PKGBUILD builds the tagged release from this repository
and installs it with pacman; the version test keeps its pkgver in step
with CMakeLists.txt. A desktop entry puts the interface in the app menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.5.1
2026-09-26 10:22:18 +02:00
SiandClaude Opus 5.5 b9f905e04c Give Effector a version and a changelog, shown from the menu
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>
v0.5.0
2026-09-26 10:14:43 +02:00
SiandClaude Opus 5.5 431fcf9c58 List recently opened projects under Open Recent
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>
2026-09-24 01:07:08 +02:00
SiandClaude Opus 5.5 9fb4428a3b Give the image overlay an optional shadow or glow
A blurred copy of the overlay's own alpha is drawn in a colour beneath it:
moved by an offset for a shadow, centred for a glow, with a size, a spread
and an opacity. The mask is integer-only so a seed still reproduces it, and
reaches past the canvas so an image lying off it still casts onto it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 00:56:27 +02:00
SiandClaude Opus 5.5 7d122ddd6c Keep the params' scrollbar clear of the spin buttons
The overlay scrollbar sat over the "+" of each spin button and took
clicks meant for it; give the param grid a gutter on its right.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 00:48:33 +02:00
SiandClaude Opus 5.5 536b8620ea Reorder the chain by dragging a row
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 00:43:51 +02:00
SiandClaude Opus 5.5 81ca17463e Keep the status line short, with the full chain and setup in its tooltip
The line now reads only the number of effects that ran and the render
time; the chain, canvas size, sources and seed move to the tooltip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.4.0
2026-09-23 22:34:07 +02:00
SiandClaude Opus 5.5 d198428dc4 Fit the split on a double-click only once the handle's drag ends
The split's own drag is still going during the second press of a
double-click, so the least movement before the release put the handle
back under the pointer right after it was fitted. The fit now waits for
that drag to end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 22:26:59 +02:00
SiandClaude Opus 5.5 a01ada5372 Fit the chain/params split to the chain's rows until it is dragged
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>
2026-09-23 22:21:51 +02:00
SiandClaude Opus 5.5 d0b5669f46 Let the mouse wheel scroll past spin buttons that do not have the focus
A GtkSpinButton grabs the focus and spins its value on any wheel turn over
it, so scrolling through the params stopped at the first spin under the
pointer and changed it. A capture-phase scroll controller now scrolls the
enclosing view instead, unless the spin already has the focus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 21:47:10 +02:00
SiandClaude Opus 5.5 5292c101db Label text stages in the chain list with their words
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>
2026-09-23 21:37:26 +02:00
SiandClaude Opus 5.5 134c575727 Add a rotation to the text effect's placement
text_rotation turns the text and its box about the box's centre, so the
centring and the offset still decide where it sits. Like the other
placement params it stays on the stage and is not carried by a style.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 21:33:37 +02:00
Si 68a1e40d3f Merge branch 'sidebar-tabs' 2026-09-23 21:28:07 +02:00
SiandClaude Opus 5.5 5a5cea69db Split the sidebar into Chain and Setup tabs, with a draggable chain list
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>
2026-09-23 21:26:42 +02:00
Si 1edf854cd1 Merge branch 'centered-preview' 2026-09-23 20:44:55 +02:00
SiandClaude Opus 5.5 4fd6ccc9b0 Keep the preview centred however long the status line gets
The status line under the preview names every stage of the chain, and a
label asks for its whole text as its minimum width. With a long chain
that was wider than the pane, so the preview column was laid out wider
than it is shown and the picture, centred in it, slid off to the right
until a shorter status (such as "Saved ...") let it back. The line is
now ellipsized and never asks for more room than it is given; the full
text stays readable as its tooltip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:44:47 +02:00
Si 352a928149 Merge branch 'chaos-effects' 2026-09-23 20:36:57 +02:00
SiandClaude Opus 5.5 ce3a422f6a Add eight effects that throw something chaotic into the chain
Slit shift, channel split, pixel sort, corruption, shatter, warp, bit
crush and feedback: tearing bands sideways, knocking the colour planes
out of register, sorting runs of pixels, damaging blocks the way a broken
stream does, breaking the picture into shards, pushing it along a noise
field, mangling its bytes and laying it over itself again and again.

All eight are pure transforms. None needs sources, so any of them can
lead a chain, and like blur they start from a crop of a random source
when they do and photographs are loaded. canvasFromSources holds that
start once for them.

Each draws its whole plan from the rng before touching a pixel, and
pixel sort hands every line a seed of its own, so the parallel work
never decides anything. The rotations in shatter and feedback take their
sine and cosine from a short series of plain multiplications rather than
libm, so a seed lands on the same pixels whatever the machine's maths
library makes of std::sin.

The tests hold every one of them to the same things -- a seed
reproduces it, it runs without sources and as a later stage, a sliver
of a canvas does not trip it, turned right down it leaves its input
alone -- plus a check of what each actually does, and the registry now
checks that no two effects declare the same param key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 20:36:09 +02:00
Si 8113a06da7 Merge branch 'empty-chain-start' v0.3.0 2026-09-19 22:03:31 +02:00
SiandClaude Opus 5 3aade6f361 Let several text stages share one named look
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>
2026-09-19 21:55:55 +02:00
SiandClaude Opus 5 eb417bb41a Let a chain stage be switched off without taking it apart
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>
2026-09-19 21:11:17 +02:00
SiandClaude Opus 5 f61161aef0 Open on an empty chain, and show the sources only when they are used
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>
2026-09-19 20:59:56 +02:00
Si fc54d64bc4 Merge branch 'canvas-formats' v0.2.0 2026-09-18 22:36:24 +02:00
SiandClaude Opus 5 d48a81c665 Offer standard web and print canvas formats
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>
2026-09-18 22:34:17 +02:00
Si f4d8457828 Merge branch 'image-overlay-effect' 2026-09-18 22:15:29 +02:00
SiandClaude Opus 5 749ea239bb Add a tiles effect
The two-dimensional counterpart of scanlines: rectangular tiles cut at
random positions from the source images, in three arrangements. A grid
draws its column widths and row heights once so the cells stay aligned
even when jittered; brick repacks every row on its own so the vertical
seams do not meet; scatter fills the canvas from one of the images and
drops loose tiles over it.

The plan is drawn serially from the rng as scanlines does, but scattered
tiles overlap, so they are painted in plan order on one thread: letting
threads race for the same pixels would leave the output depending on
their scheduling. A source smaller than a tile is repeated to fill the
cell, sideways and then downwards, which keeps grid and brick gapless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:10:54 +02:00
SiandClaude Opus 5 1d0343b90a Add a text effect
Writes a line or several over its input in a chosen family, weight, size
and colour, optionally on a box filled with a colour or with a picture
from disk. Like the blur and the overlay it needs no sources of its own,
so it can lead a chain; given sources it fills the canvas from a randomly
chosen one first.

Two param types rather than effect-specific branches in the front-ends:
Text, free-form text the user types, and Color, a colour written
"#rrggbb". Both travel as strings, so the interface gets an entry and a
colour button, the command line gets a flag (a colour that does not parse
is refused there rather than drawn as black), and a project keeps them
across a round trip.

The glyphs come from Pango, which draws through Cairo without a display,
so the core stays headless — but they are the one thing here that is not
byte-identical across machines: a machine without the chosen family
renders the substitute its fontconfig picks. Everything the effect is
handed, the rng included, is used exactly as every other effect uses it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 20:31:24 +02:00
SiandClaude Opus 5 c34bc426fc Write and read projects in the C locale, and keep the seed whole
gtkmm sets both the C and the C++ global locale from the environment, so
an unguarded stream writes "0,35" on a German desktop and groups 1920 as
"1.920" — neither of which the reader, or any other JSON reader, can take
back. Both the writer and the number parser now work in the classic
locale: the writer imbues its stream, and the parser uses from_chars
rather than stod, JSON having a point for a decimal mark whatever the
user's mark is.

The seed is read from the digits it was written as rather than from the
double they parsed into: a 64-bit seed has more bits than a double has
mantissa, and a seed that comes back off by a few is a project that no
longer renders its own picture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 20:30:55 +02:00
SiandClaude Opus 5 e2252ecd30 Add an image overlay effect
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>
2026-09-18 19:31:09 +02:00
Si 2a64ab115e Merge branch 'sidebar-chain-first' 2026-09-18 16:55:22 +02:00
SiandClaude Opus 5 efee1cf224 Add a black and white effect
Monochrome is the fourth effect and the third that declares
needsSources() false, so it works as a filter stage at the end of a
cascade and can also lead one, filling the canvas from a source the way
blur and grain do. Its knobs are the channel weighting, the contrast, a
toner and the strength: the weighting offers the mixes a photographer
would make behind the lens, so the colour names weigh that channel most
and "red" darkens a sky the way a red filter does; contrast pulls the
greys away from mid grey; the toners tint the finished greys warm
(sepia), barely (selenium) or cold (cyanotype); and strength mixes the
result back over the colour input, which at 0 is a pass through.

Contrast and toner only ever see one grey, so the whole tail of the
conversion is folded into a 256-entry table per channel, built once
before the parallel pass; the per-pixel work is then integer only, with
the weights as 1024ths and the tints as 256ths. The Rng is touched only
for the source pick and the crop offset, serially, before any threading.

The tests gain a `monochrome` section: neutrality of the output, each
weighting measured on a pure red field, the contrast pivot at mid grey,
both toner directions, strength as a pass through and as a midpoint, and
the sourceless render that stays black.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:55:00 +02:00
SiandClaude Opus 5 67417727e7 Check the canvas size before narrowing it to an int
A canvas side was cast to int with no range check, so a number that did
not fit came back as one that did: `-s 4294968296x600` rendered a
perfectly plausible 1000x600, and a hand-edited project file could ask
for a width the reader turned into something else (or, for a double out
of range, into undefined behaviour).

Both front-ends now check the side before the cast, against one shared
bound in chain.hpp — the 1..20000 the GUI spin buttons already offered,
so the help text, the CLI error, the project reader and the interface
all move together.

The tests gain a `canvas size` section covering the round trip at both
ends of the range and the refusals on either side of it, through the
project reader and through `-s`; the test binary picks up the CLI parser
to do it, which stays GUI-free.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:49:44 +02:00
SiandClaude Opus 5 82e730221e Add a film grain effect
Grain is the third effect and the second that declares needsSources()
false, so it works as a filter stage in a cascade and can also lead one,
filling the canvas from a source the way blur does. Its knobs are the
amount a grain may push a pixel, the grain size in pixels, whether the
channels are drawn apart into coloured grain, and whether the response is
weighted towards the midtones — which it is by default, since film grains
least where it is least exposed. That last one means grain over an empty
canvas stays black unless the weighting is turned off.

Determinism comes from the same shape the scanlines effect uses: the
whole grain field is drawn serially from the Rng into one cell per grain
before any threading, and only the integer pixel arithmetic afterwards
runs under parallelFor.

The canvas fill blur used to keep to itself moves to
effects/source_fill.hpp unchanged, now that two effects want it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:42:04 +02:00
SiandClaude Opus 5 b6b5f94f08 Put the chain first in the sidebar
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>
2026-09-18 16:36:43 +02:00
Si 6cb3da8dc6 Merge branch 'blur-effect' 2026-09-18 16:01:44 +02:00