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>
67 lines
2.5 KiB
C++
67 lines
2.5 KiB
C++
#include "effects/video_effect.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <stdexcept>
|
|
|
|
#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};
|
|
|
|
// An effect built from the sources takes no crop, only the one seed
|
|
// everything it draws comes from -- the same value on every frame.
|
|
if (needsSources()) {
|
|
if (sources.empty()) throw EffectError("No source images.");
|
|
const std::uint64_t seed = rng.next();
|
|
return compose(sources, width, height, values, Moment(at, seed));
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
|
|
// A video effect writes one of these two; the other is never called.
|
|
Image VideoEffect::animate(Image, const ParamValues&, const Moment&) const {
|
|
throw std::logic_error(name() + " is a transform but writes no animate()");
|
|
}
|
|
|
|
Image VideoEffect::compose(const std::vector<Image>&, int, int, const ParamValues&,
|
|
const Moment&) const {
|
|
throw std::logic_error(name() + " needs sources but writes no compose()");
|
|
}
|
|
|
|
} // namespace effector
|