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>
This commit is contained in:
Si
2026-09-26 11:22:01 +02:00
co-authored by Claude Opus 5.5
parent c069de3566
commit 385617781e
26 changed files with 1868 additions and 39 deletions
+31
View File
@@ -16,6 +16,19 @@ public:
using std::runtime_error::runtime_error;
};
// Where a render sits in time. The default -- no frame rate -- is a still
// picture; a video hands every frame its index, the clip's length in frames
// and its rate, so an effect that moves works out where it is from these
// alone and a frame can be rendered on its own, in any order.
struct FrameTime {
int index = 0;
int count = 1;
int fps = 0;
bool video() const noexcept { return fps > 0; }
double seconds() const noexcept { return video() ? static_cast<double>(index) / fps : 0.0; }
};
// Base class for every effect. Canvas size and the random source are common to
// all of them; params() covers only the effect-specific knobs.
class Effect {
@@ -45,9 +58,27 @@ public:
return title();
}
// Whether a stage with these values changes from one frame of a video to
// the next. A stage that does not is rendered once for the whole clip.
virtual bool animated(const ParamValues& values) const {
(void)values;
return false;
}
virtual Image render(const std::vector<Image>& sources, int width, int height, Rng& rng,
const ParamValues& values) const = 0;
// One frame of a video, or a still when `frame` says so. An effect that
// does not move needs only render(), which this falls back to. One that
// does must leave `rng` in the same state on every frame -- a frame's own
// randomness comes from a generator of its own -- so the stages after it
// stay put while it moves.
virtual Image renderFrame(const std::vector<Image>& sources, int width, int height, Rng& rng,
const ParamValues& values, const FrameTime& frame) const {
(void)frame;
return render(sources, width, height, rng, values);
}
ParamValues defaults() const;
};