M9.3: effect env (masks, layer params, audio, comp timing), sim cache, polyline helpers; module stubs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dee16181fe
commit
c91af194f2
|
|
@ -621,7 +621,8 @@ mod tests {
|
|||
for (k, v) in over {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: false };
|
||||
let ctx =
|
||||
EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: false, env: Default::default() };
|
||||
(s.render)(&ctx, Buf { img: img.clone(), offset: [0.0, 0.0], scale: 1.0 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -425,7 +425,8 @@ mod tests {
|
|||
for (k, v) in over {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: true };
|
||||
let ctx =
|
||||
EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: true, env: Default::default() };
|
||||
apply(s, &ctx, Buf { img, offset: [0.0, 0.0], scale: 1.0 }).img
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
//! Channel effects, batch 2.
|
||||
|
||||
use crate::EffectSpec;
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
Vec::new()
|
||||
}
|
||||
|
|
@ -886,7 +886,8 @@ mod tests {
|
|||
for (k, v) in over {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: false };
|
||||
let ctx =
|
||||
EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: false, env: Default::default() };
|
||||
(s.render)(&ctx, Buf { img: img.clone(), offset: [0.0, 0.0], scale: 1.0 }).img
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
//! Color Correction effects, batch 3.
|
||||
|
||||
use crate::EffectSpec;
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
Vec::new()
|
||||
}
|
||||
|
|
@ -690,7 +690,7 @@ mod tests {
|
|||
for (k, v) in set {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.5, layer_size: ls, seed: 3, adjustment: false };
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.5, layer_size: ls, seed: 3, adjustment: false, env: Default::default() };
|
||||
crate::apply(s, &ctx, Buf { img, offset: [0.0, 0.0], scale: 1.0 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
//! Distort effects, batch 3.
|
||||
|
||||
use crate::EffectSpec;
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
Vec::new()
|
||||
}
|
||||
|
|
@ -754,7 +754,7 @@ mod tests {
|
|||
for (k, v) in set {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.5, layer_size: ls, seed: 3, adjustment: false };
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.5, layer_size: ls, seed: 3, adjustment: false, env: Default::default() };
|
||||
crate::apply(s, &ctx, Buf { img, offset: [0.0, 0.0], scale: 1.0 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
//! Generate effects, batch 3.
|
||||
|
||||
use crate::EffectSpec;
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
Vec::new()
|
||||
}
|
||||
|
|
@ -617,7 +617,8 @@ mod tests {
|
|||
for (k, v) in over {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: false };
|
||||
let ctx =
|
||||
EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: false, env: Default::default() };
|
||||
apply(s, &ctx, Buf { img, offset: [0.0, 0.0], scale: 1.0 }).img
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
//! Keying effects, batch 2.
|
||||
|
||||
use crate::EffectSpec;
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
Vec::new()
|
||||
}
|
||||
|
|
@ -8,22 +8,36 @@
|
|||
|
||||
mod blur2;
|
||||
mod channel;
|
||||
mod channel2;
|
||||
mod color2;
|
||||
mod color3;
|
||||
mod color_fx;
|
||||
mod controls;
|
||||
mod distort;
|
||||
mod distort2;
|
||||
mod distort3;
|
||||
mod generate;
|
||||
mod generate2;
|
||||
mod generate3;
|
||||
mod keying;
|
||||
mod keying2;
|
||||
mod matte;
|
||||
mod misc;
|
||||
mod noise;
|
||||
mod noise2;
|
||||
mod obsolete;
|
||||
mod perspective;
|
||||
mod perspective2;
|
||||
mod sim;
|
||||
mod sim2;
|
||||
mod stylize2;
|
||||
mod stylize3;
|
||||
mod textfx;
|
||||
mod transition;
|
||||
mod transition2;
|
||||
pub mod util;
|
||||
mod utility;
|
||||
mod utility2;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
|
@ -122,6 +136,45 @@ impl Buf {
|
|||
}
|
||||
}
|
||||
|
||||
/// A layer mask at the current time, flattened to a polyline in layer coordinates.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct MaskShape {
|
||||
pub name: String,
|
||||
pub points: Vec<[f64; 2]>,
|
||||
pub closed: bool,
|
||||
pub inverted: bool,
|
||||
}
|
||||
|
||||
/// Pixels of another layer referenced by a layer parameter: its buffer (layer space, see
|
||||
/// [`Buf`]) and its source size in layer pixels.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LayerPixels {
|
||||
pub buf: Buf,
|
||||
pub size: [f64; 2],
|
||||
}
|
||||
|
||||
/// Services the renderer offers effects that look beyond their own pixels.
|
||||
pub trait EffectHost: Sync {
|
||||
/// Layer `id` of the same composition at the current comp time. `masks_and_effects`
|
||||
/// selects "Effects & Masks" over "Source". `None` for missing/self/recursive references.
|
||||
fn layer(&self, id: u64, masks_and_effects: bool) -> Option<LayerPixels>;
|
||||
/// `frames` stereo sample frames (interleaved L R …) of layer `id`'s audio starting at comp
|
||||
/// time `start` seconds, at `rate` Hz. `None` when the layer has no audio.
|
||||
fn audio(&self, id: u64, start: f64, frames: usize, rate: u32) -> Option<Vec<f32>>;
|
||||
}
|
||||
|
||||
/// Extra context the renderer may supply (all optional; `Default` is "nothing known").
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct EffectEnv<'a> {
|
||||
/// The layer's masks (enabled ones, top to bottom).
|
||||
pub masks: &'a [MaskShape],
|
||||
pub host: Option<&'a dyn EffectHost>,
|
||||
/// Composition time in seconds.
|
||||
pub comp_time: f64,
|
||||
/// Composition frame rate (0 when unknown).
|
||||
pub frame_rate: f64,
|
||||
}
|
||||
|
||||
/// What an effect gets to render with.
|
||||
pub struct EffectCtx<'a> {
|
||||
pub params: &'a Params,
|
||||
|
|
@ -133,6 +186,20 @@ pub struct EffectCtx<'a> {
|
|||
pub seed: u32,
|
||||
/// Rendering for an adjustment layer (operating on the comp below).
|
||||
pub adjustment: bool,
|
||||
/// Masks, other layers, audio, comp timing.
|
||||
pub env: EffectEnv<'a>,
|
||||
}
|
||||
|
||||
impl EffectCtx<'_> {
|
||||
/// The layer chosen in layer parameter `id`, via the host.
|
||||
pub fn layer_param(&self, id: &str, masks_and_effects: bool) -> Option<LayerPixels> {
|
||||
let lid = self.params.get(id)?.as_layer()?;
|
||||
self.env.host?.layer(lid, masks_and_effects)
|
||||
}
|
||||
/// Frame rate for frame-based maths (comp rate, else 30).
|
||||
pub fn fps(&self) -> f64 {
|
||||
if self.env.frame_rate > 0.0 { self.env.frame_rate } else { 30.0 }
|
||||
}
|
||||
}
|
||||
|
||||
pub type RenderFn = fn(&EffectCtx, Buf) -> Buf;
|
||||
|
|
@ -187,6 +254,20 @@ pub fn registry() -> &'static [EffectSpec] {
|
|||
v.extend(perspective::specs());
|
||||
v.extend(transition::specs());
|
||||
v.extend(utility::specs());
|
||||
v.extend(sim::specs());
|
||||
v.extend(sim2::specs());
|
||||
v.extend(distort3::specs());
|
||||
v.extend(perspective2::specs());
|
||||
v.extend(utility2::specs());
|
||||
v.extend(generate3::specs());
|
||||
v.extend(textfx::specs());
|
||||
v.extend(stylize3::specs());
|
||||
v.extend(transition2::specs());
|
||||
v.extend(channel2::specs());
|
||||
v.extend(keying2::specs());
|
||||
v.extend(noise2::specs());
|
||||
v.extend(color3::specs());
|
||||
v.extend(obsolete::specs());
|
||||
v.sort_by(|a, b| a.category.cmp(b.category).then(a.name.cmp(b.name)));
|
||||
v
|
||||
})
|
||||
|
|
@ -227,10 +308,39 @@ pub fn apply(spec: &EffectSpec, ctx: &EffectCtx, buf: Buf) -> Buf {
|
|||
(spec.render)(ctx, buf)
|
||||
}
|
||||
|
||||
/// Test helper: run effect `id` on `img` (scale 1, layer size = image size) with its defaults
|
||||
/// overridden by `vals`, at layer time `time`, with an optional environment.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn run_fx(id: &str, vals: &[(&str, Value)], img: Image, time: f64, env: EffectEnv) -> Buf {
|
||||
let s = find(id).unwrap_or_else(|| panic!("no effect {id}"));
|
||||
let mut params = Params { values: s.params.iter().map(|p| (p.id.to_string(), p.default.clone())).collect() };
|
||||
for (k, v) in vals {
|
||||
assert!(params.values.contains_key(*k), "{id}: unknown param {k}");
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: false, env };
|
||||
apply(s, &ctx, Buf { img, offset: [0.0, 0.0], scale: 1.0 })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn run_fx_passthrough() {
|
||||
let img = Image::filled(4, 3, [0.5, 0.25, 0.125, 1.0]);
|
||||
let out = run_fx("ec.control.slider", &[("slider", num(3.0))], img.clone(), 0.0, EffectEnv::default());
|
||||
assert_eq!(out.img.data, img.data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn dump_registry() {
|
||||
for s in registry() {
|
||||
println!("{} | {} | {}", s.category, s.name, s.id);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_is_consistent() {
|
||||
let r = registry();
|
||||
|
|
@ -259,7 +369,7 @@ mod tests {
|
|||
}
|
||||
for s in registry() {
|
||||
let params = Params { values: s.params.iter().map(|p| (p.id.to_string(), p.default.clone())).collect() };
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.5, layer_size: [48.0, 32.0], seed: 1, adjustment: false };
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.5, layer_size: [48.0, 32.0], seed: 1, adjustment: false, env: Default::default() };
|
||||
let out = apply(s, &ctx, Buf { img: img.clone(), offset: [0.0, 0.0], scale: 1.0 });
|
||||
assert!(!out.img.is_empty(), "{}", s.id);
|
||||
assert!(out.img.data.iter().all(|p| p[3].is_finite() && p[3] >= -1e-4 && p[3] <= 1.0001), "{} alpha out of range", s.id);
|
||||
|
|
|
|||
|
|
@ -200,7 +200,8 @@ mod tests {
|
|||
for (k, v) in over {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: true };
|
||||
let ctx =
|
||||
EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: true, env: Default::default() };
|
||||
apply(s, &ctx, Buf { img, offset: [0.0, 0.0], scale: 1.0 }).img
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -531,7 +531,7 @@ mod tests {
|
|||
for (k, v) in vals {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time, layer_size: [img.width as f64, img.height as f64], seed: 3, adjustment: false };
|
||||
let ctx = EffectCtx { params: ¶ms, time, layer_size: [img.width as f64, img.height as f64], seed: 3, adjustment: false, env: Default::default() };
|
||||
crate::apply(s, &ctx, Buf { img, offset: [0.0, 0.0], scale: 1.0 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
//! Noise & Grain effects, batch 2.
|
||||
|
||||
use crate::EffectSpec;
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
Vec::new()
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
//! Obsolete-category effects.
|
||||
|
||||
use crate::EffectSpec;
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
Vec::new()
|
||||
}
|
||||
|
|
@ -452,7 +452,8 @@ mod tests {
|
|||
params.values.insert(ps.id.to_string(), Value::Vec2([f[0] * img.width as f64, f[1] * img.height as f64]));
|
||||
}
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: false };
|
||||
let ctx =
|
||||
EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: false, env: Default::default() };
|
||||
(s.render)(&ctx, Buf { img: img.clone(), offset: [0.0, 0.0], scale: 1.0 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
//! Perspective effects, batch 2.
|
||||
|
||||
use crate::EffectSpec;
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
Vec::new()
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
//! Simulation effects (particles, physics, liquids).
|
||||
|
||||
use crate::EffectSpec;
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
Vec::new()
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
//! Simulation effects, part 2.
|
||||
|
||||
use crate::EffectSpec;
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
Vec::new()
|
||||
}
|
||||
|
|
@ -698,7 +698,7 @@ mod tests {
|
|||
for (k, v) in vals {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: false };
|
||||
let ctx = EffectCtx { params: ¶ms, time, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: false, env: Default::default() };
|
||||
crate::apply(s, &ctx, Buf { img, offset: [0.0, 0.0], scale: 1.0 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
//! Stylize effects, batch 3.
|
||||
|
||||
use crate::EffectSpec;
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
Vec::new()
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
//! Text effects (Numbers, Timecode) and the obsolete text generators, drawn with a built-in stroke font.
|
||||
|
||||
use crate::EffectSpec;
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
Vec::new()
|
||||
}
|
||||
|
|
@ -486,7 +486,8 @@ mod tests {
|
|||
for (k, v) in set {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 7, adjustment: false };
|
||||
let ctx =
|
||||
EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 7, adjustment: false, env: Default::default() };
|
||||
(s.render)(&ctx, Buf { img: img.clone(), offset: [0.0, 0.0], scale: 1.0 }).img
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
//! Transition effects, batch 2.
|
||||
|
||||
use crate::EffectSpec;
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
Vec::new()
|
||||
}
|
||||
|
|
@ -427,10 +427,253 @@ pub fn layer_rect(ctx: &crate::EffectCtx, b: &crate::Buf) -> (f64, f64, f64, f64
|
|||
(b.offset[0], b.offset[1], ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale)
|
||||
}
|
||||
|
||||
/// Resample another layer (a layer parameter) into `b`'s pixel grid. With `stretch` the other
|
||||
/// layer is scaled to this layer's size ("Stretch to Fit"); otherwise it is centred.
|
||||
pub fn fit_layer(ctx: &crate::EffectCtx, b: &crate::Buf, other: &crate::LayerPixels, stretch: bool) -> Image {
|
||||
let ls = ctx.layer_size;
|
||||
let os = other.size;
|
||||
let (sx, sy) = if stretch && ls[0] > 0.0 && ls[1] > 0.0 { (os[0] / ls[0], os[1] / ls[1]) } else { (1.0, 1.0) };
|
||||
let (dx, dy) = if stretch { (0.0, 0.0) } else { ((os[0] - ls[0]) * 0.5, (os[1] - ls[1]) * 0.5) };
|
||||
let inv = 1.0 / b.scale.max(1e-9);
|
||||
gen_image(b.img.width, b.img.height, |x, y| {
|
||||
let px = (x as f64 + 0.5 - b.offset[0]) * inv;
|
||||
let py = (y as f64 + 0.5 - b.offset[1]) * inv;
|
||||
let qx = px * sx + dx;
|
||||
let qy = py * sy + dy;
|
||||
other.buf.img.sample_bilinear(qx * other.buf.scale + other.buf.offset[0], qy * other.buf.scale + other.buf.offset[1])
|
||||
})
|
||||
}
|
||||
|
||||
/// The layer chosen in layer parameter `id` fitted to `b` (see [`fit_layer`]), or a copy of
|
||||
/// `b`'s own pixels when none is chosen / available (After Effects' "self" behaviour).
|
||||
pub fn layer_or_self(ctx: &crate::EffectCtx, b: &crate::Buf, id: &str, masks_and_effects: bool, stretch: bool) -> Image {
|
||||
match ctx.layer_param(id, masks_and_effects) {
|
||||
Some(o) => fit_layer(ctx, b, &o, stretch),
|
||||
None => b.img.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A stable hash of an effect instance's evaluated parameters, seed, layer size and buffer
|
||||
/// scale (cache key for simulations).
|
||||
pub fn params_key(ctx: &crate::EffectCtx, b: &crate::Buf, salt: u64) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
let mut keys: Vec<&String> = ctx.params.values.keys().collect();
|
||||
keys.sort();
|
||||
for k in keys {
|
||||
k.hash(&mut h);
|
||||
format!("{:?}", ctx.params.values[k]).hash(&mut h);
|
||||
}
|
||||
ctx.seed.hash(&mut h);
|
||||
ctx.layer_size[0].to_bits().hash(&mut h);
|
||||
ctx.layer_size[1].to_bits().hash(&mut h);
|
||||
b.scale.to_bits().hash(&mut h);
|
||||
salt.hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// Checkpointed, deterministic, time-seekable simulation cache.
|
||||
///
|
||||
/// A simulation is a state advanced in fixed steps from layer time 0. [`SimCache::run`] returns
|
||||
/// the state after `target` steps, starting from the nearest cached checkpoint at or before it
|
||||
/// (so scrubbing forward is incremental and seeking anywhere gives the same answer as
|
||||
/// simulating from scratch). Entries are keyed by [`params_key`]; the least recently used key
|
||||
/// is evicted beyond `cap` keys.
|
||||
pub struct SimCache<S> {
|
||||
inner: std::sync::Mutex<Vec<SimEntry<S>>>,
|
||||
cap: usize,
|
||||
}
|
||||
|
||||
struct SimEntry<S> {
|
||||
key: u64,
|
||||
used: u64,
|
||||
checkpoints: Vec<(u64, std::sync::Arc<S>)>,
|
||||
}
|
||||
|
||||
/// Checkpoint every this many steps.
|
||||
pub const SIM_CHECKPOINT: u64 = 15;
|
||||
|
||||
impl<S: Clone> SimCache<S> {
|
||||
pub const fn new(cap: usize) -> SimCache<S> {
|
||||
SimCache { inner: std::sync::Mutex::new(Vec::new()), cap }
|
||||
}
|
||||
|
||||
/// State after `target` steps. `init` builds step 0; `step(state, i)` advances from step
|
||||
/// `i` to `i + 1`.
|
||||
pub fn run(&self, key: u64, target: u64, init: impl FnOnce() -> S, mut step: impl FnMut(&mut S, u64)) -> std::sync::Arc<S> {
|
||||
use std::sync::Arc;
|
||||
let start = {
|
||||
let mut g = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let tick = g.iter().map(|e| e.used).max().unwrap_or(0) + 1;
|
||||
g.iter_mut().find(|e| e.key == key).and_then(|e| {
|
||||
e.used = tick;
|
||||
e.checkpoints.iter().filter(|(s, _)| *s <= target).max_by_key(|(s, _)| *s).map(|(s, st)| (*s, st.clone()))
|
||||
})
|
||||
};
|
||||
let had_start = start.is_some();
|
||||
let (mut i, mut st) = match start {
|
||||
Some((s, st)) if s == target => return st,
|
||||
Some((s, st)) => (s, (*st).clone()),
|
||||
None => (0, init()),
|
||||
};
|
||||
let mut fresh: Vec<(u64, Arc<S>)> = Vec::new();
|
||||
if !had_start {
|
||||
fresh.push((0, Arc::new(st.clone())));
|
||||
}
|
||||
while i < target {
|
||||
step(&mut st, i);
|
||||
i += 1;
|
||||
if i % SIM_CHECKPOINT == 0 && i != target {
|
||||
fresh.push((i, Arc::new(st.clone())));
|
||||
}
|
||||
}
|
||||
let out = Arc::new(st);
|
||||
fresh.push((target, out.clone()));
|
||||
let mut g = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let tick = g.iter().map(|e| e.used).max().unwrap_or(0) + 1;
|
||||
let idx = match g.iter().position(|e| e.key == key) {
|
||||
Some(ix) => ix,
|
||||
None => {
|
||||
if g.len() >= self.cap.max(1) {
|
||||
let old = g.iter().enumerate().min_by_key(|(_, e)| e.used).map(|(ix, _)| ix).unwrap_or(0);
|
||||
g.remove(old);
|
||||
}
|
||||
g.push(SimEntry { key, used: tick, checkpoints: Vec::new() });
|
||||
g.len() - 1
|
||||
}
|
||||
};
|
||||
let e = &mut g[idx];
|
||||
e.used = tick;
|
||||
for (s, st) in fresh {
|
||||
if !e.checkpoints.iter().any(|(c, _)| *c == s) {
|
||||
e.checkpoints.push((s, st));
|
||||
}
|
||||
}
|
||||
// Keep memory bounded: drop unaligned (exact-target) checkpoints first, then thin.
|
||||
if e.checkpoints.len() > 256 {
|
||||
e.checkpoints.retain(|(s, _)| s % SIM_CHECKPOINT == 0);
|
||||
e.checkpoints.sort_by_key(|(s, _)| *s);
|
||||
let mut k = 0;
|
||||
e.checkpoints.retain(|_| {
|
||||
k += 1;
|
||||
k % 2 == 1
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Point-in-polygon (even-odd) for a closed polyline.
|
||||
pub fn point_in_poly(pts: &[[f64; 2]], x: f64, y: f64) -> bool {
|
||||
let n = pts.len();
|
||||
if n < 3 {
|
||||
return false;
|
||||
}
|
||||
let mut inside = false;
|
||||
let mut j = n - 1;
|
||||
for i in 0..n {
|
||||
let (a, b) = (pts[i], pts[j]);
|
||||
if (a[1] > y) != (b[1] > y) && x < (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]) + a[0] {
|
||||
inside = !inside;
|
||||
}
|
||||
j = i;
|
||||
}
|
||||
inside
|
||||
}
|
||||
|
||||
/// Distance from a point to a polyline (closed adds the last→first segment).
|
||||
pub fn dist_to_poly(pts: &[[f64; 2]], closed: bool, x: f64, y: f64) -> f64 {
|
||||
let n = pts.len();
|
||||
if n == 0 {
|
||||
return f64::INFINITY;
|
||||
}
|
||||
if n == 1 {
|
||||
return ((pts[0][0] - x).powi(2) + (pts[0][1] - y).powi(2)).sqrt();
|
||||
}
|
||||
let segs = if closed { n } else { n - 1 };
|
||||
let mut best = f64::INFINITY;
|
||||
for i in 0..segs {
|
||||
let a = pts[i];
|
||||
let b = pts[(i + 1) % n];
|
||||
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
|
||||
let l2 = dx * dx + dy * dy;
|
||||
let t = if l2 > 0.0 { (((x - a[0]) * dx + (y - a[1]) * dy) / l2).clamp(0.0, 1.0) } else { 0.0 };
|
||||
let (px, py) = (a[0] + dx * t, a[1] + dy * t);
|
||||
best = best.min((px - x).powi(2) + (py - y).powi(2));
|
||||
}
|
||||
best.sqrt()
|
||||
}
|
||||
|
||||
/// Total length of a polyline.
|
||||
pub fn poly_length(pts: &[[f64; 2]], closed: bool) -> f64 {
|
||||
let n = pts.len();
|
||||
if n < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
let segs = if closed { n } else { n - 1 };
|
||||
(0..segs)
|
||||
.map(|i| {
|
||||
let (a, b) = (pts[i], pts[(i + 1) % n]);
|
||||
((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt()
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Point and unit tangent at arc length `s` along a polyline.
|
||||
pub fn poly_point_at(pts: &[[f64; 2]], closed: bool, s: f64) -> ([f64; 2], [f64; 2]) {
|
||||
let n = pts.len();
|
||||
if n == 0 {
|
||||
return ([0.0; 2], [1.0, 0.0]);
|
||||
}
|
||||
if n == 1 {
|
||||
return (pts[0], [1.0, 0.0]);
|
||||
}
|
||||
let segs = if closed { n } else { n - 1 };
|
||||
let mut rem = s.max(0.0);
|
||||
for i in 0..segs {
|
||||
let (a, b) = (pts[i], pts[(i + 1) % n]);
|
||||
let l = ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
|
||||
if rem <= l || i == segs - 1 {
|
||||
let t = if l > 0.0 { (rem / l).min(1.0) } else { 0.0 };
|
||||
let d = if l > 0.0 { [(b[0] - a[0]) / l, (b[1] - a[1]) / l] } else { [1.0, 0.0] };
|
||||
return ([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t], d);
|
||||
}
|
||||
rem -= l;
|
||||
}
|
||||
(pts[n - 1], [1.0, 0.0])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sim_cache_is_seek_consistent() {
|
||||
static C: SimCache<u64> = SimCache::new(2);
|
||||
let step = |s: &mut u64, i: u64| *s = s.wrapping_mul(6364136223846793005).wrapping_add(i);
|
||||
let direct = *C.run(1, 100, || 7, step);
|
||||
let fresh: SimCache<u64> = SimCache::new(2);
|
||||
let a = *fresh.run(1, 40, || 7, step);
|
||||
let b = *fresh.run(1, 100, || 7, step);
|
||||
let c = *fresh.run(1, 40, || 7, step);
|
||||
let d = *fresh.run(1, 77, || 7, step);
|
||||
let e = *C.run(1, 77, || 7, step);
|
||||
assert_eq!(b, direct);
|
||||
assert_eq!(a, c);
|
||||
assert_eq!(d, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poly_helpers() {
|
||||
let sq = [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]];
|
||||
assert!(point_in_poly(&sq, 5.0, 5.0));
|
||||
assert!(!point_in_poly(&sq, 15.0, 5.0));
|
||||
assert!((poly_length(&sq, true) - 40.0).abs() < 1e-9);
|
||||
assert!((dist_to_poly(&sq, true, 5.0, 5.0) - 5.0).abs() < 1e-9);
|
||||
let (pt, _) = poly_point_at(&sq, true, 15.0);
|
||||
assert!((pt[0] - 10.0).abs() < 1e-9 && (pt[1] - 5.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minmax_matches_brute_force() {
|
||||
let src: Vec<f32> = (0..37).map(|i| ((i * 7919) % 31) as f32).collect();
|
||||
|
|
|
|||
|
|
@ -363,7 +363,8 @@ mod tests {
|
|||
for (k, v) in vals {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: false };
|
||||
let ctx =
|
||||
EffectCtx { params: ¶ms, time: 0.0, layer_size: [img.width as f64, img.height as f64], seed: 1, adjustment: false, env: Default::default() };
|
||||
crate::apply(s, &ctx, Buf { img, offset: [0.0, 0.0], scale: 1.0 })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
//! Utility effects, batch 2.
|
||||
|
||||
use crate::EffectSpec;
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
Vec::new()
|
||||
}
|
||||
|
|
@ -454,6 +454,13 @@ impl FootageSource for MediaPool {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn audio(&self, _item: ItemId, footage: &Footage, t: Tick, frames: usize, rate: u32) -> Option<Vec<f32>> {
|
||||
if !footage.has_audio || footage.missing {
|
||||
return None;
|
||||
}
|
||||
Some(self.audio_samples(footage, t, frames, rate))
|
||||
}
|
||||
}
|
||||
|
||||
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ pub mod text;
|
|||
use std::sync::Arc;
|
||||
|
||||
use effectcraft_color::BlendMode;
|
||||
use effectcraft_effects::{Buf, EffectCtx, Params};
|
||||
use effectcraft_effects::{Buf, EffectCtx, EffectEnv, EffectHost, LayerPixels, Params};
|
||||
use effectcraft_geom::{Mat3, vec2};
|
||||
use effectcraft_project::{Comp, Footage, GroupKind, ItemId, ItemKind, Layer, LayerSource, MatteKind, Node, Project, Quality, Sampling};
|
||||
pub use effectcraft_raster::Image;
|
||||
|
|
@ -27,6 +27,43 @@ use rayon::prelude::*;
|
|||
pub trait FootageSource: Send + Sync {
|
||||
/// The frame of `item` at source time `t`, straight from the file (any size).
|
||||
fn frame(&self, item: ItemId, footage: &Footage, t: Tick) -> Option<Arc<Image>>;
|
||||
/// `frames` interleaved stereo sample frames of `item`'s audio from source time `t` at
|
||||
/// `rate` Hz (`None` when unavailable).
|
||||
fn audio(&self, _item: ItemId, _footage: &Footage, _t: Tick, _frames: usize, _rate: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer parameters and audio for effects (see [`effectcraft_effects::EffectHost`]).
|
||||
struct FxHost<'r, 'a, 'c> {
|
||||
r: &'r Renderer<'a>,
|
||||
ctx: &'c EvalCtx<'a>,
|
||||
layer: &'c Layer,
|
||||
}
|
||||
|
||||
impl EffectHost for FxHost<'_, '_, '_> {
|
||||
fn layer(&self, id: u64, masks_and_effects: bool) -> Option<LayerPixels> {
|
||||
let other = self.ctx.layer(effectcraft_project::LayerId(id))?;
|
||||
if other.id == self.layer.id || self.r.depth > 8 {
|
||||
return None;
|
||||
}
|
||||
let sub = Renderer { project: self.r.project, footage: self.r.footage, expr: self.r.expr, opts: self.r.opts, depth: self.r.depth + 1 };
|
||||
let buf = if masks_and_effects { sub.layer_buf(self.ctx, other)? } else { sub.source(self.ctx, other)? };
|
||||
let size = source_size(self.r.project, other);
|
||||
let size = if size.0 == 0 { [self.ctx.comp.width as f64, self.ctx.comp.height as f64] } else { [size.0 as f64, size.1 as f64] };
|
||||
Some(LayerPixels { buf, size })
|
||||
}
|
||||
|
||||
fn audio(&self, id: u64, start: f64, frames: usize, rate: u32) -> Option<Vec<f32>> {
|
||||
let other = self.ctx.layer(effectcraft_project::LayerId(id))?;
|
||||
let LayerSource::Footage { item } = &other.source else { return None };
|
||||
let ItemKind::Footage(f) = &self.r.project.item(*item)?.kind else { return None };
|
||||
if !f.has_audio {
|
||||
return None;
|
||||
}
|
||||
let st = other.layer_time(Tick::from_seconds_f64(start));
|
||||
self.r.footage.audio(*item, f, st, frames, rate)
|
||||
}
|
||||
}
|
||||
|
||||
/// No footage available (renders footage layers as transparent).
|
||||
|
|
@ -134,6 +171,9 @@ impl<'a> Renderer<'a> {
|
|||
let lt = layer.layer_time(ctx.time);
|
||||
let size = source_size(self.project, layer);
|
||||
let layer_size = if size.0 == 0 { [ctx.comp.width as f64, ctx.comp.height as f64] } else { [size.0 as f64, size.1 as f64] };
|
||||
let mask_shapes = masks::shapes(ctx, layer);
|
||||
let host = FxHost { r: self, ctx, layer };
|
||||
let env = EffectEnv { masks: &mask_shapes, host: Some(&host), comp_time: ctx.time.seconds(), frame_rate: ctx.comp.frame_rate.as_f64() };
|
||||
for g in fx.groups() {
|
||||
if !g.enabled {
|
||||
continue;
|
||||
|
|
@ -141,7 +181,7 @@ impl<'a> Renderer<'a> {
|
|||
let GroupKind::Effect { effect } = &g.kind else { continue };
|
||||
let Some(spec) = effectcraft_effects::find(effect) else { continue };
|
||||
let params = self.effect_params(ctx, layer, g);
|
||||
let ectx = EffectCtx { params: ¶ms, time: lt.seconds(), layer_size, seed: g.uid as u32, adjustment };
|
||||
let ectx = EffectCtx { params: ¶ms, time: lt.seconds(), layer_size, seed: g.uid as u32, adjustment, env };
|
||||
buf = effectcraft_effects::apply(spec, &ectx, buf);
|
||||
}
|
||||
buf
|
||||
|
|
|
|||
|
|
@ -39,6 +39,34 @@ fn coverage(ctx: &EvalCtx, layer: &Layer, g: &effectcraft_project::PropGroup, bu
|
|||
Some(cov)
|
||||
}
|
||||
|
||||
/// The layer's enabled masks at the context time, flattened to polylines in layer space (for
|
||||
/// effects that use masks as paths: Stroke, Scribble, Inner/Outer Key, Reshape…).
|
||||
pub fn shapes(ctx: &EvalCtx, layer: &Layer) -> Vec<effectcraft_effects::MaskShape> {
|
||||
let Some(masks) = layer.masks() else { return Vec::new() };
|
||||
let mut out = Vec::new();
|
||||
for g in masks.groups().filter(|g| g.enabled) {
|
||||
let GroupKind::Mask { inverted, .. } = g.kind else { continue };
|
||||
let Some(Value::Path(sp)) = ctx.group_value(layer, g, "path") else { continue };
|
||||
let path = effectcraft_path::to_kurbo(&sp);
|
||||
let mut pts: Vec<[f64; 2]> = Vec::new();
|
||||
let mut closed = false;
|
||||
kurbo::flatten(path.iter(), 0.25, |el| match el {
|
||||
effectcraft_path::PathEl::MoveTo(p) | effectcraft_path::PathEl::LineTo(p) => {
|
||||
if pts.last() != Some(&[p.x, p.y]) {
|
||||
pts.push([p.x, p.y]);
|
||||
}
|
||||
}
|
||||
effectcraft_path::PathEl::ClosePath => closed = true,
|
||||
_ => {}
|
||||
});
|
||||
if closed && pts.len() > 1 && pts.first() == pts.last() {
|
||||
pts.pop();
|
||||
}
|
||||
out.push(effectcraft_effects::MaskShape { name: g.name.clone(), points: pts, closed: closed || sp.closed, inverted });
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Apply the layer's masks to its buffer.
|
||||
pub fn apply(ctx: &EvalCtx, layer: &Layer, buf: &mut Buf) {
|
||||
let Some(masks) = layer.masks() else { return };
|
||||
|
|
|
|||
Loading…
Reference in New Issue