diff --git a/crates/rendering/src/mask.rs b/crates/rendering/src/mask.rs index c2c753d33cd..91c9f8291d6 100644 --- a/crates/rendering/src/mask.rs +++ b/crates/rendering/src/mask.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use cap_project::{ MaskKind, MaskScalarKeyframe, MaskSegment, MaskVectorKeyframe, XY, mask_effect_contract, }; @@ -6,17 +8,26 @@ use crate::{MaskRenderMode, PreparedMask}; const MASK_EFFECT_BASE_HEIGHT: f32 = 1080.0; -fn interpolate_vector(base: XY, keys: &[MaskVectorKeyframe], time: f64) -> XY { - if keys.is_empty() { - return base; +fn sorted_by_time(keys: &[T], time: impl Fn(&T) -> f64) -> Cow<'_, [T]> { + if keys.is_sorted_by(|a, b| time(a) <= time(b)) { + return Cow::Borrowed(keys); } let mut sorted = keys.to_vec(); sorted.sort_by(|a, b| { - a.time - .partial_cmp(&b.time) + time(a) + .partial_cmp(&time(b)) .unwrap_or(std::cmp::Ordering::Equal) }); + Cow::Owned(sorted) +} + +fn interpolate_vector(base: XY, keys: &[MaskVectorKeyframe], time: f64) -> XY { + if keys.is_empty() { + return base; + } + + let sorted = sorted_by_time(keys, |key| key.time); if time <= sorted[0].time { return XY::new(sorted[0].x, sorted[0].y); @@ -43,12 +54,7 @@ fn interpolate_scalar(base: f64, keys: &[MaskScalarKeyframe], time: f64) -> f64 return base; } - let mut sorted = keys.to_vec(); - sorted.sort_by(|a, b| { - a.time - .partial_cmp(&b.time) - .unwrap_or(std::cmp::Ordering::Equal) - }); + let sorted = sorted_by_time(keys, |key| key.time); if time <= sorted[0].time { return sorted[0].value; @@ -200,6 +206,194 @@ mod tests { } } + fn old_interpolate_vector(base: XY, keys: &[MaskVectorKeyframe], time: f64) -> XY { + if keys.is_empty() { + return base; + } + + let mut sorted = keys.to_vec(); + sorted.sort_by(|a, b| { + a.time + .partial_cmp(&b.time) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + if time <= sorted[0].time { + return XY::new(sorted[0].x, sorted[0].y); + } + + for window in sorted.windows(2) { + let prev = &window[0]; + let next = &window[1]; + if time <= next.time { + let span = (next.time - prev.time).max(1e-6); + let t = ((time - prev.time) / span).clamp(0.0, 1.0); + let x = prev.x + (next.x - prev.x) * t; + let y = prev.y + (next.y - prev.y) * t; + return XY::new(x, y); + } + } + + let last = sorted.last().unwrap(); + XY::new(last.x, last.y) + } + + fn old_interpolate_scalar(base: f64, keys: &[MaskScalarKeyframe], time: f64) -> f64 { + if keys.is_empty() { + return base; + } + + let mut sorted = keys.to_vec(); + sorted.sort_by(|a, b| { + a.time + .partial_cmp(&b.time) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + if time <= sorted[0].time { + return sorted[0].value; + } + + for window in sorted.windows(2) { + let prev = &window[0]; + let next = &window[1]; + if time <= next.time { + let span = (next.time - prev.time).max(1e-6); + let t = ((time - prev.time) / span).clamp(0.0, 1.0); + return prev.value + (next.value - prev.value) * t; + } + } + + sorted.last().map(|k| k.value).unwrap_or(base) + } + + fn vector_key(time: f64, x: f64, y: f64) -> MaskVectorKeyframe { + MaskVectorKeyframe { time, x, y } + } + + fn scalar_key(time: f64, value: f64) -> MaskScalarKeyframe { + MaskScalarKeyframe { time, value } + } + + const QUERY_TIMES: [f64; 13] = [ + f64::NEG_INFINITY, + -1.0, + 0.0, + 0.25, + 0.5, + 1.0, + 1.5, + 2.0, + 2.5, + 3.0, + f64::INFINITY, + f64::NAN, + -0.0, + ]; + + #[test] + fn keyframe_order_does_not_change_interpolation() { + let sorted = vec![ + vector_key(0.0, 0.1, 0.2), + vector_key(1.0, 0.4, 0.3), + vector_key(1.0, 0.6, 0.1), + vector_key(2.5, 0.9, 0.8), + ]; + let mut unsorted = sorted.clone(); + unsorted.swap(0, 3); + unsorted.swap(1, 3); + let vector_cases = [ + sorted.clone(), + unsorted, + vec![], + vec![vector_key(1.0, 0.3, 0.7)], + vec![ + vector_key(2.0, 0.9, 0.9), + vector_key(f64::NAN, 0.5, 0.5), + vector_key(0.0, 0.1, 0.1), + ], + vec![ + vector_key(0.0, 0.1, 0.1), + vector_key(f64::NAN, 0.5, 0.5), + vector_key(2.0, 0.9, 0.9), + ], + vec![ + vector_key(f64::NEG_INFINITY, 0.2, 0.2), + vector_key(f64::INFINITY, 0.8, 0.8), + ], + ]; + + let scalar_sorted = vec![ + scalar_key(0.5, 0.2), + scalar_key(1.0, 0.4), + scalar_key(1.0, 0.7), + scalar_key(2.0, 0.9), + ]; + let scalar_cases = [ + scalar_sorted.iter().rev().cloned().collect::>(), + scalar_sorted, + vec![], + vec![scalar_key(1.0, 0.6)], + vec![ + scalar_key(1.0, 0.6), + scalar_key(f64::NAN, 0.1), + scalar_key(0.0, 0.3), + ], + vec![ + scalar_key(0.0, 0.3), + scalar_key(f64::NAN, 0.1), + scalar_key(1.0, 0.6), + ], + ]; + + let base = XY::new(0.5, 0.5); + for time in QUERY_TIMES { + for keys in &vector_cases { + let expected = old_interpolate_vector(base, keys, time); + let actual = interpolate_vector(base, keys, time); + assert_eq!( + (actual.x.to_bits(), actual.y.to_bits()), + (expected.x.to_bits(), expected.y.to_bits()), + "time {time} keys {keys:?}" + ); + } + for keys in &scalar_cases { + assert_eq!( + interpolate_scalar(0.3, keys, time).to_bits(), + old_interpolate_scalar(0.3, keys, time).to_bits(), + "time {time} keys {keys:?}" + ); + } + } + } + + #[test] + fn only_out_of_order_or_non_finite_keyframes_are_cloned() { + let time = |key: &MaskVectorKeyframe| key.time; + let in_order = [ + vector_key(0.0, 0.0, 0.0), + vector_key(1.0, 1.0, 1.0), + vector_key(1.0, 0.5, 0.5), + vector_key(2.0, 0.2, 0.2), + ]; + assert!(matches!(sorted_by_time(&in_order, time), Cow::Borrowed(_))); + assert!(matches!( + sorted_by_time(&in_order[..1], time), + Cow::Borrowed(_) + )); + assert!(matches!(sorted_by_time(&[], time), Cow::Borrowed(_))); + + let out_of_order = [vector_key(1.0, 0.0, 0.0), vector_key(0.0, 1.0, 1.0)]; + assert!(matches!(sorted_by_time(&out_of_order, time), Cow::Owned(_))); + + let with_nan = [ + vector_key(0.0, 0.0, 0.0), + vector_key(f64::NAN, 0.5, 0.5), + vector_key(2.0, 1.0, 1.0), + ]; + assert!(matches!(sorted_by_time(&with_nan, time), Cow::Owned(_))); + } + #[test] fn mask_layers_follow_lane_order_with_stable_ties() { let mut front = sample_segment();