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.
3//!
4//! `t` is a `RationalTime` (or plain seconds) in the clip's source time (`source_range.start_time`
5//! plus the time since the clip's first frame, `docs/timeline.md`). `interp` shapes the segment
6//! from that key to the next. Before the first key the first value holds, after the last
7//! the last.
8
9use std::f64::consts::PI;
10
11use ffcine_core::{Error, Result};
12use serde_json::Value;
13
14/// How a segment moves from one key to the next.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum Interp {
17    #[default]
18    Linear,
19    /// Cosine ease in and out, `0.5 - 0.5 cos(pi u)`: the prototype's `crop_x` pan curve.
20    Ease,
21    /// Keep this key's value until the next key.
22    Hold,
23}
24
25impl Interp {
26    pub fn parse(s: &str) -> Option<Self> {
27        match s {
28            "linear" => Some(Self::Linear),
29            "ease" => Some(Self::Ease),
30            "hold" => Some(Self::Hold),
31            _ => None,
32        }
33    }
34
35    /// The segment's progress at `u` (0..1 of the way to the next key).
36    pub fn shape(self, u: f64) -> f64 {
37        let u = u.clamp(0.0, 1.0);
38        match self {
39            Self::Linear => u,
40            Self::Ease => ease(u),
41            Self::Hold => 0.0,
42        }
43    }
44}
45
46/// `0.5 - 0.5 cos(pi u)`.
47pub fn ease(u: f64) -> f64 {
48    0.5 - 0.5 * (PI * u.clamp(0.0, 1.0)).cos()
49}
50
51/// A value that can be keyframed.
52pub trait Lerp: Copy {
53    fn lerp(a: Self, b: Self, k: f64) -> Self;
54    fn from_json(v: &Value) -> Option<Self>;
55}
56
57impl Lerp for f64 {
58    fn lerp(a: Self, b: Self, k: f64) -> Self {
59        a + (b - a) * k
60    }
61    fn from_json(v: &Value) -> Option<Self> {
62        v.as_f64()
63    }
64}
65
66impl Lerp for [f64; 2] {
67    fn lerp(a: Self, b: Self, k: f64) -> Self {
68        [f64::lerp(a[0], b[0], k), f64::lerp(a[1], b[1], k)]
69    }
70    fn from_json(v: &Value) -> Option<Self> {
71        match v.as_array()?.as_slice() {
72            [x, y] => Some([x.as_f64()?, y.as_f64()?]),
73            _ => None,
74        }
75    }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq)]
79pub struct Key<T> {
80    /// Seconds of source time.
81    pub t: f64,
82    pub v: T,
83    pub interp: Interp,
84}
85
86/// A parameter: a constant, or keys sorted by time.
87#[derive(Debug, Clone, PartialEq)]
88pub enum Keyed<T> {
89    Const(T),
90    Keys(Vec<Key<T>>),
91}
92
93impl<T: Default> Default for Keyed<T> {
94    fn default() -> Self {
95        Self::Const(T::default())
96    }
97}
98
99/// Seconds from a `RationalTime` (`{rate, value}`) or a plain number.
100pub fn seconds(v: &Value) -> Option<f64> {
101    if let Some(s) = v.as_f64() {
102        return Some(s);
103    }
104    let rate = v.get("rate")?.as_f64()?;
105    let value = v.get("value")?.as_f64()?;
106    (rate > 0.0).then(|| value / rate)
107}
108
109/// Whether `v` is the keyframe form (an array of `{t, v}` objects).
110pub fn is_keys(v: &Value) -> bool {
111    v.as_array()
112        .and_then(|a| a.first())
113        .is_some_and(|k| k.get("t").is_some() && k.get("v").is_some())
114}
115
116impl<T: Lerp> Keyed<T> {
117    /// A constant or the keyframe form; `what` names the parameter in errors.
118    pub fn parse(v: &Value, what: &'static str) -> Result<Self> {
119        let invalid = || Error::Invalid {
120            what,
121            value: v.to_string(),
122        };
123        if !is_keys(v) {
124            return T::from_json(v).map(Self::Const).ok_or_else(invalid);
125        }
126        let mut keys = Vec::new();
127        for k in v.as_array().into_iter().flatten() {
128            let interp = match k.get("interp").and_then(Value::as_str) {
129                None => Interp::Linear,
130                Some(s) => Interp::parse(s).ok_or_else(invalid)?,
131            };
132            keys.push(Key {
133                t: k.get("t").and_then(seconds).ok_or_else(invalid)?,
134                v: k.get("v").and_then(T::from_json).ok_or_else(invalid)?,
135                interp,
136            });
137        }
138        Ok(Self::from_keys(keys))
139    }
140
141    /// Keys in any order (sorted here, stable for equal times).
142    pub fn from_keys(mut keys: Vec<Key<T>>) -> Self {
143        keys.sort_by(|a, b| a.t.total_cmp(&b.t));
144        Self::Keys(keys)
145    }
146
147    pub fn is_animated(&self) -> bool {
148        matches!(self, Self::Keys(k) if k.len() > 1)
149    }
150
151    /// The value at source time `t` (seconds).
152    pub fn eval(&self, t: f64) -> Option<T> {
153        let keys = match self {
154            Self::Const(v) => return Some(*v),
155            Self::Keys(k) => k,
156        };
157        let first = keys.first()?;
158        if t <= first.t {
159            return Some(first.v);
160        }
161        // The last key at or before t.
162        let i = keys.partition_point(|k| k.t <= t) - 1;
163        let a = &keys[i];
164        let Some(b) = keys.get(i + 1) else {
165            return Some(a.v);
166        };
167        let span = b.t - a.t;
168        if span <= 0.0 {
169            return Some(b.v);
170        }
171        Some(T::lerp(a.v, b.v, a.interp.shape((t - a.t) / span)))
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use serde_json::json;
179
180    fn rt(value: f64) -> Value {
181        json!({"OTIO_SCHEMA": "RationalTime.1", "rate": 24.0, "value": value})
182    }
183
184    #[test]
185    fn parse_and_eval() {
186        let k: Keyed<f64> = Keyed::parse(
187            &json!([
188                {"t": rt(48.0), "v": 2.0, "interp": "hold"},
189                {"t": rt(0.0), "v": 1.0, "interp": "linear"},
190                {"t": rt(72.0), "v": 4.0, "interp": "ease"},
191                {"t": rt(96.0), "v": 0.0},
192            ]),
193            "zoom",
194        )
195        .unwrap();
196        assert!(k.is_animated());
197        let at = |t| k.eval(t).unwrap();
198        assert!((at(-1.0) - 1.0).abs() < 1e-12);
199        assert!((at(1.0) - 1.5).abs() < 1e-12);
200        // Hold until the next key, then jump.
201        assert!((at(2.9) - 2.0).abs() < 1e-12);
202        assert!((at(3.0) - 4.0).abs() < 1e-12);
203        // Ease is symmetric about the segment's middle.
204        assert!((at(3.5) - 2.0).abs() < 1e-12);
205        assert!((at(3.25) - (4.0 - 4.0 * ease(0.25))).abs() < 1e-12);
206        assert!((at(10.0) - 0.0).abs() < 1e-12);
207
208        let c: Keyed<[f64; 2]> = Keyed::parse(&json!([0.25, 0.5]), "centre").unwrap();
209        assert_eq!(c.eval(3.0), Some([0.25, 0.5]));
210        assert!(Keyed::<f64>::parse(&json!("x"), "zoom").is_err());
211        assert!(Keyed::<f64>::parse(&json!([{"t": 0, "v": 1, "interp": "bounce"}]), "z").is_err());
212    }
213
214    #[test]
215    fn ease_is_smooth() {
216        assert!(ease(0.0).abs() < 1e-15 && (ease(1.0) - 1.0).abs() < 1e-15);
217        // Zero slope at both ends.
218        assert!(ease(1e-4) < 1e-7 && 1.0 - ease(1.0 - 1e-4) < 1e-7);
219    }
220}