diff options
Diffstat (limited to 'rust/crates/animation/src')
| -rw-r--r-- | rust/crates/animation/src/bezier.rs | 156 | ||||
| -rw-r--r-- | rust/crates/animation/src/channel_data.rs | 92 | ||||
| -rw-r--r-- | rust/crates/animation/src/channel_layout.rs | 124 | ||||
| -rw-r--r-- | rust/crates/animation/src/color.rs | 137 | ||||
| -rw-r--r-- | rust/crates/animation/src/id.rs | 61 | ||||
| -rw-r--r-- | rust/crates/animation/src/interpolation.rs | 467 | ||||
| -rw-r--r-- | rust/crates/animation/src/keyframes.rs | 1150 | ||||
| -rw-r--r-- | rust/crates/animation/src/lib.rs | 1 | ||||
| -rw-r--r-- | rust/crates/animation/src/types.rs | 45 |
9 files changed, 2233 insertions, 0 deletions
diff --git a/rust/crates/animation/src/bezier.rs b/rust/crates/animation/src/bezier.rs new file mode 100644 index 00000000..6bec252f --- /dev/null +++ b/rust/crates/animation/src/bezier.rs @@ -0,0 +1,156 @@ +use scene::{CurveHandle, ScalarAnimationKey}; +use time::MediaTime; + +pub const BEZIER_SOLVE_ITERATIONS: usize = 20; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct BezierHandle { + pub dt: f64, + pub dv: f64, +} + +impl From<CurveHandle> for BezierHandle { + fn from(handle: CurveHandle) -> Self { + Self { + dt: handle.dt.as_ticks() as f64, + dv: handle.dv, + } + } +} + +pub fn bezier_point(progress: f64, p0: f64, p1: f64, p2: f64, p3: f64) -> f64 { + let mt = 1.0 - progress; + mt * mt * mt * p0 + + 3.0 * mt * mt * progress * p1 + + 3.0 * mt * progress * progress * p2 + + progress * progress * progress * p3 +} + +pub fn default_right_handle( + left_key: &ScalarAnimationKey, + right_key: &ScalarAnimationKey, +) -> BezierHandle { + let span = (right_key.time - left_key.time).as_ticks() as f64; + let value_delta = right_key.value - left_key.value; + BezierHandle { + dt: span / 3.0, + dv: value_delta / 3.0, + } +} + +pub fn default_left_handle( + left_key: &ScalarAnimationKey, + right_key: &ScalarAnimationKey, +) -> BezierHandle { + let span = (right_key.time - left_key.time).as_ticks() as f64; + let value_delta = right_key.value - left_key.value; + BezierHandle { + dt: -span / 3.0, + dv: -value_delta / 3.0, + } +} + +pub fn segment_handles( + left_key: &ScalarAnimationKey, + right_key: &ScalarAnimationKey, +) -> (BezierHandle, BezierHandle) { + let right_handle = left_key + .right_handle + .map(BezierHandle::from) + .unwrap_or_else(|| default_right_handle(left_key, right_key)); + let left_handle = right_key + .left_handle + .map(BezierHandle::from) + .unwrap_or_else(|| default_left_handle(left_key, right_key)); + (right_handle, left_handle) +} + +pub fn solve_bezier_progress_for_time( + time: MediaTime, + left_key: &ScalarAnimationKey, + right_key: &ScalarAnimationKey, +) -> f64 { + let (right_handle, left_handle) = segment_handles(left_key, right_key); + let target = time.as_ticks() as f64; + let p0 = left_key.time.as_ticks() as f64; + let p3 = right_key.time.as_ticks() as f64; + let mut lower = 0.0; + let mut upper = 1.0; + + for _ in 0..BEZIER_SOLVE_ITERATIONS { + let mid = (lower + upper) / 2.0; + let estimate = bezier_point(mid, p0, p0 + right_handle.dt, p3 + left_handle.dt, p3); + if estimate < target { + lower = mid; + } else { + upper = mid; + } + } + + (lower + upper) / 2.0 +} + +#[cfg(test)] +mod tests { + use super::*; + use scene::{ScalarSegmentType, TangentMode}; + + fn key(time: i64, value: f64) -> ScalarAnimationKey { + ScalarAnimationKey { + id: format!("k{time}"), + time: MediaTime::from_ticks(time), + value, + left_handle: None, + right_handle: None, + segment_to_next: ScalarSegmentType::Bezier, + tangent_mode: TangentMode::Flat, + } + } + + #[test] + fn bezier_point_evaluates_cubic() { + assert_eq!(bezier_point(0.0, 0.0, 1.0, 2.0, 3.0), 0.0); + assert_eq!(bezier_point(1.0, 0.0, 1.0, 2.0, 3.0), 3.0); + assert_eq!(bezier_point(0.5, 0.0, 0.0, 1.0, 1.0), 0.5); + } + + #[test] + fn default_handles_split_span_into_thirds() { + let left = key(0, 10.0); + let right = key(90, 40.0); + let right_handle = default_right_handle(&left, &right); + let left_handle = default_left_handle(&left, &right); + assert_eq!(right_handle.dt, 30.0); + assert_eq!(right_handle.dv, 10.0); + assert_eq!(left_handle.dt, -30.0); + assert_eq!(left_handle.dv, -10.0); + } + + #[test] + fn solve_progress_is_linear_for_diagonal_control_points() { + let left = key(0, 0.0); + let right = key(100, 100.0); + let progress = solve_bezier_progress_for_time(MediaTime::from_ticks(50), &left, &right); + assert!((progress - 0.5).abs() < 1e-6); + let progress = solve_bezier_progress_for_time(MediaTime::from_ticks(25), &left, &right); + assert!((progress - 0.25).abs() < 1e-6); + } + + #[test] + fn solve_progress_respects_flat_tangents() { + let mut left = key(0, 0.0); + left.right_handle = Some(CurveHandle { + dt: MediaTime::from_ticks(0), + dv: 0.0, + }); + let mut right = key(100, 100.0); + right.left_handle = Some(CurveHandle { + dt: MediaTime::from_ticks(0), + dv: 0.0, + }); + let early = solve_bezier_progress_for_time(MediaTime::from_ticks(25), &left, &right); + let late = solve_bezier_progress_for_time(MediaTime::from_ticks(75), &left, &right); + assert!(early < 0.25); + assert!(late > 0.75); + } +} diff --git a/rust/crates/animation/src/channel_data.rs b/rust/crates/animation/src/channel_data.rs new file mode 100644 index 00000000..a05396e5 --- /dev/null +++ b/rust/crates/animation/src/channel_data.rs @@ -0,0 +1,92 @@ +use scene::{AnimationChannel, ChannelData, CompositeChannelData}; + +const LEGACY_ANIMATION_STORAGE_KEYS: [&str; 2] = ["bindings", "channels"]; + +pub fn is_animation_storage_key(key: &str) -> bool { + !LEGACY_ANIMATION_STORAGE_KEYS.contains(&key) +} + +fn component_order(key: &str) -> usize { + match key { + "value" => 0, + "r" => 1, + "g" => 2, + "b" => 3, + "a" => 4, + _ => 5, + } +} + +pub fn ordered_composite_entries(data: &CompositeChannelData) -> Vec<(&String, &AnimationChannel)> { + let mut entries: Vec<(&String, &AnimationChannel)> = data.iter().collect(); + entries.sort_by_key(|(key, _)| component_order(key)); + entries +} + +pub fn get_channels_from_data(data: Option<&ChannelData>) -> Vec<&AnimationChannel> { + match data { + Some(ChannelData::Single(channel)) => vec![channel], + Some(ChannelData::Composite(components)) => ordered_composite_entries(components) + .into_iter() + .map(|(_, channel)| channel) + .collect(), + None => Vec::new(), + } +} + +pub fn get_channel_entries_from_data(data: Option<&ChannelData>) -> Vec<(String, &AnimationChannel)> { + match data { + Some(ChannelData::Single(channel)) => vec![("value".to_string(), channel)], + Some(ChannelData::Composite(components)) => ordered_composite_entries(components) + .into_iter() + .map(|(key, channel)| (key.clone(), channel)) + .collect(), + None => Vec::new(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use scene::{AnimationChannel, DiscreteChannel, ScalarChannel}; + use std::collections::BTreeMap; + + fn scalar_channel() -> AnimationChannel { + AnimationChannel::Scalar(ScalarChannel { + keys: Vec::new(), + extrapolation: None, + }) + } + + #[test] + fn leaf_data_yields_value_entry() { + let data = ChannelData::Single(scalar_channel()); + let entries = get_channel_entries_from_data(Some(&data)); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].0, "value"); + } + + #[test] + fn composite_entries_follow_canonical_component_order() { + let mut components = BTreeMap::new(); + components.insert("a".to_string(), scalar_channel()); + components.insert("b".to_string(), scalar_channel()); + components.insert("g".to_string(), scalar_channel()); + components.insert("r".to_string(), scalar_channel()); + components.insert( + "mode".to_string(), + AnimationChannel::Discrete(DiscreteChannel { keys: Vec::new() }), + ); + let data = ChannelData::Composite(components); + let entries = get_channel_entries_from_data(Some(&data)); + let keys: Vec<&str> = entries.iter().map(|(key, _)| key.as_str()).collect(); + assert_eq!(keys, ["r", "g", "b", "a", "mode"]); + } + + #[test] + fn legacy_storage_keys_are_rejected() { + assert!(!is_animation_storage_key("bindings")); + assert!(!is_animation_storage_key("channels")); + assert!(is_animation_storage_key("opacity")); + } +} diff --git a/rust/crates/animation/src/channel_layout.rs b/rust/crates/animation/src/channel_layout.rs new file mode 100644 index 00000000..59bbc3f6 --- /dev/null +++ b/rust/crates/animation/src/channel_layout.rs @@ -0,0 +1,124 @@ +use scene::ParamValue; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +use crate::color::{LinearRgba, parse_color_to_linear_rgba}; +use crate::types::AnimationInterpolation; + +#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum ChannelValueKind { + Scalar, + Discrete, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ChannelComponentDefinition { + pub key: String, + pub value_kind: ChannelValueKind, + pub default_interpolation: AnimationInterpolation, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum ChannelLayout { + Leaf(ChannelComponentDefinition), + Composite(Vec<ChannelComponentDefinition>), +} + +impl ChannelLayout { + pub fn components(&self) -> &[ChannelComponentDefinition] { + match self { + ChannelLayout::Leaf(component) => std::slice::from_ref(component), + ChannelLayout::Composite(components) => components, + } + } + + pub fn decompose(&self, value: &ParamValue) -> Option<BTreeMap<String, ParamValue>> { + match self { + ChannelLayout::Leaf(component) => { + let mut values = BTreeMap::new(); + values.insert(component.key.clone(), value.clone()); + Some(values) + } + ChannelLayout::Composite(_) => { + let ParamValue::String(color) = value else { + return None; + }; + let LinearRgba { r, g, b, a } = parse_color_to_linear_rgba(color)?; + let mut values = BTreeMap::new(); + values.insert("r".to_string(), ParamValue::Number(r)); + values.insert("g".to_string(), ParamValue::Number(g)); + values.insert("b".to_string(), ParamValue::Number(b)); + values.insert("a".to_string(), ParamValue::Number(a)); + Some(values) + } + } + } +} + +pub fn number_channel_layout() -> ChannelLayout { + ChannelLayout::Leaf(ChannelComponentDefinition { + key: "value".to_string(), + value_kind: ChannelValueKind::Scalar, + default_interpolation: AnimationInterpolation::Linear, + }) +} + +pub fn boolean_channel_layout() -> ChannelLayout { + ChannelLayout::Leaf(ChannelComponentDefinition { + key: "value".to_string(), + value_kind: ChannelValueKind::Discrete, + default_interpolation: AnimationInterpolation::Hold, + }) +} + +pub fn string_channel_layout() -> ChannelLayout { + ChannelLayout::Leaf(ChannelComponentDefinition { + key: "value".to_string(), + value_kind: ChannelValueKind::Discrete, + default_interpolation: AnimationInterpolation::Hold, + }) +} + +pub fn color_channel_layout() -> ChannelLayout { + let component = |key: &str| ChannelComponentDefinition { + key: key.to_string(), + value_kind: ChannelValueKind::Scalar, + default_interpolation: AnimationInterpolation::Linear, + }; + ChannelLayout::Composite(vec![ + component("r"), + component("g"), + component("b"), + component("a"), + ]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn leaf_decompose_wraps_value_under_component_key() { + let layout = number_channel_layout(); + let values = layout.decompose(&ParamValue::Number(3.0)).unwrap(); + assert_eq!(values.get("value"), Some(&ParamValue::Number(3.0))); + } + + #[test] + fn color_decompose_parses_hex_into_linear_components() { + let layout = color_channel_layout(); + let values = layout + .decompose(&ParamValue::String("#ff0000".to_string())) + .unwrap(); + assert_eq!(values.get("r"), Some(&ParamValue::Number(1.0))); + assert_eq!(values.get("g"), Some(&ParamValue::Number(0.0))); + assert_eq!(values.get("a"), Some(&ParamValue::Number(1.0))); + assert!( + layout + .decompose(&ParamValue::Number(1.0)) + .is_none() + ); + } +} diff --git a/rust/crates/animation/src/color.rs b/rust/crates/animation/src/color.rs new file mode 100644 index 00000000..03f409a1 --- /dev/null +++ b/rust/crates/animation/src/color.rs @@ -0,0 +1,137 @@ +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct LinearRgba { + pub r: f64, + pub g: f64, + pub b: f64, + pub a: f64, +} + +fn clamp_unit(value: f64) -> f64 { + value.max(0.0).min(1.0) +} + +fn srgb_to_linear(value: f64) -> f64 { + if value <= 0.04045 { + value / 12.92 + } else { + ((value + 0.055) / 1.055).powf(2.4) + } +} + +fn linear_to_srgb(value: f64) -> f64 { + let clamped = clamp_unit(value); + if clamped <= 0.003_130_8 { + clamped * 12.92 + } else { + 1.055 * clamped.powf(1.0 / 2.4) - 0.055 + } +} + +fn parse_hex_digit(digit: char) -> Option<f64> { + digit.to_digit(16).map(|value| value as f64 / 15.0) +} + +fn parse_hex_pair(pair: &str) -> Option<f64> { + u8::from_str_radix(pair, 16) + .ok() + .map(|value| value as f64 / 255.0) +} + +pub fn parse_color_to_linear_rgba(color: &str) -> Option<LinearRgba> { + let hex = color.strip_prefix('#')?; + let (r, g, b, a) = match hex.len() { + 3 | 4 => { + let mut digits = hex.chars(); + let r = parse_hex_digit(digits.next()?)?; + let g = parse_hex_digit(digits.next()?)?; + let b = parse_hex_digit(digits.next()?)?; + let a = digits.next().map(parse_hex_digit).transpose()?.unwrap_or(1.0); + (r, g, b, a) + } + 6 | 8 => { + let r = parse_hex_pair(&hex[0..2])?; + let g = parse_hex_pair(&hex[2..4])?; + let b = parse_hex_pair(&hex[4..6])?; + let a = if hex.len() == 8 { + parse_hex_pair(&hex[6..8])? + } else { + 1.0 + }; + (r, g, b, a) + } + _ => return None, + }; + Some(LinearRgba { + r: srgb_to_linear(r), + g: srgb_to_linear(g), + b: srgb_to_linear(b), + a: clamp_unit(a), + }) +} + +fn hex_component(value: f64) -> u8 { + (clamp_unit(value) * 255.0).round() as u8 +} + +pub fn format_linear_rgba(color: &LinearRgba) -> String { + let r = hex_component(linear_to_srgb(color.r)); + let g = hex_component(linear_to_srgb(color.g)); + let b = hex_component(linear_to_srgb(color.b)); + let a = clamp_unit(color.a); + if a < 1.0 { + format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, hex_component(a)) + } else { + format!("#{:02x}{:02x}{:02x}", r, g, b) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_hex_colors_to_linear() { + let red = parse_color_to_linear_rgba("#ff0000").unwrap(); + assert_eq!(red.r, 1.0); + assert_eq!(red.g, 0.0); + assert_eq!(red.b, 0.0); + assert_eq!(red.a, 1.0); + + let short = parse_color_to_linear_rgba("#fff").unwrap(); + assert_eq!(short.r, 1.0); + assert_eq!(short.g, 1.0); + + let with_alpha = parse_color_to_linear_rgba("#00000080").unwrap(); + assert!((with_alpha.a - 128.0 / 255.0).abs() < 1e-9); + + assert!(parse_color_to_linear_rgba("red").is_none()); + assert!(parse_color_to_linear_rgba("#ff").is_none()); + } + + #[test] + fn formats_linear_colors_as_hex() { + let red = LinearRgba { + r: 1.0, + g: 0.0, + b: 0.0, + a: 1.0, + }; + assert_eq!(format_linear_rgba(&red), "#ff0000"); + + let translucent = LinearRgba { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.5, + }; + assert_eq!(format_linear_rgba(&translucent), "#00000080"); + } + + #[test] + fn hex_round_trips_through_linear_space() { + for color in ["#ffffff", "#000000", "#123456", "#abcdef"] { + let parsed = parse_color_to_linear_rgba(color).unwrap(); + assert_eq!(format_linear_rgba(&parsed), color); + } + } +} diff --git a/rust/crates/animation/src/id.rs b/rust/crates/animation/src/id.rs new file mode 100644 index 00000000..95288660 --- /dev/null +++ b/rust/crates/animation/src/id.rs @@ -0,0 +1,61 @@ +use std::cell::Cell; +use std::time::{SystemTime, UNIX_EPOCH}; + +thread_local! { + static RNG_STATE: Cell<u64> = const { Cell::new(0) }; +} + +fn next_u64() -> u64 { + RNG_STATE.with(|state| { + let mut value = state.get(); + if value == 0 { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_nanos() as u64) + .unwrap_or(0x9e37_79b9_7f4a_7c15); + value = nanos + ^ (std::process::id() as u64).rotate_left(32) + ^ 0x9e37_79b9_7f4a_7c15; + } + value ^= value << 13; + value ^= value >> 7; + value ^= value << 17; + state.set(value); + value + }) +} + +pub fn generate_uuid() -> String { + let mut bytes = [0u8; 16]; + for chunk in bytes.chunks_mut(8) { + let random = next_u64().to_le_bytes(); + chunk.copy_from_slice(&random[..chunk.len()]); + } + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + let hex: Vec<String> = bytes.iter().map(|byte| format!("{byte:02x}")).collect(); + format!( + "{}-{}-{}-{}-{}", + hex[0..4].join(""), + hex[4..6].join(""), + hex[6..8].join(""), + hex[8..10].join(""), + hex[10..16].join("") + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generates_uuid_shaped_unique_ids() { + let id = generate_uuid(); + let parts: Vec<&str> = id.split('-').collect(); + assert_eq!(parts.len(), 5); + assert_eq!(parts[0].len(), 8); + assert_eq!(parts[1].len(), 4); + assert_eq!(parts[4].len(), 12); + assert_ne!(generate_uuid(), generate_uuid()); + } +} diff --git a/rust/crates/animation/src/interpolation.rs b/rust/crates/animation/src/interpolation.rs new file mode 100644 index 00000000..85c0735d --- /dev/null +++ b/rust/crates/animation/src/interpolation.rs @@ -0,0 +1,467 @@ +use scene::{ + AnimationChannel, ChannelExtrapolationMode, CurveHandle, DiscreteChannel, DiscreteValue, + ParamValue, ScalarAnimationKey, ScalarChannel, ScalarSegmentType, +}; +use time::MediaTime; + +use crate::bezier::{bezier_point, segment_handles, solve_bezier_progress_for_time}; +use crate::types::AnimationInterpolation; + +pub fn is_scalar_channel(channel: &AnimationChannel) -> bool { + matches!(channel, AnimationChannel::Scalar(_)) +} + +fn normalize_right_handle( + handle: Option<CurveHandle>, + left_key: &ScalarAnimationKey, + right_key: &ScalarAnimationKey, +) -> Option<CurveHandle> { + let handle = handle?; + let span = (right_key.time - left_key.time).as_ticks().max(1); + Some(CurveHandle { + dt: MediaTime::from_ticks(handle.dt.as_ticks().clamp(0, span)), + dv: handle.dv, + }) +} + +fn normalize_left_handle( + handle: Option<CurveHandle>, + left_key: &ScalarAnimationKey, + right_key: &ScalarAnimationKey, +) -> Option<CurveHandle> { + let handle = handle?; + let span = (right_key.time - left_key.time).as_ticks().max(1); + Some(CurveHandle { + dt: MediaTime::from_ticks(handle.dt.as_ticks().clamp(-span, 0)), + dv: handle.dv, + }) +} + +pub fn normalize_scalar_channel(channel: &ScalarChannel) -> ScalarChannel { + let mut sorted_keys = channel.keys.clone(); + sorted_keys.sort_by_key(|key| key.time); + + let mut next_keys = Vec::with_capacity(sorted_keys.len()); + for (index, key) in sorted_keys.iter().enumerate() { + let previous_key = index.checked_sub(1).and_then(|i| sorted_keys.get(i)); + let next_key = sorted_keys.get(index + 1); + let left_handle = + previous_key.and_then(|previous| normalize_left_handle(key.left_handle, previous, key)); + let right_handle = + next_key.and_then(|next| normalize_right_handle(key.right_handle, key, next)); + next_keys.push(ScalarAnimationKey { + left_handle, + right_handle, + ..key.clone() + }); + } + + ScalarChannel { + keys: next_keys, + extrapolation: channel.extrapolation.clone(), + } +} + +pub fn normalize_discrete_channel(channel: &DiscreteChannel) -> DiscreteChannel { + let mut keys = channel.keys.clone(); + keys.sort_by_key(|key| key.time); + DiscreteChannel { keys } +} + +pub fn normalize_channel(channel: &AnimationChannel) -> AnimationChannel { + match channel { + AnimationChannel::Scalar(scalar) => { + AnimationChannel::Scalar(normalize_scalar_channel(scalar)) + } + AnimationChannel::Discrete(discrete) => { + AnimationChannel::Discrete(normalize_discrete_channel(discrete)) + } + } +} + +pub fn scalar_segment_interpolation(segment: ScalarSegmentType) -> AnimationInterpolation { + match segment { + ScalarSegmentType::Step => AnimationInterpolation::Hold, + ScalarSegmentType::Linear => AnimationInterpolation::Linear, + ScalarSegmentType::Bezier => AnimationInterpolation::Bezier, + } +} + +fn extrapolate_scalar_edge( + mode: ChannelExtrapolationMode, + edge_key: &ScalarAnimationKey, + neighbor_key: Option<&ScalarAnimationKey>, + time: f64, +) -> f64 { + let Some(neighbor_key) = neighbor_key else { + return edge_key.value; + }; + if mode == ChannelExtrapolationMode::Hold { + return edge_key.value; + } + + let span = (neighbor_key.time - edge_key.time).as_ticks() as f64; + if span == 0.0 { + return edge_key.value; + } + + edge_key.value + + ((time - edge_key.time.as_ticks() as f64) / span) * (neighbor_key.value - edge_key.value) +} + +fn lerp_number(left_value: f64, right_value: f64, progress: f64) -> f64 { + left_value + (right_value - left_value) * progress +} + +pub fn scalar_channel_value_at_time( + channel: Option<&ScalarChannel>, + time: MediaTime, + fallback_value: f64, +) -> f64 { + let Some(channel) = channel.filter(|channel| !channel.keys.is_empty()) else { + return fallback_value; + }; + + let normalized = normalize_scalar_channel(channel); + let keys = &normalized.keys; + let first_key = &keys[0]; + let last_key = &keys[keys.len() - 1]; + let t = time.as_ticks() as f64; + + if time <= first_key.time { + if time < first_key.time { + return extrapolate_scalar_edge( + normalized + .extrapolation + .as_ref() + .map(|e| e.before) + .unwrap_or(ChannelExtrapolationMode::Hold), + first_key, + keys.get(1), + t, + ); + } + return first_key.value; + } + + if time >= last_key.time { + if time > last_key.time { + return extrapolate_scalar_edge( + normalized + .extrapolation + .as_ref() + .map(|e| e.after) + .unwrap_or(ChannelExtrapolationMode::Hold), + last_key, + keys.len().checked_sub(2).and_then(|index| keys.get(index)), + t, + ); + } + return last_key.value; + } + + for key_index in 0..keys.len() - 1 { + let left_key = &keys[key_index]; + let right_key = &keys[key_index + 1]; + if time == right_key.time { + return right_key.value; + } + if time < left_key.time || time > right_key.time { + continue; + } + + if left_key.segment_to_next == ScalarSegmentType::Step { + return left_key.value; + } + + let span = (right_key.time - left_key.time).as_ticks() as f64; + if span == 0.0 { + return right_key.value; + } + + let progress = ((t - left_key.time.as_ticks() as f64) / span).clamp(0.0, 1.0); + if left_key.segment_to_next == ScalarSegmentType::Linear { + return lerp_number(left_key.value, right_key.value, progress); + } + + let curve_progress = solve_bezier_progress_for_time(time, left_key, right_key); + let (right_handle, left_handle) = segment_handles(left_key, right_key); + return bezier_point( + curve_progress, + left_key.value, + left_key.value + right_handle.dv, + right_key.value + left_handle.dv, + right_key.value, + ); + } + + last_key.value +} + +pub fn discrete_channel_value_at_time( + channel: Option<&DiscreteChannel>, + time: MediaTime, + fallback_value: DiscreteValue, +) -> DiscreteValue { + let Some(channel) = channel.filter(|channel| !channel.keys.is_empty()) else { + return fallback_value; + }; + + let normalized = normalize_discrete_channel(channel); + let mut current_value = fallback_value; + for key in &normalized.keys { + if time < key.time { + break; + } + current_value = key.value.clone(); + } + current_value +} + +pub fn channel_value_at_time( + channel: Option<&AnimationChannel>, + time: MediaTime, + fallback_value: &ParamValue, +) -> ParamValue { + let Some(channel) = channel.filter(|channel| !channel.keys.is_empty()) else { + return fallback_value.clone(); + }; + + if let ParamValue::Number(number_fallback) = fallback_value { + return match channel { + AnimationChannel::Scalar(scalar) => ParamValue::Number(scalar_channel_value_at_time( + Some(scalar), + time, + *number_fallback, + )), + AnimationChannel::Discrete(_) => fallback_value.clone(), + }; + } + + match channel { + AnimationChannel::Discrete(discrete) => { + let discrete_fallback = match fallback_value { + ParamValue::Bool(value) => DiscreteValue::Bool(*value), + ParamValue::String(value) => DiscreteValue::String(value.clone()), + ParamValue::Number(_) => return fallback_value.clone(), + }; + discrete_channel_value_at_time(Some(discrete), time, discrete_fallback).into() + } + AnimationChannel::Scalar(_) => fallback_value.clone(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use scene::{ChannelExtrapolation, TangentMode}; + + fn scalar_key(id: &str, time: i64, value: f64, segment: ScalarSegmentType) -> ScalarAnimationKey { + ScalarAnimationKey { + id: id.to_string(), + time: MediaTime::from_ticks(time), + value, + left_handle: None, + right_handle: None, + segment_to_next: segment, + tangent_mode: TangentMode::Flat, + } + } + + fn channel(keys: Vec<ScalarAnimationKey>) -> ScalarChannel { + ScalarChannel { + keys, + extrapolation: None, + } + } + + fn at(time: i64) -> MediaTime { + MediaTime::from_ticks(time) + } + + #[test] + fn empty_channel_returns_fallback() { + assert_eq!(scalar_channel_value_at_time(None, at(10), 7.0), 7.0); + assert_eq!( + scalar_channel_value_at_time(Some(&channel(Vec::new())), at(10), 7.0), + 7.0 + ); + } + + #[test] + fn linear_segment_interpolates() { + let channel = channel(vec![ + scalar_key("a", 0, 0.0, ScalarSegmentType::Linear), + scalar_key("b", 100, 10.0, ScalarSegmentType::Linear), + ]); + assert_eq!(scalar_channel_value_at_time(Some(&channel), at(50), 0.0), 5.0); + assert_eq!(scalar_channel_value_at_time(Some(&channel), at(0), 0.0), 0.0); + assert_eq!(scalar_channel_value_at_time(Some(&channel), at(100), 0.0), 10.0); + } + + #[test] + fn step_segment_holds_left_value() { + let channel = channel(vec![ + scalar_key("a", 0, 1.0, ScalarSegmentType::Step), + scalar_key("b", 100, 9.0, ScalarSegmentType::Linear), + ]); + assert_eq!(scalar_channel_value_at_time(Some(&channel), at(99), 0.0), 1.0); + assert_eq!(scalar_channel_value_at_time(Some(&channel), at(100), 0.0), 9.0); + } + + #[test] + fn unsorted_keys_are_sorted_before_sampling() { + let channel = channel(vec![ + scalar_key("b", 100, 10.0, ScalarSegmentType::Linear), + scalar_key("a", 0, 0.0, ScalarSegmentType::Linear), + ]); + assert_eq!(scalar_channel_value_at_time(Some(&channel), at(50), 0.0), 5.0); + } + + #[test] + fn hold_extrapolation_is_the_default() { + let channel = channel(vec![ + scalar_key("a", 10, 1.0, ScalarSegmentType::Linear), + scalar_key("b", 100, 10.0, ScalarSegmentType::Linear), + ]); + assert_eq!(scalar_channel_value_at_time(Some(&channel), at(0), 0.0), 1.0); + assert_eq!(scalar_channel_value_at_time(Some(&channel), at(200), 0.0), 10.0); + } + + #[test] + fn linear_extrapolation_extends_edge_segments() { + let mut channel = channel(vec![ + scalar_key("a", 0, 0.0, ScalarSegmentType::Linear), + scalar_key("b", 100, 10.0, ScalarSegmentType::Linear), + ]); + channel.extrapolation = Some(ChannelExtrapolation { + before: ChannelExtrapolationMode::Linear, + after: ChannelExtrapolationMode::Linear, + }); + assert_eq!(scalar_channel_value_at_time(Some(&channel), at(-50), 0.0), -5.0); + assert_eq!(scalar_channel_value_at_time(Some(&channel), at(150), 0.0), 15.0); + } + + #[test] + fn linear_extrapolation_holds_with_single_key() { + let mut channel = channel(vec![scalar_key("a", 10, 3.0, ScalarSegmentType::Linear)]); + channel.extrapolation = Some(ChannelExtrapolation { + before: ChannelExtrapolationMode::Linear, + after: ChannelExtrapolationMode::Linear, + }); + assert_eq!(scalar_channel_value_at_time(Some(&channel), at(0), 0.0), 3.0); + assert_eq!(scalar_channel_value_at_time(Some(&channel), at(50), 0.0), 3.0); + } + + #[test] + fn bezier_segment_with_default_handles_matches_diagonal() { + let channel = channel(vec![ + scalar_key("a", 0, 0.0, ScalarSegmentType::Bezier), + scalar_key("b", 100, 100.0, ScalarSegmentType::Bezier), + ]); + let value = scalar_channel_value_at_time(Some(&channel), at(50), 0.0); + assert!((value - 50.0).abs() < 1e-6); + } + + #[test] + fn bezier_segment_with_flat_handles_eases() { + let mut left = scalar_key("a", 0, 0.0, ScalarSegmentType::Bezier); + left.right_handle = Some(CurveHandle { + dt: at(50), + dv: 0.0, + }); + let mut right = scalar_key("b", 100, 100.0, ScalarSegmentType::Bezier); + right.left_handle = Some(CurveHandle { + dt: at(-50), + dv: 0.0, + }); + let channel = channel(vec![left, right]); + let quarter = scalar_channel_value_at_time(Some(&channel), at(25), 0.0); + let mid = scalar_channel_value_at_time(Some(&channel), at(50), 0.0); + assert!(quarter < 25.0); + assert!((mid - 50.0).abs() < 1e-6); + } + + #[test] + fn handles_are_clamped_to_segment_span() { + let mut left = scalar_key("a", 0, 0.0, ScalarSegmentType::Bezier); + left.right_handle = Some(CurveHandle { + dt: at(500), + dv: 1.0, + }); + let mut right = scalar_key("b", 100, 100.0, ScalarSegmentType::Bezier); + right.left_handle = Some(CurveHandle { + dt: at(50), + dv: 1.0, + }); + let normalized = normalize_scalar_channel(&channel(vec![left, right])); + assert_eq!(normalized.keys[0].right_handle.unwrap().dt, at(100)); + assert_eq!(normalized.keys[1].left_handle.unwrap().dt, at(0)); + } + + #[test] + fn edge_keys_drop_outer_handles() { + let mut left = scalar_key("a", 0, 0.0, ScalarSegmentType::Bezier); + left.left_handle = Some(CurveHandle { + dt: at(-10), + dv: 1.0, + }); + let mut right = scalar_key("b", 100, 100.0, ScalarSegmentType::Bezier); + right.right_handle = Some(CurveHandle { + dt: at(10), + dv: 1.0, + }); + let normalized = normalize_scalar_channel(&channel(vec![left, right])); + assert_eq!(normalized.keys[0].left_handle, None); + assert_eq!(normalized.keys[1].right_handle, None); + } + + #[test] + fn discrete_channel_returns_last_key_at_or_before_time() { + let channel = DiscreteChannel { + keys: vec![ + DiscreteAnimationKey { + id: "a".to_string(), + time: at(0), + value: DiscreteValue::String("x".to_string()), + }, + DiscreteAnimationKey { + id: "b".to_string(), + time: at(100), + value: DiscreteValue::Bool(true), + }, + ], + }; + let fallback = DiscreteValue::Bool(false); + assert_eq!( + discrete_channel_value_at_time(Some(&channel), at(-1), fallback.clone()), + fallback + ); + assert_eq!( + discrete_channel_value_at_time(Some(&channel), at(50), fallback.clone()), + DiscreteValue::String("x".to_string()) + ); + assert_eq!( + discrete_channel_value_at_time(Some(&channel), at(100), fallback.clone()), + DiscreteValue::Bool(true) + ); + } + + #[test] + fn channel_value_at_time_dispatches_on_fallback_type() { + let scalar = AnimationChannel::Scalar(channel(vec![scalar_key( + "a", + 0, + 2.0, + ScalarSegmentType::Linear, + )])); + assert_eq!( + channel_value_at_time(Some(&scalar), at(0), &ParamValue::Number(0.0)), + ParamValue::Number(2.0) + ); + assert_eq!( + channel_value_at_time(Some(&scalar), at(0), &ParamValue::Bool(true)), + ParamValue::Bool(true) + ); + } +} diff --git a/rust/crates/animation/src/keyframes.rs b/rust/crates/animation/src/keyframes.rs new file mode 100644 index 00000000..0ce938f9 --- /dev/null +++ b/rust/crates/animation/src/keyframes.rs @@ -0,0 +1,1150 @@ +use std::collections::{BTreeMap, HashMap}; + +use scene::{ + AnimationChannel, ChannelData, DiscreteAnimationKey, DiscreteChannel, DiscreteValue, + ElementAnimations, ParamValue, ScalarAnimationKey, ScalarChannel, ScalarSegmentType, + TangentMode, +}; +use time::MediaTime; + +use crate::bezier::{segment_handles, solve_bezier_progress_for_time}; +use crate::channel_data::{ + get_channel_entries_from_data, get_channels_from_data, is_animation_storage_key, +}; +use crate::channel_layout::{ChannelLayout, ChannelValueKind}; +use crate::id::generate_uuid; +use crate::interpolation::{ + channel_value_at_time, is_scalar_channel, normalize_channel, normalize_discrete_channel, + normalize_scalar_channel, scalar_segment_interpolation, +}; +use crate::types::{AnimationInterpolation, ScalarCurveKeyframePatch}; + +fn round_media_time(time: f64) -> MediaTime { + MediaTime::from_ticks(time.round() as i64) +} + +fn has_channel_keys(channel: Option<&AnimationChannel>) -> bool { + channel.is_some_and(|channel| !channel.keys().is_empty()) +} + +trait ChannelKeys { + fn keys(&self) -> usize; +} + +impl ChannelKeys for AnimationChannel { + fn keys(&self) -> usize { + match self { + AnimationChannel::Scalar(scalar) => scalar.keys.len(), + AnimationChannel::Discrete(discrete) => discrete.keys.len(), + } + } +} + +fn has_channel_data(data: Option<&ChannelData>) -> bool { + get_channels_from_data(data) + .iter() + .any(|channel| has_channel_keys(Some(channel))) +} + +pub fn to_animation(animations: ElementAnimations) -> Option<ElementAnimations> { + let next: ElementAnimations = animations + .into_iter() + .filter(|(key, data)| is_animation_storage_key(key) && has_channel_data(Some(data))) + .collect(); + if next.is_empty() { None } else { Some(next) } +} + +fn get_channel_from_data<'a>( + data: Option<&'a ChannelData>, + component_key: &str, +) -> Option<&'a AnimationChannel> { + match data { + Some(ChannelData::Single(channel)) => { + if component_key == "value" { Some(channel) } else { None } + } + Some(ChannelData::Composite(components)) => components.get(component_key), + None => None, + } +} + +fn get_primary_component_key(channel_layout: &ChannelLayout) -> String { + channel_layout + .components() + .first() + .map(|component| component.key.clone()) + .unwrap_or_else(|| "value".to_string()) +} + +fn get_primary_channel_from_data<'a>( + data: Option<&'a ChannelData>, + channel_layout: &ChannelLayout, +) -> Option<&'a AnimationChannel> { + get_channel_from_data(data, &get_primary_component_key(channel_layout)) +} + +fn set_channel_in_data( + data: Option<ChannelData>, + component_key: &str, + channel: Option<AnimationChannel>, +) -> Option<ChannelData> { + if component_key == "value" { + return channel.map(ChannelData::Single); + } + + let mut components = match data { + Some(ChannelData::Composite(components)) => components, + _ => BTreeMap::new(), + }; + match channel { + Some(channel) if has_channel_keys(Some(&channel)) => { + components.insert(component_key.to_string(), channel); + } + _ => { + components.remove(component_key); + } + } + if components.is_empty() { + None + } else { + Some(ChannelData::Composite(components)) + } +} + +fn scalar_segment_type(interpolation: AnimationInterpolation) -> ScalarSegmentType { + match interpolation { + AnimationInterpolation::Hold => ScalarSegmentType::Step, + AnimationInterpolation::Bezier => ScalarSegmentType::Bezier, + AnimationInterpolation::Linear => ScalarSegmentType::Linear, + } +} + +fn create_scalar_key( + id: String, + time: MediaTime, + value: f64, + interpolation: Option<AnimationInterpolation>, + previous_key: Option<&ScalarAnimationKey>, +) -> ScalarAnimationKey { + ScalarAnimationKey { + id, + time, + value, + left_handle: previous_key.and_then(|key| key.left_handle), + right_handle: previous_key.and_then(|key| key.right_handle), + segment_to_next: previous_key.map(|key| key.segment_to_next).unwrap_or_else(|| { + scalar_segment_type(interpolation.unwrap_or(AnimationInterpolation::Linear)) + }), + tangent_mode: previous_key + .map(|key| key.tangent_mode) + .unwrap_or(TangentMode::Flat), + } +} + +fn channel_key_ids_and_times(channel: &AnimationChannel) -> Vec<(String, MediaTime)> { + match channel { + AnimationChannel::Scalar(scalar) => scalar + .keys + .iter() + .map(|key| (key.id.clone(), key.time)) + .collect(), + AnimationChannel::Discrete(discrete) => discrete + .keys + .iter() + .map(|key| (key.id.clone(), key.time)) + .collect(), + } +} + +fn get_target_key_metadata( + channel: Option<&AnimationChannel>, + time: MediaTime, + keyframe_id: Option<&str>, +) -> (String, MediaTime) { + let normalized = channel.map(normalize_channel); + let keys = normalized + .as_ref() + .map(channel_key_ids_and_times) + .unwrap_or_default(); + + if let Some(id) = keyframe_id { + if let Some((found_id, _)) = keys.iter().find(|(key_id, _)| key_id == id) { + return (found_id.clone(), time); + } + } + + if let Some((found_id, found_time)) = keys.iter().find(|(_, key_time)| *key_time == time) { + return (found_id.clone(), *found_time); + } + + ( + keyframe_id.map(str::to_string).unwrap_or_else(generate_uuid), + time, + ) +} + +pub fn upsert_discrete_channel_key( + channel: Option<&DiscreteChannel>, + time: MediaTime, + value: DiscreteValue, + keyframe_id: Option<&str>, +) -> DiscreteChannel { + let normalized = normalize_discrete_channel(&channel.cloned().unwrap_or(DiscreteChannel { + keys: Vec::new(), + })); + let mut keys = normalized.keys; + + if let Some(id) = keyframe_id { + if let Some(index) = keys.iter().position(|key| key.id == id) { + keys[index] = DiscreteAnimationKey { + id: keys[index].id.clone(), + time, + value, + }; + return normalize_discrete_channel(&DiscreteChannel { keys }); + } + } + + if let Some(index) = keys.iter().position(|key| key.time == time) { + keys[index] = DiscreteAnimationKey { + id: keys[index].id.clone(), + time: keys[index].time, + value, + }; + return normalize_discrete_channel(&DiscreteChannel { keys }); + } + + keys.push(DiscreteAnimationKey { + id: keyframe_id.map(str::to_string).unwrap_or_else(generate_uuid), + time, + value, + }); + normalize_discrete_channel(&DiscreteChannel { keys }) +} + +fn replace_scalar_key_at( + keys: &mut [ScalarAnimationKey], + index: usize, + time: MediaTime, + value: f64, + interpolation: Option<AnimationInterpolation>, + keep_existing_time: bool, +) { + let mut previous_key = keys[index].clone(); + if let Some(interpolation) = interpolation { + previous_key.segment_to_next = scalar_segment_type(interpolation); + } + let key_time = if keep_existing_time { + previous_key.time + } else { + time + }; + keys[index] = create_scalar_key( + previous_key.id.clone(), + key_time, + value, + interpolation, + Some(&previous_key), + ); +} + +pub fn upsert_scalar_channel_key( + channel: Option<&ScalarChannel>, + time: MediaTime, + value: f64, + interpolation: Option<AnimationInterpolation>, + default_interpolation: Option<AnimationInterpolation>, + keyframe_id: Option<&str>, +) -> ScalarChannel { + let normalized = normalize_scalar_channel(&channel.cloned().unwrap_or(ScalarChannel { + keys: Vec::new(), + extrapolation: None, + })); + let mut keys = normalized.keys; + + if let Some(id) = keyframe_id { + if let Some(index) = keys.iter().position(|key| key.id == id) { + replace_scalar_key_at(&mut keys, index, time, value, interpolation, false); + return normalize_scalar_channel(&ScalarChannel { + keys, + extrapolation: normalized.extrapolation, + }); + } + } + + if let Some(index) = keys.iter().position(|key| key.time == time) { + replace_scalar_key_at(&mut keys, index, time, value, interpolation, true); + return normalize_scalar_channel(&ScalarChannel { + keys, + extrapolation: normalized.extrapolation, + }); + } + + keys.push(create_scalar_key( + keyframe_id.map(str::to_string).unwrap_or_else(generate_uuid), + time, + value, + interpolation.or(default_interpolation), + None, + )); + normalize_scalar_channel(&ScalarChannel { + keys, + extrapolation: normalized.extrapolation, + }) +} + +pub fn get_channel<'a>( + animations: Option<&'a ElementAnimations>, + property_path: &str, +) -> Option<&'a AnimationChannel> { + let data = animations?.get(property_path); + match data { + ChannelData::Single(channel) => Some(channel), + ChannelData::Composite(_) => get_channels_from_data(Some(data)).into_iter().next(), + } +} + +pub fn upsert_path_keyframe( + animations: Option<&ElementAnimations>, + property_path: &str, + time: MediaTime, + value: &ParamValue, + interpolation: Option<AnimationInterpolation>, + keyframe_id: Option<&str>, + channel_layout: &ChannelLayout, + coerce_value: impl Fn(&ParamValue) -> Option<ParamValue>, +) -> Option<ElementAnimations> { + let coerced_value = coerce_value(value)?; + + let mut next_animations = animations.cloned().unwrap_or_default(); + let current_data = animations.and_then(|a| a.get(property_path)).cloned(); + let primary_channel = get_primary_channel_from_data(current_data.as_ref(), channel_layout); + let (target_id, target_time) = get_target_key_metadata(primary_channel, time, keyframe_id); + let component_values = channel_layout.decompose(&coerced_value)?; + + let mut next_data = current_data.clone(); + for component in channel_layout.components() { + let component_key = component.key.as_str(); + let Some(next_value) = component_values.get(component_key) else { + continue; + }; + let current_channel = get_channel_from_data(current_data.as_ref(), component_key); + + match component.value_kind { + ChannelValueKind::Discrete => { + let discrete_value = match next_value { + ParamValue::String(value) => DiscreteValue::String(value.clone()), + ParamValue::Bool(value) => DiscreteValue::Bool(*value), + ParamValue::Number(_) => continue, + }; + let discrete_channel = match current_channel { + Some(AnimationChannel::Discrete(discrete)) => Some(discrete), + _ => None, + }; + let next_channel = upsert_discrete_channel_key( + discrete_channel, + target_time, + discrete_value, + Some(&target_id), + ); + next_data = set_channel_in_data( + next_data, + component_key, + Some(AnimationChannel::Discrete(next_channel)), + ); + } + ChannelValueKind::Scalar => { + let ParamValue::Number(number_value) = next_value else { + continue; + }; + let scalar_channel = match current_channel { + Some(AnimationChannel::Scalar(scalar)) => Some(scalar), + _ => None, + }; + let next_channel = upsert_scalar_channel_key( + scalar_channel, + target_time, + *number_value, + interpolation, + Some(component.default_interpolation), + Some(&target_id), + ); + next_data = set_channel_in_data( + next_data, + component_key, + Some(AnimationChannel::Scalar(next_channel)), + ); + } + } + } + + match next_data { + Some(data) => { + next_animations.insert(property_path.to_string(), data); + } + None => { + next_animations.remove(property_path); + } + } + to_animation(next_animations) +} + +pub fn upsert_keyframe( + channel: Option<&AnimationChannel>, + time: MediaTime, + value: &ParamValue, + interpolation: Option<AnimationInterpolation>, + keyframe_id: Option<&str>, +) -> Option<AnimationChannel> { + let channel = channel?; + + match value { + ParamValue::String(value) => { + let discrete = match channel { + AnimationChannel::Discrete(discrete) => Some(discrete), + _ => None, + }; + Some(AnimationChannel::Discrete(upsert_discrete_channel_key( + discrete, + time, + DiscreteValue::String(value.clone()), + keyframe_id, + ))) + } + ParamValue::Bool(value) => { + let discrete = match channel { + AnimationChannel::Discrete(discrete) => Some(discrete), + _ => None, + }; + Some(AnimationChannel::Discrete(upsert_discrete_channel_key( + discrete, + time, + DiscreteValue::Bool(*value), + keyframe_id, + ))) + } + ParamValue::Number(value) => { + let scalar = match channel { + AnimationChannel::Scalar(scalar) => Some(scalar), + _ => None, + }; + Some(AnimationChannel::Scalar(upsert_scalar_channel_key( + scalar, + time, + *value, + interpolation, + None, + keyframe_id, + ))) + } + } +} + +pub fn remove_keyframe( + channel: Option<&AnimationChannel>, + keyframe_id: &str, +) -> Option<AnimationChannel> { + let channel = channel?; + + match channel { + AnimationChannel::Scalar(scalar) => { + let keys: Vec<ScalarAnimationKey> = scalar + .keys + .iter() + .filter(|key| key.id != keyframe_id) + .cloned() + .collect(); + if keys.is_empty() { + return None; + } + Some(AnimationChannel::Scalar(normalize_scalar_channel( + &ScalarChannel { + keys, + extrapolation: scalar.extrapolation.clone(), + }, + ))) + } + AnimationChannel::Discrete(discrete) => { + let keys: Vec<DiscreteAnimationKey> = discrete + .keys + .iter() + .filter(|key| key.id != keyframe_id) + .cloned() + .collect(); + if keys.is_empty() { + return None; + } + Some(AnimationChannel::Discrete(normalize_discrete_channel( + &DiscreteChannel { keys }, + ))) + } + } +} + +pub fn retime_keyframe( + channel: Option<&AnimationChannel>, + keyframe_id: &str, + time: MediaTime, +) -> Option<AnimationChannel> { + let channel = channel?; + + match channel { + AnimationChannel::Scalar(scalar) => { + let Some(index) = scalar.keys.iter().position(|key| key.id == keyframe_id) else { + return Some(channel.clone()); + }; + let mut keys = scalar.keys.clone(); + keys[index].time = time; + Some(AnimationChannel::Scalar(normalize_scalar_channel( + &ScalarChannel { + keys, + extrapolation: scalar.extrapolation.clone(), + }, + ))) + } + AnimationChannel::Discrete(discrete) => { + let Some(index) = discrete.keys.iter().position(|key| key.id == keyframe_id) else { + return Some(channel.clone()); + }; + let mut keys = discrete.keys.clone(); + keys[index].time = time; + Some(AnimationChannel::Discrete(normalize_discrete_channel( + &DiscreteChannel { keys }, + ))) + } + } +} + +pub fn set_channel( + animations: Option<&ElementAnimations>, + property_path: &str, + channel: Option<&AnimationChannel>, +) -> Option<ElementAnimations> { + set_binding_component_channel(animations, property_path, "value", channel) +} + +pub fn set_binding_component_channel( + animations: Option<&ElementAnimations>, + property_path: &str, + component_key: &str, + channel: Option<&AnimationChannel>, +) -> Option<ElementAnimations> { + let mut next_animations = animations.cloned().unwrap_or_default(); + let prepared = channel + .filter(|channel| has_channel_keys(Some(channel))) + .map(normalize_channel); + let data = set_channel_in_data( + next_animations.get(property_path).cloned(), + component_key, + prepared, + ); + match data { + Some(data) => { + next_animations.insert(property_path.to_string(), data); + } + None => { + next_animations.remove(property_path); + } + } + to_animation(next_animations) +} + +pub fn update_scalar_keyframe_curve( + animations: Option<&ElementAnimations>, + property_path: &str, + component_key: &str, + keyframe_id: &str, + patch: &ScalarCurveKeyframePatch, +) -> Option<ElementAnimations> { + let channel = get_channel_from_data( + animations.and_then(|a| a.get(property_path)), + component_key, + ); + let Some(AnimationChannel::Scalar(scalar)) = channel else { + return animations.cloned(); + }; + + let Some(keyframe_index) = scalar.keys.iter().position(|key| key.id == keyframe_id) else { + return animations.cloned(); + }; + + let mut keys = scalar.keys.clone(); + let current_key = &keys[keyframe_index]; + keys[keyframe_index] = ScalarAnimationKey { + left_handle: patch.left_handle.unwrap_or(current_key.left_handle), + right_handle: patch.right_handle.unwrap_or(current_key.right_handle), + segment_to_next: patch.segment_to_next.unwrap_or(current_key.segment_to_next), + tangent_mode: patch.tangent_mode.unwrap_or(current_key.tangent_mode), + ..current_key.clone() + }; + + set_binding_component_channel( + animations, + property_path, + component_key, + Some(&AnimationChannel::Scalar(ScalarChannel { + keys, + extrapolation: scalar.extrapolation.clone(), + })), + ) +} + +fn clone_channel_with_key_ids( + channel: &AnimationChannel, + key_id_map: &HashMap<String, String>, +) -> AnimationChannel { + let remap = |id: &str| key_id_map.get(id).cloned().unwrap_or_else(|| id.to_string()); + match channel { + AnimationChannel::Scalar(scalar) => AnimationChannel::Scalar(normalize_scalar_channel( + &ScalarChannel { + keys: scalar + .keys + .iter() + .map(|key| ScalarAnimationKey { + id: remap(&key.id), + ..key.clone() + }) + .collect(), + extrapolation: scalar.extrapolation.clone(), + }, + )), + AnimationChannel::Discrete(discrete) => { + AnimationChannel::Discrete(normalize_discrete_channel(&DiscreteChannel { + keys: discrete + .keys + .iter() + .map(|key| DiscreteAnimationKey { + id: remap(&key.id), + ..key.clone() + }) + .collect(), + })) + } + } +} + +pub fn clone_animations( + animations: Option<&ElementAnimations>, + regenerate_keyframe_ids: bool, +) -> Option<ElementAnimations> { + let animations = animations?; + + let mut next_animations = animations.clone(); + for (property_path, data) in animations + .iter() + .filter(|(key, _)| is_animation_storage_key(key)) + { + let channels = get_channels_from_data(Some(data)); + let mut key_id_map = HashMap::new(); + if let Some(primary_channel) = channels.first() { + for (id, _) in channel_key_ids_and_times(primary_channel) { + let next_id = if regenerate_keyframe_ids { + generate_uuid() + } else { + id.clone() + }; + key_id_map.insert(id, next_id); + } + } + + let next_data = match data { + ChannelData::Single(channel) => { + ChannelData::Single(clone_channel_with_key_ids(channel, &key_id_map)) + } + ChannelData::Composite(components) => ChannelData::Composite( + components + .iter() + .map(|(component_key, channel)| { + ( + component_key.clone(), + clone_channel_with_key_ids(channel, &key_id_map), + ) + }) + .collect(), + ), + }; + next_animations.insert(property_path.clone(), next_data); + } + + to_animation(next_animations) +} + +pub fn clamp_animations_to_duration( + animations: Option<&ElementAnimations>, + duration: MediaTime, +) -> Option<ElementAnimations> { + let animations = animations?; + if duration <= MediaTime::ZERO { + return None; + } + split_animations_at_time_with_options(Some(animations), duration, true).0 +} + +#[derive(Clone, Copy, Debug)] +struct Point { + x: f64, + y: f64, +} + +fn lerp_point(left: Point, right: Point, progress: f64) -> Point { + Point { + x: left.x + (right.x - left.x) * progress, + y: left.y + (right.y - left.y) * progress, + } +} + +pub struct SplitChannelResult { + pub left_channel: Option<AnimationChannel>, + pub right_channel: Option<AnimationChannel>, +} + +fn split_discrete_channel_at_time( + channel: Option<&DiscreteChannel>, + split_time: MediaTime, + left_boundary_id: &str, + right_boundary_id: &str, + include_split_boundary: bool, +) -> SplitChannelResult { + let Some(channel) = channel.filter(|channel| !channel.keys.is_empty()) else { + return SplitChannelResult { + left_channel: None, + right_channel: None, + }; + }; + + let normalized = normalize_discrete_channel(channel); + let mut left_keys: Vec<DiscreteAnimationKey> = normalized + .keys + .iter() + .filter(|key| key.time <= split_time) + .cloned() + .collect(); + let mut right_keys: Vec<DiscreteAnimationKey> = normalized + .keys + .iter() + .filter(|key| key.time >= split_time) + .map(|key| DiscreteAnimationKey { + time: key.time - split_time, + ..key.clone() + }) + .collect(); + + if include_split_boundary { + let has_boundary_on_left = left_keys.iter().any(|key| key.time == split_time); + let has_boundary_on_right = right_keys.iter().any(|key| key.time == MediaTime::ZERO); + let boundary_value = discrete_value_at_time( + &normalized, + split_time, + normalized.keys[0].value.clone(), + ); + if !has_boundary_on_left { + left_keys.push(DiscreteAnimationKey { + id: left_boundary_id.to_string(), + time: split_time, + value: boundary_value.clone(), + }); + } + if !has_boundary_on_right { + right_keys.insert( + 0, + DiscreteAnimationKey { + id: right_boundary_id.to_string(), + time: MediaTime::ZERO, + value: boundary_value, + }, + ); + } + } + + SplitChannelResult { + left_channel: if left_keys.is_empty() { + None + } else { + Some(AnimationChannel::Discrete(normalize_discrete_channel( + &DiscreteChannel { keys: left_keys }, + ))) + }, + right_channel: if right_keys.is_empty() { + None + } else { + Some(AnimationChannel::Discrete(normalize_discrete_channel( + &DiscreteChannel { keys: right_keys }, + ))) + }, + } +} + +fn discrete_value_at_time( + channel: &DiscreteChannel, + time: MediaTime, + fallback_value: DiscreteValue, +) -> DiscreteValue { + let ParamValue::Bool(_) = ParamValue::Bool(false) else { + unreachable!() + }; + crate::interpolation::discrete_channel_value_at_time(Some(channel), time, fallback_value) +} + +fn build_split_scalar_result( + left_keys: Vec<ScalarAnimationKey>, + right_keys: Vec<ScalarAnimationKey>, + extrapolation: Option<scene::ChannelExtrapolation>, +) -> SplitChannelResult { + SplitChannelResult { + left_channel: if left_keys.is_empty() { + None + } else { + Some(AnimationChannel::Scalar(normalize_scalar_channel( + &ScalarChannel { + keys: left_keys, + extrapolation: extrapolation.clone(), + }, + ))) + }, + right_channel: if right_keys.is_empty() { + None + } else { + Some(AnimationChannel::Scalar(normalize_scalar_channel( + &ScalarChannel { + keys: right_keys, + extrapolation, + }, + ))) + }, + } +} + +fn split_scalar_channel_at_time( + channel: Option<&ScalarChannel>, + split_time: MediaTime, + left_boundary_id: &str, + right_boundary_id: &str, + include_split_boundary: bool, +) -> SplitChannelResult { + let Some(channel) = channel.filter(|channel| !channel.keys.is_empty()) else { + return SplitChannelResult { + left_channel: None, + right_channel: None, + }; + }; + + let normalized = normalize_scalar_channel(channel); + let mut left_keys: Vec<ScalarAnimationKey> = normalized + .keys + .iter() + .filter(|key| key.time <= split_time) + .cloned() + .collect(); + let mut right_keys: Vec<ScalarAnimationKey> = normalized + .keys + .iter() + .filter(|key| key.time >= split_time) + .map(|key| ScalarAnimationKey { + time: key.time - split_time, + ..key.clone() + }) + .collect(); + + let has_boundary_on_left = left_keys.iter().any(|key| key.time == split_time); + let has_boundary_on_right = right_keys.iter().any(|key| key.time == MediaTime::ZERO); + if !include_split_boundary || (has_boundary_on_left && has_boundary_on_right) { + return build_split_scalar_result( + left_keys, + right_keys, + normalized.extrapolation.clone(), + ); + } + + for key_index in 0..normalized.keys.len().saturating_sub(1) { + let left_key = &normalized.keys[key_index]; + let right_key = &normalized.keys[key_index + 1]; + if split_time <= left_key.time || split_time >= right_key.time { + continue; + } + + let boundary_value = crate::interpolation::scalar_channel_value_at_time( + Some(&normalized), + split_time, + left_key.value, + ); + + if left_key.segment_to_next == ScalarSegmentType::Bezier { + let (right_handle, left_handle) = segment_handles(left_key, right_key); + let progress = solve_bezier_progress_for_time(split_time, left_key, right_key); + let p0 = Point { + x: left_key.time.as_ticks() as f64, + y: left_key.value, + }; + let p1 = Point { + x: p0.x + right_handle.dt, + y: p0.y + right_handle.dv, + }; + let p3 = Point { + x: right_key.time.as_ticks() as f64, + y: right_key.value, + }; + let p2 = Point { + x: p3.x + left_handle.dt, + y: p3.y + left_handle.dv, + }; + let q0 = lerp_point(p0, p1, progress); + let q1 = lerp_point(p1, p2, progress); + let q2 = lerp_point(p2, p3, progress); + let r0 = lerp_point(q0, q1, progress); + let r1 = lerp_point(q1, q2, progress); + let split_point = lerp_point(r0, r1, progress); + + let mut next_left_keys: Vec<ScalarAnimationKey> = normalized + .keys + .iter() + .filter(|key| key.time < split_time) + .cloned() + .collect(); + next_left_keys.push(ScalarAnimationKey { + right_handle: Some(scene::CurveHandle { + dt: round_media_time(q0.x - p0.x), + dv: q0.y - p0.y, + }), + ..left_key.clone() + }); + next_left_keys.push(ScalarAnimationKey { + id: left_boundary_id.to_string(), + time: split_time, + value: boundary_value, + left_handle: Some(scene::CurveHandle { + dt: round_media_time(r0.x - split_point.x), + dv: r0.y - split_point.y, + }), + right_handle: None, + segment_to_next: left_key.segment_to_next, + tangent_mode: left_key.tangent_mode, + }); + + let mut next_right_keys: Vec<ScalarAnimationKey> = vec![ + ScalarAnimationKey { + id: right_boundary_id.to_string(), + time: MediaTime::ZERO, + value: boundary_value, + left_handle: None, + right_handle: Some(scene::CurveHandle { + dt: round_media_time(r1.x - split_point.x), + dv: r1.y - split_point.y, + }), + segment_to_next: ScalarSegmentType::Bezier, + tangent_mode: left_key.tangent_mode, + }, + ScalarAnimationKey { + time: right_key.time - split_time, + left_handle: Some(scene::CurveHandle { + dt: round_media_time(q2.x - p3.x), + dv: q2.y - p3.y, + }), + ..right_key.clone() + }, + ]; + next_right_keys.extend( + normalized + .keys + .iter() + .filter(|key| key.time > right_key.time) + .map(|key| ScalarAnimationKey { + time: key.time - split_time, + ..key.clone() + }), + ); + + return SplitChannelResult { + left_channel: Some(AnimationChannel::Scalar(normalize_scalar_channel( + &ScalarChannel { + keys: next_left_keys, + extrapolation: normalized.extrapolation.clone(), + }, + ))), + right_channel: Some(AnimationChannel::Scalar(normalize_scalar_channel( + &ScalarChannel { + keys: next_right_keys, + extrapolation: normalized.extrapolation.clone(), + }, + ))), + }; + } + + left_keys.push(create_scalar_key( + left_boundary_id.to_string(), + split_time, + boundary_value, + Some(AnimationInterpolation::Linear), + None, + )); + right_keys.insert( + 0, + create_scalar_key( + right_boundary_id.to_string(), + MediaTime::ZERO, + boundary_value, + Some(scalar_segment_interpolation(left_key.segment_to_next)), + None, + ), + ); + return build_split_scalar_result( + left_keys, + right_keys, + normalized.extrapolation.clone(), + ); + } + + build_split_scalar_result(left_keys, right_keys, normalized.extrapolation.clone()) +} + +fn split_channel_at_time( + channel: Option<&AnimationChannel>, + split_time: MediaTime, + left_boundary_id: &str, + right_boundary_id: &str, + include_split_boundary: bool, +) -> SplitChannelResult { + match channel { + Some(AnimationChannel::Discrete(discrete)) => split_discrete_channel_at_time( + Some(discrete), + split_time, + left_boundary_id, + right_boundary_id, + include_split_boundary, + ), + scalar => split_scalar_channel_at_time( + match scalar { + Some(AnimationChannel::Scalar(scalar)) => Some(scalar), + _ => None, + }, + split_time, + left_boundary_id, + right_boundary_id, + include_split_boundary, + ), + } +} + +pub fn split_animations_at_time_with_options( + animations: Option<&ElementAnimations>, + split_time: MediaTime, + include_split_boundary: bool, +) -> (Option<ElementAnimations>, Option<ElementAnimations>) { + let Some(animations) = animations else { + return (None, None); + }; + + let mut left_animations = ElementAnimations::new(); + let mut right_animations = ElementAnimations::new(); + + for (property_path, data) in animations + .iter() + .filter(|(key, _)| is_animation_storage_key(key)) + { + let left_boundary_id = generate_uuid(); + let right_boundary_id = generate_uuid(); + + for (component_key, channel) in get_channel_entries_from_data(Some(data)) { + let split = split_channel_at_time( + Some(channel), + split_time, + &left_boundary_id, + &right_boundary_id, + include_split_boundary, + ); + if let Some(left_channel) = split.left_channel { + if let Some(data) = set_channel_in_data( + left_animations.get(property_path).cloned(), + &component_key, + Some(left_channel), + ) { + left_animations.insert(property_path.clone(), data); + } + } + if let Some(right_channel) = split.right_channel { + if let Some(data) = set_channel_in_data( + right_animations.get(property_path).cloned(), + &component_key, + Some(right_channel), + ) { + right_animations.insert(property_path.clone(), data); + } + } + } + } + + (to_animation(left_animations), to_animation(right_animations)) +} + +pub fn split_animations_at_time( + animations: &ElementAnimations, + split_time: MediaTime, +) -> (ElementAnimations, ElementAnimations) { + let (left, right) = split_animations_at_time_with_options(Some(animations), split_time, true); + (left.unwrap_or_default(), right.unwrap_or_default()) +} + +pub fn remove_element_keyframe( + animations: Option<&ElementAnimations>, + property_path: &str, + keyframe_id: &str, +) -> Option<ElementAnimations> { + let data = animations.and_then(|a| a.get(property_path))?; + + let mut next_animations = animations.cloned().unwrap_or_default(); + let next_data = match data { + ChannelData::Single(channel) => remove_keyframe(Some(channel), keyframe_id), + ChannelData::Composite(components) => { + let mut next_data = Some(data.clone()); + for (component_key, channel) in components { + next_data = set_channel_in_data( + next_data, + component_key, + remove_keyframe(Some(channel), keyframe_id), + ); + } + next_data + } + }; + match next_data { + Some(data) => { + next_animations.insert(property_path.to_string(), data); + } + None => { + next_animations.remove(property_path); + } + } + to_animation(next_animations) +} + +pub fn retime_element_keyframe( + animations: Option<&ElementAnimations>, + property_path: &str, + keyframe_id: &str, + time: MediaTime, +) -> Option<ElementAnimations> { + let data = animations.and_then(|a| a.get(property_path))?; + + let mut next_animations = animations.cloned().unwrap_or_default(); + let next_data = match data { + ChannelData::Single(channel) => retime_keyframe(Some(channel), keyframe_id, time), + ChannelData::Composite(components) => { + let mut next_data = Some(data.clone()); + for (component_key, channel) in components { + next_data = set_channel_in_data( + next_data, + component_key, + retime_keyframe(Some(channel), keyframe_id, time), + ); + } + next_data + } + }; + match next_data { + Some(data) => { + next_animations.insert(property_path.to_string(), data); + } + None => { + next_animations.remove(property_path); + } + } + to_animation(next_animations) +} diff --git a/rust/crates/animation/src/lib.rs b/rust/crates/animation/src/lib.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/rust/crates/animation/src/lib.rs @@ -0,0 +1 @@ + diff --git a/rust/crates/animation/src/types.rs b/rust/crates/animation/src/types.rs new file mode 100644 index 00000000..ca0c3c20 --- /dev/null +++ b/rust/crates/animation/src/types.rs @@ -0,0 +1,45 @@ +use scene::{CurveHandle, ParamValue, ScalarSegmentType, TangentMode}; +use serde::{Deserialize, Serialize}; +use time::MediaTime; + +#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum AnimationInterpolation { + Linear, + Hold, + Bezier, +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct NumericSpec { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub min: Option<f64>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max: Option<f64>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub step: Option<f64>, +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ScalarCurveKeyframePatch { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub left_handle: Option<Option<CurveHandle>>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub right_handle: Option<Option<CurveHandle>>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub segment_to_next: Option<ScalarSegmentType>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tangent_mode: Option<TangentMode>, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ElementKeyframe { + pub property_path: String, + pub id: String, + pub time: MediaTime, + pub value: ParamValue, + pub interpolation: AnimationInterpolation, +} |
