ffcine_reframe/
keyframe.rs1use std::f64::consts::PI;
10
11use ffcine_core::{Error, Result};
12use serde_json::Value;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum Interp {
17 #[default]
18 Linear,
19 Ease,
21 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 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
46pub fn ease(u: f64) -> f64 {
48 0.5 - 0.5 * (PI * u.clamp(0.0, 1.0)).cos()
49}
50
51pub 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 pub t: f64,
82 pub v: T,
83 pub interp: Interp,
84}
85
86#[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
99pub 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
109pub 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 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 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 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 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 assert!((at(2.9) - 2.0).abs() < 1e-12);
202 assert!((at(3.0) - 4.0).abs() < 1e-12);
203 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 assert!(ease(1e-4) < 1e-7 && 1.0 - ease(1.0 - 1e-4) < 1e-7);
219 }
220}