lottie_core/
animatable.rs

1use glam::{Vec2, Vec3, Vec4};
2use lottie_data::model::{BezierPath, Property, TextDocument, Value};
3#[cfg(feature = "expressions")]
4use crate::expressions::ExpressionEvaluator;
5#[cfg(feature = "expressions")]
6use boa_engine::{JsValue, js_string};
7
8pub trait Interpolatable: Sized + Clone {
9    fn lerp(&self, other: &Self, t: f32) -> Self;
10
11    fn lerp_spatial(
12        &self,
13        other: &Self,
14        t: f32,
15        _tan_in: Option<&Vec<f32>>,
16        _tan_out: Option<&Vec<f32>>,
17    ) -> Self {
18        self.lerp(other, t)
19    }
20}
21
22impl Interpolatable for TextDocument {
23    fn lerp(&self, other: &Self, t: f32) -> Self {
24        if t < 1.0 {
25            self.clone()
26        } else {
27            other.clone()
28        }
29    }
30}
31
32impl Interpolatable for BezierPath {
33    fn lerp(&self, other: &Self, t: f32) -> Self {
34        if t < 1.0 {
35            self.clone()
36        } else {
37            other.clone()
38        }
39    }
40}
41
42impl Interpolatable for f32 {
43    fn lerp(&self, other: &Self, t: f32) -> Self {
44        self + (other - self) * t
45    }
46}
47
48impl Interpolatable for Vec2 {
49    fn lerp(&self, other: &Self, t: f32) -> Self {
50        Vec2::lerp(*self, *other, t)
51    }
52
53    fn lerp_spatial(
54        &self,
55        other: &Self,
56        t: f32,
57        tan_in: Option<&Vec<f32>>,
58        tan_out: Option<&Vec<f32>>,
59    ) -> Self {
60        let p0 = *self;
61        let p3 = *other;
62
63        let t_out = if let Some(to) = tan_out {
64            if to.len() >= 2 {
65                Vec2::new(to[0], to[1])
66            } else {
67                Vec2::ZERO
68            }
69        } else {
70            Vec2::ZERO
71        };
72
73        let t_in = if let Some(ti) = tan_in {
74            if ti.len() >= 2 {
75                Vec2::new(ti[0], ti[1])
76            } else {
77                Vec2::ZERO
78            }
79        } else {
80            Vec2::ZERO
81        };
82
83        let p1 = p0 + t_out;
84        let p2 = p3 + t_in;
85
86        let one_minus_t = 1.0 - t;
87        let one_minus_t_sq = one_minus_t * one_minus_t;
88        let one_minus_t_cub = one_minus_t_sq * one_minus_t;
89
90        let t_sq = t * t;
91        let t_cub = t_sq * t;
92
93        p0 * one_minus_t_cub
94            + p1 * 3.0 * one_minus_t_sq * t
95            + p2 * 3.0 * one_minus_t * t_sq
96            + p3 * t_cub
97    }
98}
99
100impl Interpolatable for Vec3 {
101    fn lerp(&self, other: &Self, t: f32) -> Self {
102        Vec3::lerp(*self, *other, t)
103    }
104
105    fn lerp_spatial(
106        &self,
107        other: &Self,
108        t: f32,
109        tan_in: Option<&Vec<f32>>,
110        tan_out: Option<&Vec<f32>>,
111    ) -> Self {
112        let p0 = *self;
113        let p3 = *other;
114
115        let t_out = if let Some(to) = tan_out {
116            if to.len() >= 3 {
117                Vec3::new(to[0], to[1], to[2])
118            } else if to.len() >= 2 {
119                Vec3::new(to[0], to[1], 0.0)
120            } else {
121                Vec3::ZERO
122            }
123        } else {
124            Vec3::ZERO
125        };
126
127        let t_in = if let Some(ti) = tan_in {
128            if ti.len() >= 3 {
129                Vec3::new(ti[0], ti[1], ti[2])
130            } else if ti.len() >= 2 {
131                Vec3::new(ti[0], ti[1], 0.0)
132            } else {
133                Vec3::ZERO
134            }
135        } else {
136            Vec3::ZERO
137        };
138
139        let p1 = p0 + t_out;
140        let p2 = p3 + t_in;
141
142        let one_minus_t = 1.0 - t;
143        let one_minus_t_sq = one_minus_t * one_minus_t;
144        let one_minus_t_cub = one_minus_t_sq * one_minus_t;
145
146        let t_sq = t * t;
147        let t_cub = t_sq * t;
148
149        p0 * one_minus_t_cub
150            + p1 * 3.0 * one_minus_t_sq * t
151            + p2 * 3.0 * one_minus_t * t_sq
152            + p3 * t_cub
153    }
154}
155
156impl Interpolatable for Vec4 {
157    fn lerp(&self, other: &Self, t: f32) -> Self {
158        Vec4::lerp(*self, *other, t)
159    }
160}
161
162// For gradient colors (Vec<f32>)
163impl Interpolatable for Vec<f32> {
164    fn lerp(&self, other: &Self, t: f32) -> Self {
165        self.iter()
166            .zip(other.iter())
167            .map(|(a, b)| a + (b - a) * t)
168            .collect()
169    }
170}
171
172// Helper to convert Interpolatable to JS Value
173#[cfg(feature = "expressions")]
174pub trait ToJsValue {
175    fn to_js_value(&self, context: &mut boa_engine::Context) -> JsValue;
176    fn from_js_value(v: &JsValue, context: &mut boa_engine::Context) -> Option<Self> where Self: Sized;
177}
178
179#[cfg(not(feature = "expressions"))]
180pub trait ToJsValue {}
181
182#[cfg(not(feature = "expressions"))]
183impl<T> ToJsValue for T {}
184
185#[cfg(feature = "expressions")]
186impl ToJsValue for f32 {
187    fn to_js_value(&self, _context: &mut boa_engine::Context) -> JsValue {
188        JsValue::new(*self)
189    }
190    fn from_js_value(v: &JsValue, context: &mut boa_engine::Context) -> Option<Self> {
191        v.to_number(context).ok().map(|n| n as f32)
192    }
193}
194
195#[cfg(feature = "expressions")]
196impl ToJsValue for Vec<f32> {
197    fn to_js_value(&self, context: &mut boa_engine::Context) -> JsValue {
198        let vals: Vec<JsValue> = self.iter().map(|f| JsValue::new(*f)).collect();
199        boa_engine::object::builtins::JsArray::from_iter(vals, context).into()
200    }
201     fn from_js_value(v: &JsValue, context: &mut boa_engine::Context) -> Option<Self> {
202        if let Some(obj) = v.as_object() {
203            if obj.is_array() {
204                if let Ok(len_val) = obj.get(js_string!("length"), context) {
205                    if let Ok(len) = len_val.to_number(context) {
206                        let len_u64 = len as u64;
207                        let mut vec = Vec::with_capacity(len_u64 as usize);
208                        for i in 0..len_u64 {
209                            if let Ok(val) = obj.get(i, context) {
210                                if let Ok(n) = val.to_number(context) {
211                                    vec.push(n as f32);
212                                }
213                            }
214                        }
215                        return Some(vec);
216                    }
217                }
218            }
219        }
220        None
221    }
222}
223
224#[cfg(feature = "expressions")]
225impl ToJsValue for Vec2 {
226    fn to_js_value(&self, context: &mut boa_engine::Context) -> JsValue {
227        let vals = vec![JsValue::new(self.x), JsValue::new(self.y)];
228        boa_engine::object::builtins::JsArray::from_iter(vals, context).into()
229    }
230    fn from_js_value(v: &JsValue, context: &mut boa_engine::Context) -> Option<Self> {
231        if let Some(obj) = v.as_object() {
232            if obj.is_array() {
233                let x = obj.get(0, context).ok()?.to_number(context).ok()? as f32;
234                let y = obj.get(1, context).ok()?.to_number(context).ok()? as f32;
235                return Some(Vec2::new(x, y));
236            }
237        }
238        None
239    }
240}
241
242#[cfg(feature = "expressions")]
243impl ToJsValue for Vec3 {
244    fn to_js_value(&self, context: &mut boa_engine::Context) -> JsValue {
245        let vals = vec![JsValue::new(self.x), JsValue::new(self.y), JsValue::new(self.z)];
246        boa_engine::object::builtins::JsArray::from_iter(vals, context).into()
247    }
248    fn from_js_value(v: &JsValue, context: &mut boa_engine::Context) -> Option<Self> {
249        if let Some(obj) = v.as_object() {
250            if obj.is_array() {
251                let x = obj.get(0, context).ok()?.to_number(context).ok()? as f32;
252                let y = obj.get(1, context).ok()?.to_number(context).ok()? as f32;
253                let z = obj.get(2, context).ok()?.to_number(context).ok()? as f32;
254                return Some(Vec3::new(x, y, z));
255            }
256        }
257        None
258    }
259}
260
261#[cfg(feature = "expressions")]
262impl ToJsValue for Vec4 {
263    fn to_js_value(&self, context: &mut boa_engine::Context) -> JsValue {
264        let vals = vec![JsValue::new(self.x), JsValue::new(self.y), JsValue::new(self.z), JsValue::new(self.w)];
265        boa_engine::object::builtins::JsArray::from_iter(vals, context).into()
266    }
267    fn from_js_value(v: &JsValue, context: &mut boa_engine::Context) -> Option<Self> {
268        if let Some(obj) = v.as_object() {
269            if obj.is_array() {
270                let x = obj.get(0, context).ok()?.to_number(context).ok()? as f32;
271                let y = obj.get(1, context).ok()?.to_number(context).ok()? as f32;
272                let z = obj.get(2, context).ok()?.to_number(context).ok()? as f32;
273                let w = obj.get(3, context).ok()?.to_number(context).ok()? as f32;
274                return Some(Vec4::new(x, y, z, w));
275            }
276        }
277        None
278    }
279}
280
281#[cfg(feature = "expressions")]
282impl ToJsValue for BezierPath {
283    fn to_js_value(&self, _context: &mut boa_engine::Context) -> JsValue {
284        JsValue::Undefined
285    }
286    fn from_js_value(_v: &JsValue, _context: &mut boa_engine::Context) -> Option<Self> {
287        None
288    }
289}
290
291#[cfg(feature = "expressions")]
292impl ToJsValue for TextDocument {
293    fn to_js_value(&self, _context: &mut boa_engine::Context) -> JsValue {
294        JsValue::Undefined
295    }
296    fn from_js_value(_v: &JsValue, _context: &mut boa_engine::Context) -> Option<Self> {
297        None
298    }
299}
300
301// Cubic Bezier Easing
302pub fn solve_cubic_bezier(p1: Vec2, p2: Vec2, x: f32) -> f32 {
303    if x <= 0.0 {
304        return 0.0;
305    }
306    if x >= 1.0 {
307        return 1.0;
308    }
309
310    // Newton-Raphson
311    let mut t = x;
312    for _ in 0..8 {
313        let one_minus_t = 1.0 - t;
314        let x_est = 3.0 * one_minus_t * one_minus_t * t * p1.x
315            + 3.0 * one_minus_t * t * t * p2.x
316            + t * t * t;
317
318        let err = x_est - x;
319        if err.abs() < 1e-4 {
320            break;
321        }
322
323        let dx_dt = 3.0 * one_minus_t * one_minus_t * p1.x
324            + 6.0 * one_minus_t * t * (p2.x - p1.x)
325            + 3.0 * t * t * (1.0 - p2.x);
326
327        if dx_dt.abs() < 1e-6 {
328            break;
329        }
330        t -= err / dx_dt;
331    }
332
333    let one_minus_t = 1.0 - t;
334    3.0 * one_minus_t * one_minus_t * t * p1.y + 3.0 * one_minus_t * t * t * p2.y + t * t * t
335}
336
337pub struct Animator;
338
339impl Animator {
340    pub fn resolve<T, U>(
341        prop: &Property<T>,
342        frame: f32,
343        converter: impl Fn(&T) -> U,
344        default: U,
345        #[cfg(feature = "expressions")] evaluator: Option<&mut ExpressionEvaluator>,
346        #[cfg(not(feature = "expressions"))] _evaluator: Option<&mut ()>, // Dummy type
347        frame_rate: f32,
348    ) -> U
349    where
350        U: Interpolatable + 'static + ToJsValue,
351    {
352        // 1. Calculate Base Value (Keyframes)
353        let base_value = Self::resolve_keyframes(prop, frame, &converter, default.clone());
354
355        // 2. Expression Check
356        #[cfg(feature = "expressions")]
357        if let Some(expr) = &prop.x {
358            if let Some(eval) = evaluator {
359                 let time = frame / frame_rate; // Seconds
360
361                 // Calculate Loop Value (pre-calc logic for loopOut("cycle"))
362                 let loop_value = if let Value::Animated(keyframes) = &prop.k {
363                     if !keyframes.is_empty() {
364                         let first_t = keyframes[0].t;
365                         let last_t = keyframes[keyframes.len() - 1].t;
366                         let duration = last_t - first_t;
367
368                         if duration > 0.0 && frame > last_t {
369                             let t_since_end = frame - last_t;
370                             let cycle_offset = t_since_end % duration;
371                             let cycle_frame = first_t + cycle_offset;
372                             Self::resolve_keyframes(prop, cycle_frame, &converter, default.clone())
373                         } else {
374                             base_value.clone()
375                         }
376                     } else {
377                         base_value.clone()
378                     }
379                 } else {
380                     base_value.clone()
381                 };
382
383                 let (js_val, js_loop_val) = {
384                     let ctx = eval.context();
385                     (base_value.to_js_value(ctx), loop_value.to_js_value(ctx))
386                 };
387
388                 match eval.evaluate(expr, &js_val, &js_loop_val, time, frame_rate) {
389                     Ok(res) => {
390                          let context = eval.context();
391                          if let Some(val) = U::from_js_value(&res, context) {
392                               return val;
393                          }
394                     },
395                     Err(_e) => {
396                         // eprintln!("Expression failed: {}", _e);
397                     }
398                 }
399            }
400        }
401
402        base_value
403    }
404
405    fn resolve_keyframes<T, U>(
406        prop: &Property<T>,
407        frame: f32,
408        converter: &impl Fn(&T) -> U,
409        default: U,
410    ) -> U
411    where
412        U: Interpolatable,
413    {
414         match &prop.k {
415            Value::Default => default,
416            Value::Static(v) => converter(v),
417            Value::Animated(keyframes) => {
418                if keyframes.is_empty() {
419                    return default;
420                }
421
422                // Optimization: Binary Search
423                // Find the first keyframe where kf.t > frame
424                // 'idx' will be the index of that keyframe.
425                // The current segment is between idx-1 and idx.
426                let idx = keyframes.partition_point(|kf| kf.t <= frame);
427
428                // If idx == 0, then all keyframes have t > frame. frame is before start.
429                if idx == 0 {
430                    if let Some(s) = &keyframes[0].s {
431                        return converter(s);
432                    }
433                    return default;
434                }
435
436                let len = keyframes.len();
437                // If idx == len, then all keyframes have t <= frame. frame is after end (or exactly at end).
438                if idx >= len {
439                     let last = &keyframes[len - 1];
440                     // Use end value if present, else start value
441                     if let Some(e) = &last.e {
442                         return converter(e);
443                     }
444                     if let Some(s) = &last.s {
445                         return converter(s);
446                     }
447                     return default;
448                }
449
450                // Segment is [idx-1, idx]
451                let kf_start = &keyframes[idx - 1];
452                let kf_end = &keyframes[idx];
453
454                let start_val = kf_start
455                    .s
456                    .as_ref()
457                    .map(|v| converter(v))
458                    .unwrap_or(default.clone());
459
460                // End value logic
461                let end_val = kf_start
462                    .e
463                    .as_ref()
464                    .map(|v| converter(v))
465                    .or_else(|| kf_end.s.as_ref().map(|v| converter(v)))
466                    .unwrap_or(start_val.clone());
467
468                let duration = kf_end.t - kf_start.t;
469                if duration <= 0.0 {
470                    return start_val;
471                }
472
473                let mut local_t = (frame - kf_start.t) / duration;
474
475                // Easing
476                let p1 = if let Some(o) = kf_start.o {
477                    Vec2::new(o[0], o[1])
478                } else {
479                    Vec2::new(0.0, 0.0)
480                };
481                let p2 = if let Some(i) = kf_end.i {
482                    Vec2::new(i[0], i[1])
483                } else {
484                    Vec2::new(1.0, 1.0)
485                };
486
487                // If Hold keyframe
488                if let Some(h) = kf_start.h {
489                    if h == 1 {
490                        return start_val;
491                    }
492                }
493
494                local_t = solve_cubic_bezier(p1, p2, local_t);
495
496                start_val.lerp_spatial(
497                    &end_val,
498                    local_t,
499                    kf_end.ti.as_ref(),
500                    kf_start.to.as_ref(),
501                )
502            }
503        }
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    use lottie_data::model::{Keyframe, Value};
511
512    #[test]
513    fn test_animator_resolve_binary_search() {
514        // Setup a property with keyframes at 0, 10, 20
515        let keyframes = vec![
516            Keyframe {
517                t: 0.0,
518                s: Some(0.0),
519                e: Some(10.0),
520                i: None, o: None, to: None, ti: None, h: None,
521            },
522            Keyframe {
523                t: 10.0,
524                s: Some(10.0),
525                e: Some(20.0),
526                i: None, o: None, to: None, ti: None, h: None,
527            },
528            Keyframe {
529                t: 20.0,
530                s: Some(20.0),
531                e: Some(30.0),
532                i: None, o: None, to: None, ti: None, h: None,
533            }
534        ];
535
536        let prop = Property {
537            a: 1,
538            k: Value::Animated(keyframes),
539            ix: None,
540            x: None,
541        };
542
543        let conv = |v: &f32| *v;
544
545        // 1. Exact match start
546        assert_eq!(Animator::resolve(&prop, 0.0, conv, -1.0, None, 60.0), 0.0);
547
548        // 2. Exact match middle
549        assert_eq!(Animator::resolve(&prop, 10.0, conv, -1.0, None, 60.0), 10.0);
550
551        // 3. Exact match end
552        assert_eq!(Animator::resolve(&prop, 20.0, conv, -1.0, None, 60.0), 30.0);
553
554        // 4. Before first
555        assert_eq!(Animator::resolve(&prop, -5.0, conv, -1.0, None, 60.0), 0.0);
556
557        // 5. After last
558        assert_eq!(Animator::resolve(&prop, 25.0, conv, -1.0, None, 60.0), 30.0);
559
560        // 6. Mid-segment
561        assert_eq!(Animator::resolve(&prop, 5.0, conv, -1.0, None, 60.0), 5.0);
562
563        // 7. Mid-segment 2
564        assert_eq!(Animator::resolve(&prop, 15.0, conv, -1.0, None, 60.0), 15.0);
565    }
566}