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>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
#include "effects/video_effect.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "core/video.hpp"
|
||||
#include "effects/source_fill.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
double Moment::length() const noexcept {
|
||||
return static_cast<double>(std::max(1, frame_.count)) / std::max(1, frame_.fps);
|
||||
}
|
||||
|
||||
double Moment::loopRate(double perSecond) const noexcept {
|
||||
const double clip = length();
|
||||
return std::max(1.0, std::round(perSecond * clip)) / clip;
|
||||
}
|
||||
|
||||
double Moment::cycle(double perSecond) const noexcept {
|
||||
return std::fmod(seconds() * loopRate(perSecond), 1.0);
|
||||
}
|
||||
|
||||
Rng Moment::fresh() const noexcept {
|
||||
return Rng(seed_ ^ (0x9e3779b97f4a7c15ULL * static_cast<std::uint64_t>(frame_.index + 1)));
|
||||
}
|
||||
|
||||
Image VideoEffect::render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||
const ParamValues& values) const {
|
||||
return renderFrame(sources, width, height, rng, values, FrameTime{0, kDefaultFps, kDefaultFps});
|
||||
}
|
||||
|
||||
Image VideoEffect::renderFrame(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||
const ParamValues& values, const FrameTime& frame) const {
|
||||
if (width < 1 || height < 1) throw EffectError("Canvas size must be at least 1x1 pixels.");
|
||||
const FrameTime at = frame.video() ? frame : FrameTime{0, kDefaultFps, kDefaultFps};
|
||||
|
||||
// Both of these take the same values from the shared rng on every frame:
|
||||
// the crop a leading stage starts from, and the one seed everything the
|
||||
// effect draws comes from.
|
||||
Image input = canvasFromSources(sources, width, height, rng);
|
||||
const std::uint64_t seed = rng.next();
|
||||
|
||||
return animate(std::move(input), values, Moment(at, seed));
|
||||
}
|
||||
|
||||
} // namespace effector
|
||||
Reference in New Issue
Block a user