A tile has to come back to the cut it started with for the clip to
loop, so every tile swapped at least twice a clip, and below that
rounding left every tile still: the bottom of the slider was either
frozen or busy. Swap rate is now the average per tile: below two swaps
a clip only a share of the tiles swap (twice each), fewer the lower it
goes, and above it each tile rounds up or down by its own draw.
ParamSpec::slider() takes an optional curve: the scale runs over the
slider's travel and sliderValue/sliderPosition map it onto the number
as position^curve. Swap rate uses a curve of 3, so the low end gets
most of the slider; the command line and project file are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new ParamSpec::slider() marks an Int or Double param to be shown in
the interface as a Gtk::Scale with its minimum and maximum marked under
the ends, rather than a spin button; the command line and the project
file are unchanged. Swap rate uses it, and now runs from 0 to 5 in
steps of 0.01 (was 0 to 30 in steps of 0.1): with every tile swapping
on its own, even 0.1 swapped the canvas many times a second.
Version 0.8.2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ctrl+Z undoes, Ctrl+Y and Ctrl+Shift+Z redo, caught in the capture
phase so a focused entry or spin never undoes only its own text; both
are in the header-bar menu too. The history keeps whole snapshots of
the project (currentProject() plus the immutable source list itself,
so stepping back never decodes again), compared by their JSON.
requestRender() notes every edit, and a step is committed once the
controls have been still for half a second. New and Open start the
history over once the project's images are in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new Font param type (ParamSpec::makeFont) is a font family as the
machine names it. The interface shows it as a family-only
FontDialogButton; the command line and the project file treat it as a
plain string, as they did the Text param it replaces, so existing
projects and --font-family keep working. A family the machine lacks is
kept as written. Bumps the version to 0.8.1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before decoding, loadImages reads every header to size the batch against
the free memory. That pass was serial, silent and could not be cancelled,
so the loading window sat at "0 of N". LoadProgress gains onSized, the
headers are read in parallel on the same cursor-driven workers as the
decodes, and a cancel stops the pass. The window's bar runs twice,
"Checking sizes" then "Decoding", and the CLI prints a sizing line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With eight video effects their list filled its 110-pixel cap, and the menu's
headings take more than the 120 pixels the height test allowed for them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blur's box blur moves to effects/box_blur.hpp so both share it; Blur renders
exactly as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>