Commit Graph
9 Commits
Author SHA1 Message Date
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 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
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 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 a82fba7886 Save and open projects as JSON
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>
2026-09-18 15:37:37 +02:00
SiandClaude Opus 5 89985cb989 Cascade effects through a chain
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>
2026-09-18 15:21:29 +02:00
SiandClaude Opus 5 ee4b1038ac Add a blur effect plugin
Fills the canvas from one source, tiling it when it is smaller, then
applies a separable box blur repeated `passes` times to approximate a
gaussian. Params: radius, passes, direction and first_source, so the CLI
flags and the GUI widgets come out of the ParamSpecs as usual.

All random values (source index and crop offset) are drawn serially
before the parallel pixel work, so a seed reproduces the image exactly;
the box window clamps at both ends and the radius is clamped to the
canvas so an oversized radius cannot read out of bounds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:05:15 +02:00
hans 925a719da0 Effector: C++/CMake image effects app
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/.
2026-09-18 14:28:13 +02:00