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    /// Whether both components are zero (either sign).
27    #[must_use]
28    pub fn is_zero(&self) -> bool {
29        self.x == 0.0 && self.y == 0.0
30    }
31}
32
33/// A value that is either constant or controlled by sorted keyframes.
34#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
35#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
36#[serde(deny_unknown_fields)]
37#[serde(tag = "mode", rename_all = "snake_case")]
38#[cfg_attr(feature = "schema", schemars(rename = "Animated_{T}"))]
39pub enum Animated<T> {
40    /// A value without animation.
41    Constant {
42        /// Value used at every timestamp.
43        value: T,
44    },
45    /// Values evaluated from their keyframe sequence.
46    Keyframes {
47        /// Strictly ascending keyframes. The evaluator clamps outside the sequence.
48        keyframes: Vec<Keyframe<T>>,
49    },
50}
51
52/// A position that can move along curves between its keyframes.
53///
54/// Its JSON is that of an `Animated<Vec2>`, plus optional spatial tangents on
55/// each keyframe: with them, the value travels along the cubic Bezier from
56/// one keyframe's value through its `out_tangent` and the next keyframe's
57/// `in_tangent`, at the speed its timing curve sets along the curve's
58/// length. Without them it moves in a straight line, as an `Animated<Vec2>`
59/// does.
60#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
61#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
62#[serde(deny_unknown_fields)]
63#[serde(tag = "mode", rename_all = "snake_case")]
64pub enum AnimatedPoint {
65    /// A value without animation.
66    Constant {
67        /// Value used at every timestamp.
68        value: Vec2,
69    },
70    /// Values evaluated from their keyframe sequence.
71    Keyframes {
72        /// Strictly ascending keyframes. The evaluator clamps outside the sequence.
73        keyframes: Vec<PointKeyframe>,
74    },
75}
76
77impl Default for AnimatedPoint {
78    fn default() -> Self {
79        Self::Constant {
80            value: Vec2::default(),
81        }
82    }
83}
84
85impl From<Animated<Vec2>> for AnimatedPoint {
86    fn from(animation: Animated<Vec2>) -> Self {
87        match animation {
88            Animated::Constant { value } => Self::Constant { value },
89            Animated::Keyframes { keyframes } => Self::Keyframes {
90                keyframes: keyframes
91                    .into_iter()
92                    .map(|keyframe| PointKeyframe {
93                        time: keyframe.time,
94                        value: keyframe.value,
95                        interpolation: keyframe.interpolation,
96                        out_tangent: Vec2::default(),
97                        in_tangent: Vec2::default(),
98                    })
99                    .collect(),
100            },
101        }
102    }
103}
104
105impl AnimatedPoint {
106    /// Whether the value changes over time.
107    #[must_use]
108    pub const fn is_animated(&self) -> bool {
109        matches!(self, Self::Keyframes { .. })
110    }
111}
112
113/// One keyframe of an [`AnimatedPoint`].
114#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
115#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
116#[serde(deny_unknown_fields)]
117pub struct PointKeyframe {
118    /// Timestamp of this value, as a [`Keyframe`]'s.
119    pub time: alpha2_time::MediaTime,
120    /// Value at `time`.
121    pub value: Vec2,
122    /// Interpolation from this keyframe to its successor: how fast the value
123    /// travels along the way there.
124    pub interpolation: Interpolation,
125    /// The start of the way to the next keyframe's value, relative to this
126    /// value: the curve's first control point.
127    #[serde(default, skip_serializing_if = "Vec2::is_zero")]
128    pub out_tangent: Vec2,
129    /// The end of the way here from the previous keyframe's value, relative
130    /// to this value: that curve's second control point.
131    #[serde(default, skip_serializing_if = "Vec2::is_zero")]
132    pub in_tangent: Vec2,
133}
134
135/// One timestamped control value.
136#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
137#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
138#[serde(deny_unknown_fields)]
139#[cfg_attr(feature = "schema", schemars(rename = "Keyframe_{T}"))]
140pub struct Keyframe<T> {
141    /// Timestamp of this value: timeline time, except in a composition
142    /// node's settings, which are keyed in composition time.
143    pub time: alpha2_time::MediaTime,
144    /// Value at `time`.
145    pub value: T,
146    /// Interpolation from this keyframe to its successor.
147    pub interpolation: Interpolation,
148}
149
150/// Interpolation from one keyframe to the next.
151///
152/// Values are interpolated numerically: a rotation from 0 to 720 degrees
153/// turns twice, and one from 0 to 360 turns once. A host that wants the
154/// shortest path normalizes its keyframe values.
155#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
156#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
157#[serde(tag = "type", rename_all = "snake_case", from = "InterpolationWire")]
158pub enum Interpolation {
159    /// Keep this keyframe's value until the next keyframe.
160    Hold,
161    /// Move to the next value at a constant rate.
162    Linear,
163    /// Move to the next value along a CSS `cubic-bezier()` timing curve.
164    CubicBezier(CubicBezier),
165}
166
167/// The wire form of [`Interpolation`]; see `EasingWire` for why the unit
168/// variants are empty structs here.
169#[derive(Deserialize)]
170#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
171#[cfg_attr(feature = "schema", schemars(rename = "Interpolation"))]
172#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
173pub(crate) enum InterpolationWire {
174    /// Keep this keyframe's value until the next keyframe.
175    Hold {},
176    /// Move to the next value at a constant rate.
177    Linear {},
178    /// Move to the next value along a CSS `cubic-bezier()` timing curve.
179    CubicBezier(CubicBezier),
180}
181
182impl From<InterpolationWire> for Interpolation {
183    fn from(wire: InterpolationWire) -> Self {
184        match wire {
185            InterpolationWire::Hold {} => Self::Hold,
186            InterpolationWire::Linear {} => Self::Linear,
187            InterpolationWire::CubicBezier(curve) => Self::CubicBezier(curve),
188        }
189    }
190}
191
192impl Interpolation {
193    /// The fraction of the way to the next value at linear segment
194    /// `progress` (clamped to `0..=1`), or `None` while holding.
195    ///
196    /// A cubic Bezier curve may overshoot, so the fraction can leave `0..=1`.
197    #[must_use]
198    pub fn amount(self, progress: f64) -> Option<f64> {
199        match self {
200            Self::Hold => None,
201            Self::Linear => Some(Easing::Linear.apply(progress)),
202            Self::CubicBezier(curve) => Some(curve.apply(progress)),
203        }
204    }
205
206    /// Whether the interpolation's parameters are usable.
207    #[must_use]
208    pub fn is_valid(self) -> bool {
209        match self {
210            Self::Hold | Self::Linear => true,
211            Self::CubicBezier(curve) => curve.is_valid(),
212        }
213    }
214}
215
216/// Resolution-independent visual presentation controls for a clip.
217#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
218#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
219#[serde(deny_unknown_fields)]
220pub struct VisualProperties {
221    /// Animated normalized canvas translation, along spatial curves where its
222    /// keyframes have tangents.
223    pub position: AnimatedPoint,
224    /// Unanimated normalized point within the source rectangle.
225    pub anchor: Vec2,
226    /// Animated per-axis scale.
227    pub scale: Animated<Vec2>,
228    /// Animated clockwise rotation in degrees.
229    pub rotation_degrees: Animated<f32>,
230    /// Animated opacity constrained to `0..=1`.
231    pub opacity: Animated<f32>,
232}
233
234impl Default for VisualProperties {
235    fn default() -> Self {
236        Self {
237            position: AnimatedPoint::Constant {
238                value: Vec2::new(0.5, 0.5),
239            },
240            anchor: Vec2::new(0.5, 0.5),
241            scale: Animated::Constant {
242                value: Vec2::new(1.0, 1.0),
243            },
244            rotation_degrees: Animated::Constant { value: 0.0 },
245            opacity: Animated::Constant { value: 1.0 },
246        }
247    }
248}
249
250impl VisualProperties {
251    pub(crate) fn validate(&self) -> bool {
252        self.anchor.is_finite()
253            && valid_point_animation(&self.position)
254            && valid_vec2_animation(&self.scale)
255            && valid_scalar_animation(&self.rotation_degrees, false)
256            && valid_scalar_animation(&self.opacity, true)
257    }
258}
259
260fn valid_vec2_animation(animation: &Animated<Vec2>) -> bool {
261    match animation {
262        Animated::Constant { value } => value.is_finite(),
263        Animated::Keyframes { keyframes } => {
264            valid_keyframes(keyframes)
265                && keyframes.iter().all(|keyframe| keyframe.value.is_finite())
266        }
267    }
268}
269
270/// Valid keyframes (or a constant) whose values and tangents are finite.
271pub(crate) fn valid_point_animation(animation: &AnimatedPoint) -> bool {
272    match animation {
273        AnimatedPoint::Constant { value } => value.is_finite(),
274        AnimatedPoint::Keyframes { keyframes } => {
275            !keyframes.is_empty()
276                && keyframes.windows(2).all(|pair| pair[0].time < pair[1].time)
277                && keyframes.iter().all(|keyframe| {
278                    keyframe.interpolation.is_valid()
279                        && keyframe.value.is_finite()
280                        && keyframe.out_tangent.is_finite()
281                        && keyframe.in_tangent.is_finite()
282                })
283        }
284    }
285}
286
287fn valid_scalar_animation(animation: &Animated<f32>, unit_interval: bool) -> bool {
288    let valid_value =
289        |value: f32| value.is_finite() && (!unit_interval || (0.0..=1.0).contains(&value));
290    match animation {
291        Animated::Constant { value } => valid_value(*value),
292        Animated::Keyframes { keyframes } => {
293            valid_keyframes(keyframes)
294                && keyframes.iter().all(|keyframe| valid_value(keyframe.value))
295        }
296    }
297}
298
299/// Valid keyframes (or a constant) whose every finite value passes `check`.
300pub(crate) fn valid_scalar_values(animation: &Animated<f32>, check: impl Fn(f32) -> bool) -> bool {
301    let valid = |value: f32| value.is_finite() && check(value);
302    match animation {
303        Animated::Constant { value } => valid(*value),
304        Animated::Keyframes { keyframes } => {
305            valid_keyframes(keyframes) && keyframes.iter().all(|keyframe| valid(keyframe.value))
306        }
307    }
308}
309
310/// A non-empty, strictly ascending sequence whose interpolations are usable.
311pub(crate) fn valid_keyframes<T>(keyframes: &[Keyframe<T>]) -> bool {
312    !keyframes.is_empty()
313        && keyframes.windows(2).all(|pair| pair[0].time < pair[1].time)
314        && keyframes
315            .iter()
316            .all(|keyframe| keyframe.interpolation.is_valid())
317}