Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 205 additions & 11 deletions crates/rendering/src/mask.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::borrow::Cow;

use cap_project::{
MaskKind, MaskScalarKeyframe, MaskSegment, MaskVectorKeyframe, XY, mask_effect_contract,
};
Expand All @@ -6,17 +8,26 @@ use crate::{MaskRenderMode, PreparedMask};

const MASK_EFFECT_BASE_HEIGHT: f32 = 1080.0;

fn interpolate_vector(base: XY<f64>, keys: &[MaskVectorKeyframe], time: f64) -> XY<f64> {
if keys.is_empty() {
return base;
fn sorted_by_time<T: Clone>(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<f64>, keys: &[MaskVectorKeyframe], time: f64) -> XY<f64> {
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);
Expand All @@ -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;
Expand Down Expand Up @@ -200,6 +206,194 @@ mod tests {
}
}

fn old_interpolate_vector(base: XY<f64>, keys: &[MaskVectorKeyframe], time: f64) -> XY<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 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::<Vec<_>>(),
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();
Expand Down
Loading