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
52impl<T> Animated<T> {
53    /// The same animation with each value mapped by `map`, keeping times and interpolations.
54    #[must_use]
55    pub fn map<U>(&self, map: impl Fn(&T) -> U) -> Animated<U> {
56        match self {
57            Self::Constant { value } => Animated::Constant { value: map(value) },
58            Self::Keyframes { keyframes } => Animated::Keyframes {
59                keyframes: keyframes
60                    .iter()
61                    .map(|keyframe| Keyframe {
62                        time: keyframe.time,
63                        value: map(&keyframe.value),
64                        interpolation: keyframe.interpolation,
65                    })
66                    .collect(),
67            },
68        }
69    }
70}
71
72/// A position that can move along curves between its keyframes.
73///
74/// Its JSON is an `Animated<Vec2>` plus optional spatial tangents per keyframe. With them,
75/// the value travels the cubic Bezier through this keyframe's `out_tangent` and the next's
76/// `in_tangent`, at the speed its timing curve sets along the arc length. Without them it
77/// moves in a straight line.
78#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
79#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
80#[serde(deny_unknown_fields)]
81#[serde(tag = "mode", rename_all = "snake_case")]
82pub enum AnimatedPoint {
83    /// A value without animation.
84    Constant {
85        /// Value used at every timestamp.
86        value: Vec2,
87    },
88    /// Values evaluated from their keyframe sequence.
89    Keyframes {
90        /// Strictly ascending keyframes. The evaluator clamps outside the sequence.
91        keyframes: Vec<PointKeyframe>,
92    },
93}
94
95impl Default for AnimatedPoint {
96    fn default() -> Self {
97        Self::Constant {
98            value: Vec2::default(),
99        }
100    }
101}
102
103impl From<Animated<Vec2>> for AnimatedPoint {
104    fn from(animation: Animated<Vec2>) -> Self {
105        match animation {
106            Animated::Constant { value } => Self::Constant { value },
107            Animated::Keyframes { keyframes } => Self::Keyframes {
108                keyframes: keyframes
109                    .into_iter()
110                    .map(|keyframe| PointKeyframe {
111                        time: keyframe.time,
112                        value: keyframe.value,
113                        interpolation: keyframe.interpolation,
114                        out_tangent: Vec2::default(),
115                        in_tangent: Vec2::default(),
116                    })
117                    .collect(),
118            },
119        }
120    }
121}
122
123impl AnimatedPoint {
124    /// Whether the value changes over time.
125    #[must_use]
126    pub const fn is_animated(&self) -> bool {
127        matches!(self, Self::Keyframes { .. })
128    }
129}
130
131/// One keyframe of an [`AnimatedPoint`].
132#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
133#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
134#[serde(deny_unknown_fields)]
135pub struct PointKeyframe {
136    /// Timestamp of this value, as a [`Keyframe`]'s.
137    pub time: alpha2_time::MediaTime,
138    /// Value at `time`.
139    pub value: Vec2,
140    /// Interpolation from this keyframe to its successor: how fast the value travels along the
141    /// way.
142    pub interpolation: Interpolation,
143    /// The curve's first control point toward the next keyframe, relative to this value.
144    #[serde(default, skip_serializing_if = "Vec2::is_zero")]
145    pub out_tangent: Vec2,
146    /// The curve's second control point from the previous keyframe, relative to this value.
147    #[serde(default, skip_serializing_if = "Vec2::is_zero")]
148    pub in_tangent: Vec2,
149}
150
151/// One timestamped control value.
152#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
153#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
154#[serde(deny_unknown_fields)]
155#[cfg_attr(feature = "schema", schemars(rename = "Keyframe_{T}"))]
156pub struct Keyframe<T> {
157    /// Timestamp of this value. Timeline time, except composition time in a composition node's
158    /// settings and picture time in a shapes or layout generator's tree.
159    pub time: alpha2_time::MediaTime,
160    /// Value at `time`.
161    pub value: T,
162    /// Interpolation from this keyframe to its successor.
163    pub interpolation: Interpolation,
164}
165
166/// Interpolation from one keyframe to the next.
167///
168/// Values interpolate numerically: a rotation from 0 to 720 degrees turns twice. A host that
169/// wants the shortest path normalizes its keyframe values.
170#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
171#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
172#[serde(tag = "type", rename_all = "snake_case", from = "InterpolationWire")]
173pub enum Interpolation {
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
182/// The wire form of [`Interpolation`]; see `EasingWire` for why unit variants are empty
183/// structs.
184#[derive(Deserialize)]
185#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
186#[cfg_attr(feature = "schema", schemars(rename = "Interpolation"))]
187#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
188pub(crate) enum InterpolationWire {
189    /// Keep this keyframe's value until the next keyframe.
190    Hold {},
191    /// Move to the next value at a constant rate.
192    Linear {},
193    /// Move to the next value along a CSS `cubic-bezier()` timing curve.
194    CubicBezier(CubicBezier),
195}
196
197impl From<InterpolationWire> for Interpolation {
198    fn from(wire: InterpolationWire) -> Self {
199        match wire {
200            InterpolationWire::Hold {} => Self::Hold,
201            InterpolationWire::Linear {} => Self::Linear,
202            InterpolationWire::CubicBezier(curve) => Self::CubicBezier(curve),
203        }
204    }
205}
206
207impl Interpolation {
208    /// The fraction of the way to the next value at linear segment `progress` (clamped to
209    /// `0..=1`), or `None` while holding. A cubic Bezier may overshoot `0..=1`.
210    #[must_use]
211    pub fn amount(self, progress: f64) -> Option<f64> {
212        match self {
213            Self::Hold => None,
214            Self::Linear => Some(Easing::Linear.apply(progress)),
215            Self::CubicBezier(curve) => Some(curve.apply(progress)),
216        }
217    }
218
219    /// Whether the interpolation's parameters are usable.
220    #[must_use]
221    pub fn is_valid(self) -> bool {
222        match self {
223            Self::Hold | Self::Linear => true,
224            Self::CubicBezier(curve) => curve.is_valid(),
225        }
226    }
227}
228
229/// Resolution-independent visual presentation controls for a clip.
230#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
231#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
232#[serde(deny_unknown_fields)]
233///
234/// Omitted fields default to the source centred on the canvas, unscaled, unrotated and opaque.
235pub struct VisualProperties {
236    /// Animated normalized canvas translation, along spatial curves where keyframes have
237    /// tangents.
238    #[serde(default = "centre_point")]
239    pub position: AnimatedPoint,
240    /// Unanimated normalized point within the source rectangle.
241    #[serde(default = "centre")]
242    pub anchor: Vec2,
243    /// Animated per-axis scale.
244    #[serde(default = "unit_scale")]
245    pub scale: Animated<Vec2>,
246    /// Animated clockwise rotation in degrees.
247    #[serde(default = "no_rotation")]
248    pub rotation_degrees: Animated<f32>,
249    /// Animated opacity constrained to `0..=1`.
250    #[serde(default = "opaque")]
251    pub opacity: Animated<f32>,
252}
253
254impl Default for VisualProperties {
255    fn default() -> Self {
256        Self {
257            position: centre_point(),
258            anchor: centre(),
259            scale: unit_scale(),
260            rotation_degrees: no_rotation(),
261            opacity: opaque(),
262        }
263    }
264}
265
266const fn centre() -> Vec2 {
267    Vec2::new(0.5, 0.5)
268}
269
270const fn centre_point() -> AnimatedPoint {
271    AnimatedPoint::Constant { value: centre() }
272}
273
274pub(crate) const fn unit_scale() -> Animated<Vec2> {
275    Animated::Constant {
276        value: Vec2::new(1.0, 1.0),
277    }
278}
279
280const fn no_rotation() -> Animated<f32> {
281    Animated::Constant { value: 0.0 }
282}
283
284pub(crate) const fn opaque() -> Animated<f32> {
285    Animated::Constant { value: 1.0 }
286}
287
288/// Calls `visit` with `at` and the times of `animated`'s keyframes, if any.
289pub(crate) fn visit_keyed<T>(
290    at: &str,
291    animated: &Animated<T>,
292    visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime]),
293) {
294    if let Animated::Keyframes { keyframes } = animated {
295        let times = keyframes
296            .iter()
297            .map(|keyframe| keyframe.time)
298            .collect::<Vec<_>>();
299        visit(at, &times);
300    }
301}
302
303/// [`visit_keyed`] for a point along a spatial curve.
304pub(crate) fn visit_keyed_point(
305    at: &str,
306    animated: &AnimatedPoint,
307    visit: &mut impl FnMut(&str, &[alpha2_time::MediaTime]),
308) {
309    if let AnimatedPoint::Keyframes { keyframes } = animated {
310        let times = keyframes
311            .iter()
312            .map(|keyframe| keyframe.time)
313            .collect::<Vec<_>>();
314        visit(at, &times);
315    }
316}
317
318impl VisualProperties {
319    pub(crate) fn validate(&self) -> bool {
320        self.anchor.is_finite()
321            && valid_point_animation(&self.position)
322            && valid_vec2_animation(&self.scale)
323            && valid_scalar_animation(&self.rotation_degrees, false)
324            && valid_scalar_animation(&self.opacity, true)
325    }
326}
327
328fn valid_vec2_animation(animation: &Animated<Vec2>) -> bool {
329    match animation {
330        Animated::Constant { value } => value.is_finite(),
331        Animated::Keyframes { keyframes } => {
332            valid_keyframes(keyframes)
333                && keyframes.iter().all(|keyframe| keyframe.value.is_finite())
334        }
335    }
336}
337
338/// Valid keyframes (or a constant) whose values and tangents are finite.
339pub(crate) fn valid_point_animation(animation: &AnimatedPoint) -> bool {
340    match animation {
341        AnimatedPoint::Constant { value } => value.is_finite(),
342        AnimatedPoint::Keyframes { keyframes } => {
343            !keyframes.is_empty()
344                && keyframes.windows(2).all(|pair| pair[0].time < pair[1].time)
345                && keyframes.iter().all(|keyframe| {
346                    keyframe.interpolation.is_valid()
347                        && keyframe.value.is_finite()
348                        && keyframe.out_tangent.is_finite()
349                        && keyframe.in_tangent.is_finite()
350                })
351        }
352    }
353}
354
355fn valid_scalar_animation(animation: &Animated<f32>, unit_interval: bool) -> bool {
356    let valid_value =
357        |value: f32| value.is_finite() && (!unit_interval || (0.0..=1.0).contains(&value));
358    match animation {
359        Animated::Constant { value } => valid_value(*value),
360        Animated::Keyframes { keyframes } => {
361            valid_keyframes(keyframes)
362                && keyframes.iter().all(|keyframe| valid_value(keyframe.value))
363        }
364    }
365}
366
367/// Valid keyframes (or a constant) whose every finite value passes `check`.
368pub(crate) fn valid_scalar_values(animation: &Animated<f32>, check: impl Fn(f32) -> bool) -> bool {
369    let valid = |value: f32| value.is_finite() && check(value);
370    match animation {
371        Animated::Constant { value } => valid(*value),
372        Animated::Keyframes { keyframes } => {
373            valid_keyframes(keyframes) && keyframes.iter().all(|keyframe| valid(keyframe.value))
374        }
375    }
376}
377
378/// A non-empty, strictly ascending sequence whose interpolations are usable.
379pub(crate) fn valid_keyframes<T>(keyframes: &[Keyframe<T>]) -> bool {
380    !keyframes.is_empty()
381        && keyframes.windows(2).all(|pair| pair[0].time < pair[1].time)
382        && keyframes
383            .iter()
384            .all(|keyframe| keyframe.interpolation.is_valid())
385}