Skip to main content

alpha2_model/
animation.rs

1use crate::{CubicBezier, Easing};
2use serde::{Deserialize, Serialize};
3
4/// A two-dimensional value in normalized output coordinates.
5#[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
6#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
7#[serde(deny_unknown_fields)]
8pub struct Vec2 {
9    /// Horizontal component.
10    pub x: f32,
11    /// Vertical component.
12    pub y: f32,
13}
14
15impl Vec2 {
16    /// Creates a two-dimensional value.
17    #[must_use]
18    pub const fn new(x: f32, y: f32) -> Self {
19        Self { x, y }
20    }
21
22    fn is_finite(self) -> bool {
23        self.x.is_finite() && self.y.is_finite()
24    }
25}
26
27/// A value that is either constant or controlled by sorted keyframes.
28#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
29#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
30#[serde(deny_unknown_fields)]
31#[serde(tag = "mode", rename_all = "snake_case")]
32#[cfg_attr(feature = "schema", schemars(rename = "Animated_{T}"))]
33pub enum Animated<T> {
34    /// A value without animation.
35    Constant {
36        /// Value used at every timestamp.
37        value: T,
38    },
39    /// Values evaluated from their keyframe sequence.
40    Keyframes {
41        /// Strictly ascending keyframes. The evaluator clamps outside the sequence.
42        keyframes: Vec<Keyframe<T>>,
43    },
44}
45
46/// One timestamped control value.
47#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
48#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
49#[serde(deny_unknown_fields)]
50#[cfg_attr(feature = "schema", schemars(rename = "Keyframe_{T}"))]
51pub struct Keyframe<T> {
52    /// Timestamp of this value: timeline time, except in a composition
53    /// node's settings, which are keyed in composition time.
54    pub time: alpha2_time::MediaTime,
55    /// Value at `time`.
56    pub value: T,
57    /// Interpolation from this keyframe to its successor.
58    pub interpolation: Interpolation,
59}
60
61/// Interpolation from one keyframe to the next.
62///
63/// Values are interpolated numerically: a rotation from 0 to 720 degrees
64/// turns twice, and one from 0 to 360 turns once. A host that wants the
65/// shortest path normalizes its keyframe values.
66#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
67#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
68#[serde(tag = "type", rename_all = "snake_case", from = "InterpolationWire")]
69pub enum Interpolation {
70    /// Keep this keyframe's value until the next keyframe.
71    Hold,
72    /// Move to the next value at a constant rate.
73    Linear,
74    /// Move to the next value along a CSS `cubic-bezier()` timing curve.
75    CubicBezier(CubicBezier),
76}
77
78/// The wire form of [`Interpolation`]; see `EasingWire` for why the unit
79/// variants are empty structs here.
80#[derive(Deserialize)]
81#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
82#[cfg_attr(feature = "schema", schemars(rename = "Interpolation"))]
83#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
84pub(crate) enum InterpolationWire {
85    /// Keep this keyframe's value until the next keyframe.
86    Hold {},
87    /// Move to the next value at a constant rate.
88    Linear {},
89    /// Move to the next value along a CSS `cubic-bezier()` timing curve.
90    CubicBezier(CubicBezier),
91}
92
93impl From<InterpolationWire> for Interpolation {
94    fn from(wire: InterpolationWire) -> Self {
95        match wire {
96            InterpolationWire::Hold {} => Self::Hold,
97            InterpolationWire::Linear {} => Self::Linear,
98            InterpolationWire::CubicBezier(curve) => Self::CubicBezier(curve),
99        }
100    }
101}
102
103impl Interpolation {
104    /// The fraction of the way to the next value at linear segment
105    /// `progress` (clamped to `0..=1`), or `None` while holding.
106    ///
107    /// A cubic Bezier curve may overshoot, so the fraction can leave `0..=1`.
108    #[must_use]
109    pub fn amount(self, progress: f64) -> Option<f64> {
110        match self {
111            Self::Hold => None,
112            Self::Linear => Some(Easing::Linear.apply(progress)),
113            Self::CubicBezier(curve) => Some(curve.apply(progress)),
114        }
115    }
116
117    /// Whether the interpolation's parameters are usable.
118    #[must_use]
119    pub fn is_valid(self) -> bool {
120        match self {
121            Self::Hold | Self::Linear => true,
122            Self::CubicBezier(curve) => curve.is_valid(),
123        }
124    }
125}
126
127/// Resolution-independent visual presentation controls for a clip.
128#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
129#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
130#[serde(deny_unknown_fields)]
131pub struct VisualProperties {
132    /// Animated normalized canvas translation.
133    pub position: Animated<Vec2>,
134    /// Unanimated normalized point within the source rectangle.
135    pub anchor: Vec2,
136    /// Animated per-axis scale.
137    pub scale: Animated<Vec2>,
138    /// Animated clockwise rotation in degrees.
139    pub rotation_degrees: Animated<f32>,
140    /// Animated opacity constrained to `0..=1`.
141    pub opacity: Animated<f32>,
142}
143
144impl Default for VisualProperties {
145    fn default() -> Self {
146        Self {
147            position: Animated::Constant {
148                value: Vec2::new(0.5, 0.5),
149            },
150            anchor: Vec2::new(0.5, 0.5),
151            scale: Animated::Constant {
152                value: Vec2::new(1.0, 1.0),
153            },
154            rotation_degrees: Animated::Constant { value: 0.0 },
155            opacity: Animated::Constant { value: 1.0 },
156        }
157    }
158}
159
160impl VisualProperties {
161    pub(crate) fn validate(&self) -> bool {
162        self.anchor.is_finite()
163            && valid_vec2_animation(&self.position)
164            && valid_vec2_animation(&self.scale)
165            && valid_scalar_animation(&self.rotation_degrees, false)
166            && valid_scalar_animation(&self.opacity, true)
167    }
168}
169
170fn valid_vec2_animation(animation: &Animated<Vec2>) -> bool {
171    match animation {
172        Animated::Constant { value } => value.is_finite(),
173        Animated::Keyframes { keyframes } => {
174            valid_keyframes(keyframes)
175                && keyframes.iter().all(|keyframe| keyframe.value.is_finite())
176        }
177    }
178}
179
180fn valid_scalar_animation(animation: &Animated<f32>, unit_interval: bool) -> bool {
181    let valid_value =
182        |value: f32| value.is_finite() && (!unit_interval || (0.0..=1.0).contains(&value));
183    match animation {
184        Animated::Constant { value } => valid_value(*value),
185        Animated::Keyframes { keyframes } => {
186            valid_keyframes(keyframes)
187                && keyframes.iter().all(|keyframe| valid_value(keyframe.value))
188        }
189    }
190}
191
192/// Valid keyframes (or a constant) whose every finite value passes `check`.
193pub(crate) fn valid_scalar_values(animation: &Animated<f32>, check: impl Fn(f32) -> bool) -> bool {
194    let valid = |value: f32| value.is_finite() && check(value);
195    match animation {
196        Animated::Constant { value } => valid(*value),
197        Animated::Keyframes { keyframes } => {
198            valid_keyframes(keyframes) && keyframes.iter().all(|keyframe| valid(keyframe.value))
199        }
200    }
201}
202
203/// [`valid_scalar_values`] for two-dimensional values.
204pub(crate) fn valid_vec2_values(animation: &Animated<Vec2>, check: impl Fn(Vec2) -> bool) -> bool {
205    let valid = |value: Vec2| value.is_finite() && check(value);
206    match animation {
207        Animated::Constant { value } => valid(*value),
208        Animated::Keyframes { keyframes } => {
209            valid_keyframes(keyframes) && keyframes.iter().all(|keyframe| valid(keyframe.value))
210        }
211    }
212}
213
214/// A non-empty, strictly ascending sequence whose interpolations are usable.
215pub(crate) fn valid_keyframes<T>(keyframes: &[Keyframe<T>]) -> bool {
216    !keyframes.is_empty()
217        && keyframes.windows(2).all(|pair| pair[0].time < pair[1].time)
218        && keyframes
219            .iter()
220            .all(|keyframe| keyframe.interpolation.is_valid())
221}