diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index e86adec96..758d690ba 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -354,8 +354,8 @@ pub trait EffectHost: Sync { self.layer(id, false) } /// The effect's own layer at another **layer time** `layer_time` (seconds): its source with - /// masks applied, followed by the first `effects` effects of its stack (0 = none, which is - /// what After Effects' Time effects see). `None` when unavailable (no host, recursion + /// masks applied, followed by the first `effects` effects of its stack (0 = none; the host + /// never goes past the effect being rendered). `None` when unavailable (no host, recursion /// limit, adjustment layers). Time effects use this to read neighbouring frames. fn self_at(&self, _layer_time: f64, _effects: usize) -> Option { None diff --git a/crates/effects/src/time_fx.rs b/crates/effects/src/time_fx.rs index d455ce039..27d2264e5 100644 --- a/crates/effects/src/time_fx.rs +++ b/crates/effects/src/time_fx.rs @@ -1,10 +1,11 @@ //! Time effects (Effect > Time): Echo, Posterize Time, Time Difference, Time Displacement, //! Timewarp, CC Force Motion Blur, CC Wide Time and Pixel Motion Blur. //! -//! These read the layer at *other* layer times through [`EffectHost::self_at`] with zero -//! preceding effects: like After Effects' Time effects they see the layer's source with its -//! masks and ignore effects applied before them (precompose to include those). Without a host -//! (tests, thumbnails) they pass their input through. +//! These read the layer at *other* layer times through [`EffectHost::self_at`]: its source with +//! its masks and the effects applied before them, as After Effects renders them (SceneYard: +//! Echo after CC Particle Systems II echoes the fireworks, not the black solid under them; +//! Posterize Time after Turbulent Displace steps the displacement). Without a host (tests, +//! thumbnails) they pass their input through. //! //! Timewarp's Pixel Motion estimates block-matching optical flow between neighbouring frames //! ([`effectcraft_raster::flow`]); Pixel Motion Blur averages sub-frame samples. @@ -169,7 +170,8 @@ fn fetch(ctx: &EffectCtx, times: &[f64]) -> Option { let mut uniq: Vec = times.iter().map(|t| (t * 1e6).round() as i64).collect(); uniq.sort_unstable(); uniq.dedup(); - let bufs: Vec<(i64, Option)> = uniq.iter().map(|&k| (k, host.self_at(k as f64 / 1e6, 0))).collect(); + // Every effect before this one (the host bounds the count to them). + let bufs: Vec<(i64, Option)> = uniq.iter().map(|&k| (k, host.self_at(k as f64 / 1e6, usize::MAX))).collect(); let first = bufs.iter().find_map(|(_, b)| b.as_ref())?; let scale = first.scale; // Union of every frame's extent in layer pixels (offset = where layer (0,0) sits). @@ -1014,7 +1016,8 @@ mod tests { None } fn self_at(&self, t: f64, effects: usize) -> Option { - assert_eq!(effects, 0); + // Time effects see every effect before them. + assert_eq!(effects, usize::MAX); let v = if self.static_layer { 0.4 } else { t as f32 }; Some(Buf { img: Image::filled(6, 4, [v, v, v, 1.0]), offset: [0.0; 2], scale: 1.0 }) } diff --git a/crates/render/src/tests_time.rs b/crates/render/src/tests_time.rs index 31ed9db3c..8739140de 100644 --- a/crates/render/src/tests_time.rs +++ b/crates/render/src/tests_time.rs @@ -64,6 +64,22 @@ fn echo_blends_neighbouring_frames() { assert!((a - want).abs() < 0.01, "{a} vs {want}"); } +/// Time effects see the effects applied before them, as After Effects renders them (SceneYard +/// "Task Preparation UI": Echo after CC Particle Systems II echoed the black solid under the +/// fireworks and blacked out the white page). Fill (red) then Echo: the echoes are red. +#[test] +fn time_effects_see_the_effects_before_them() { + let (mut p, cid) = scene(Some(("ec.generate.fill", &[]))); + let spec = effectcraft_effects::find("ec.time.echo").unwrap(); + let mut next = p.next_id; + let mut g = effectcraft_effects::instantiate(spec, &mut Ids(&mut next), spec.name, [200.0, 100.0]); + g.prop_mut("numberOfEchoes").unwrap().value = Value::Scalar(0.0); + p.next_id = next; + p.comp_mut(cid).unwrap().layers[0].props.sub_mut("effects").unwrap().children.push(g.into()); + let c = render(&p, cid, 1.5, None).get(100, 50); + assert!(c[0] > 0.99 && c[1] < 0.01 && c[2] < 0.01 && c[3] > 0.99, "{c:?}"); +} + #[test] fn time_difference_of_static_layer_is_black() { let mut p = Project::default();