Skip to main content

ffcine_reframe/
keyframe.rs

1//! The one keyframe form, `[{"t": RationalTime, "v": value, "interp": "linear" | "ease" | "hold"}]`
2//! (`docs/timeline.md#effect-schemas`), evaluated in Rust per frame: a thin wrapper over
3//! [`ffcine_core::anim`]'s tracks on [`Clock::Source`].
4//!
5//! `t` is a `RationalTime` (or plain seconds) in the clip's source time (`source_range.start_time`
6//! plus the time since the clip's first frame, `docs/timeline.md`). `interp` (or `ease`) shapes
7//! the segment from that key to the next, with any [`Interp`] (the text keyframes' set). Before
8//! the first key the first value holds, after the last the last. A value may also be an
9//! expression over the source time `st` (`docs/compositing.md#expressions`).
10
11use ffcine_core::Result;
12use ffcine_core::anim::{Animatable, Clock, Track, keys_at};
13use serde_json::Value;
14
15/// How a segment moves from one key to the next (`ffcine_core::anim::Ease`).
16pub use ffcine_core::anim::Ease as Interp;
17pub use ffcine_core::anim::{Key, is_keys, seconds};
18
19/// A value that can be keyframed.
20pub use ffcine_core::anim::Animatable as Lerp;
21
22/// `0.5 - 0.5 cos(pi u)`.
23pub fn ease(u: f64) -> f64 {
24    Interp::Ease.at(u)
25}
26
27/// A parameter: a constant, keys sorted by time, or an expression track.
28#[derive(Debug, Clone, PartialEq)]
29pub enum Keyed<T> {
30    Const(T),
31    Keys(Vec<Key<T>>),
32    /// An expression, or keys under one (`{"keys", "expr"}`).
33    Track(Track<T>),
34}
35
36impl<T: Default> Default for Keyed<T> {
37    fn default() -> Self {
38        Self::Const(T::default())
39    }
40}
41
42impl<T: Animatable> Keyed<T> {
43    /// A constant, the keyframe form or an expression; `what` names the parameter in errors.
44    pub fn parse(v: &Value, what: &'static str) -> Result<Self> {
45        Ok(match Track::parse(v, Clock::Source, what)? {
46            Track::Const(c) => Self::Const(c),
47            Track::Keys(k) => Self::Keys(k),
48            t => Self::Track(t),
49        })
50    }
51
52    /// Keys in any order (sorted here, stable for equal times).
53    pub fn from_keys(mut keys: Vec<Key<T>>) -> Self {
54        keys.sort_by(|a, b| a.t.total_cmp(&b.t));
55        Self::Keys(keys)
56    }
57
58    pub fn is_animated(&self) -> bool {
59        match self {
60            Self::Const(_) => false,
61            Self::Keys(k) => k.len() > 1,
62            Self::Track(t) => t.is_animated(),
63        }
64    }
65
66    /// The value at source time `t` (seconds); `None` for no keys, or an expression that isn't
67    /// finite there.
68    pub fn eval(&self, t: f64) -> Option<T> {
69        self.try_eval(t).ok().flatten()
70    }
71
72    /// [`Keyed::eval`], with an expression's error.
73    pub fn try_eval(&self, t: f64) -> Result<Option<T>> {
74        Ok(match self {
75            Self::Const(v) => Some(*v),
76            Self::Keys(k) => keys_at(k, t),
77            Self::Track(tr) => tr.try_eval(
78                Clock::Source,
79                &ffcine_core::anim::Frame::at(Clock::Source, t, 0.0),
80            )?,
81        })
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use serde_json::json;
89
90    fn rt(value: f64) -> Value {
91        json!({"OTIO_SCHEMA": "RationalTime.1", "rate": 24.0, "value": value})
92    }
93
94    #[test]
95    fn parse_and_eval() {
96        let k: Keyed<f64> = Keyed::parse(
97            &json!([
98                {"t": rt(48.0), "v": 2.0, "interp": "hold"},
99                {"t": rt(0.0), "v": 1.0, "interp": "linear"},
100                {"t": rt(72.0), "v": 4.0, "interp": "ease"},
101                {"t": rt(96.0), "v": 0.0},
102            ]),
103            "zoom",
104        )
105        .unwrap();
106        assert!(k.is_animated());
107        let at = |t| k.eval(t).unwrap();
108        assert!((at(-1.0) - 1.0).abs() < 1e-12);
109        assert!((at(1.0) - 1.5).abs() < 1e-12);
110        // Hold until the next key, then jump.
111        assert!((at(2.9) - 2.0).abs() < 1e-12);
112        assert!((at(3.0) - 4.0).abs() < 1e-12);
113        // Ease is symmetric about the segment's middle.
114        assert!((at(3.5) - 2.0).abs() < 1e-12);
115        assert!((at(3.25) - (4.0 - 4.0 * ease(0.25))).abs() < 1e-12);
116        assert!((at(10.0) - 0.0).abs() < 1e-12);
117
118        let c: Keyed<[f64; 2]> = Keyed::parse(&json!([0.25, 0.5]), "centre").unwrap();
119        assert_eq!(c.eval(3.0), Some([0.25, 0.5]));
120        assert!(Keyed::<f64>::parse(&json!("x"), "zoom").is_err());
121        assert!(Keyed::<f64>::parse(&json!([{"t": 0, "v": 1, "interp": "bounce"}]), "z").is_err());
122    }
123
124    #[test]
125    fn ease_is_smooth() {
126        assert!(ease(0.0).abs() < 1e-15 && (ease(1.0) - 1.0).abs() < 1e-15);
127        // Zero slope at both ends.
128        assert!(ease(1e-4) < 1e-7 && 1.0 - ease(1.0 - 1e-4) < 1e-7);
129    }
130
131    #[test]
132    fn expressions_over_source_time() {
133        let k: Keyed<f64> = Keyed::parse(&json!("1 + st / 10"), "zoom").unwrap();
134        assert!(k.is_animated());
135        assert!((k.eval(5.0).unwrap() - 1.5).abs() < 1e-12);
136        let c: Keyed<[f64; 2]> =
137            Keyed::parse(&json!([0.25, "0.5 + 0.1 * sin(st)"]), "centre").unwrap();
138        let v = c.eval(1.0).unwrap();
139        assert!((v[0] - 0.25).abs() < 1e-12 && (v[1] - (0.5 + 0.1 * 1f64.sin())).abs() < 1e-12);
140        let ke: Keyed<f64> = Keyed::parse(
141            &json!({"keys": [{"t": 0, "v": 1}, {"t": 2, "v": 3}], "expr": "value * 2"}),
142            "zoom",
143        )
144        .unwrap();
145        assert!((ke.eval(1.0).unwrap() - 4.0).abs() < 1e-12);
146        // Only the source time is known in a timeline effect.
147        assert!(Keyed::<f64>::parse(&json!("t * 2"), "zoom").is_err());
148        let nan: Keyed<f64> = Keyed::parse(&json!("log(st)"), "zoom").unwrap();
149        assert!(nan.eval(-1.0).is_none() && nan.try_eval(-1.0).is_err());
150        // A constant expression folds.
151        assert_eq!(
152            Keyed::<f64>::parse(&json!("2 * 0.5"), "zoom").unwrap(),
153            Keyed::Const(1.0)
154        );
155    }
156}