Skip to main content

sequoia_animation/
import.rs

1use std::collections::HashMap;
2
3use sequoia_core::Color;
4
5use crate::Easing;
6use crate::animation::{Animation, Interpolation, Keyframe, Track};
7use crate::bezier::CubicBezier;
8use crate::value::Value;
9
10#[derive(Clone, Debug, PartialEq)]
11pub struct ImportError(pub String);
12
13impl std::fmt::Display for ImportError {
14    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        write!(f, "animation import: {}", self.0)
16    }
17}
18
19impl std::error::Error for ImportError {}
20
21fn err(msg: impl Into<String>) -> ImportError {
22    ImportError(msg.into())
23}
24
25fn get_f64(v: &serde_json::Value) -> Option<f64> {
26    v.as_f64()
27}
28
29fn parse_color(v: &serde_json::Value) -> Result<Color, ImportError> {
30    if let Some(s) = v.as_str() {
31        return Color::parse_hex(s).ok_or_else(|| err(format!("bad color {s:?}")));
32    }
33    if let Some(arr) = v.as_array() {
34        let n: Vec<f32> = arr
35            .iter()
36            .filter_map(|x| x.as_f64().map(|f| f as f32))
37            .collect();
38        if n.len() == 4 {
39            return Ok(Color::rgba(n[0], n[1], n[2], n[3]));
40        }
41        if n.len() == 3 {
42            return Ok(Color::rgb(n[0], n[1], n[2]));
43        }
44    }
45    Err(err(format!("bad color {v}")))
46}
47
48fn parse_value(v: &serde_json::Value) -> Result<Value, ImportError> {
49    if let Some(n) = v.as_f64() {
50        return Ok(Value::f32(n as f32));
51    }
52    let o = v.as_object().ok_or_else(|| err(format!("bad value {v}")))?;
53    if let Some(n) = o.get("px").and_then(get_f64) {
54        return Ok(Value::px(n as f32));
55    }
56    if let Some(n) = o.get("f32").and_then(get_f64) {
57        return Ok(Value::f32(n as f32));
58    }
59    if let Some(c) = o.get("color") {
60        return Ok(Value::color(parse_color(c)?));
61    }
62    if let Some(p) = o.get("point").and_then(|x| x.as_array())
63        && p.len() == 2
64    {
65        let x = p[0].as_f64().ok_or_else(|| err("bad point.x"))? as f32;
66        let y = p[1].as_f64().ok_or_else(|| err("bad point.y"))? as f32;
67        return Ok(Value::point(x, y));
68    }
69    if let Some(s) = o.get("size").and_then(|x| x.as_array())
70        && s.len() == 2
71    {
72        let w = s[0].as_f64().ok_or_else(|| err("bad size.w"))? as f32;
73        let h = s[1].as_f64().ok_or_else(|| err("bad size.h"))? as f32;
74        return Ok(Value::size(w, h));
75    }
76    Err(err(format!("bad value {v}")))
77}
78
79fn named_easing(name: &str) -> Option<Easing> {
80    let n: String = name
81        .chars()
82        .filter(|c| c.is_alphanumeric())
83        .collect::<String>()
84        .to_lowercase();
85    Some(match n.as_str() {
86        "linear" => Easing::Linear,
87        "easein" => Easing::EaseIn,
88        "easeout" => Easing::EaseOut,
89        "easeinout" => Easing::EaseInOut,
90        "easeinquad" => Easing::EaseInQuad,
91        "easeoutquad" => Easing::EaseOutQuad,
92        "easeinoutquad" => Easing::EaseInOutQuad,
93        "easeincubic" => Easing::EaseInCubic,
94        "easeoutcubic" => Easing::EaseOutCubic,
95        "easeinoutcubic" => Easing::EaseInOutCubic,
96        "easeinquart" => Easing::EaseInQuart,
97        "easeoutquart" => Easing::EaseOutQuart,
98        "easeinoutquart" => Easing::EaseInOutQuart,
99        "easeinquint" => Easing::EaseInQuint,
100        "easeoutquint" => Easing::EaseOutQuint,
101        "easeinoutquint" => Easing::EaseInOutQuint,
102        "easeinexpo" => Easing::EaseInExpo,
103        "easeoutexpo" => Easing::EaseOutExpo,
104        "easeinoutexpo" => Easing::EaseInOutExpo,
105        "easeincirc" => Easing::EaseInCirc,
106        "easeoutcirc" => Easing::EaseOutCirc,
107        "easeinoutcirc" => Easing::EaseInOutCirc,
108        "easeinsine" => Easing::EaseInSine,
109        "easeoutsine" => Easing::EaseOutSine,
110        "easeinoutsine" => Easing::EaseInOutSine,
111        "easeinback" => Easing::EaseInBack,
112        "easeoutback" => Easing::EaseOutBack,
113        "easeinoutback" => Easing::EaseInOutBack,
114        "bouncein" => Easing::BounceIn,
115        "bounceout" => Easing::BounceOut,
116        "elasticout" => Easing::ElasticOut,
117        _ => return None,
118    })
119}
120
121fn parse_interpolation(v: Option<&serde_json::Value>) -> Result<Interpolation, ImportError> {
122    let Some(v) = v else {
123        return Ok(Interpolation::Linear);
124    };
125    if let Some(s) = v.as_str() {
126        let low = s.to_lowercase();
127        if low == "hold" {
128            return Ok(Interpolation::Hold);
129        }
130        if low == "linear" {
131            return Ok(Interpolation::Linear);
132        }
133        if let Some(e) = named_easing(s) {
134            return Ok(Interpolation::from_easing(e));
135        }
136        return Err(err(format!("bad interp {s:?}")));
137    }
138    if let Some(o) = v.as_object()
139        && let Some(b) = o.get("bezier").and_then(|x| x.as_array())
140    {
141        if b.len() == 4 {
142            let f: Vec<f32> = b
143                .iter()
144                .filter_map(|x| x.as_f64().map(|f| f as f32))
145                .collect();
146            if f.len() == 4 {
147                return Ok(Interpolation::Bezier(CubicBezier::new(
148                    f[0], f[1], f[2], f[3],
149                )));
150            }
151        }
152        return Err(err(format!("bad bezier {v}")));
153    }
154    Err(err(format!("bad interp {v}")))
155}
156
157fn parse_keyframe(v: &serde_json::Value) -> Result<Keyframe, ImportError> {
158    let o = v
159        .as_object()
160        .ok_or_else(|| err(format!("bad keyframe {v}")))?;
161    let t = o
162        .get("t")
163        .and_then(get_f64)
164        .ok_or_else(|| err(format!("keyframe without t: {v}")))?;
165    let value = o
166        .get("v")
167        .ok_or_else(|| err(format!("keyframe without v: {v}")))?;
168    let interp = parse_interpolation(o.get("ease").or_else(|| o.get("interp")))?;
169    Ok(Keyframe::new(t, parse_value(value)?).with_interpolation(interp))
170}
171
172fn parse_track(v: &serde_json::Value) -> Result<Track, ImportError> {
173    let arr = v.as_array().ok_or_else(|| err(format!("bad track {v}")))?;
174    let mut kfs = Vec::with_capacity(arr.len());
175    for k in arr {
176        kfs.push(parse_keyframe(k)?);
177    }
178    Track::try_new(kfs).ok_or_else(|| err(format!("invalid track {v}")))
179}
180
181pub fn animation_from_json(name: &str, v: &serde_json::Value) -> Result<Animation, ImportError> {
182    let o = v
183        .as_object()
184        .ok_or_else(|| err("animation must be an object"))?;
185    let duration = o
186        .get("duration")
187        .and_then(get_f64)
188        .ok_or_else(|| err("animation without duration"))?;
189    if !duration.is_finite() || duration < 0.0 {
190        return Err(err("bad duration"));
191    }
192    let tracks = o
193        .get("tracks")
194        .and_then(|t| t.as_object())
195        .ok_or_else(|| err("animation without tracks"))?;
196    let mut anim = Animation::new(name, duration);
197    for (track_name, track_v) in tracks {
198        anim = anim.with_track(track_name, parse_track(track_v)?);
199    }
200    Ok(anim)
201}
202
203pub fn bundle_from_json(json: &str) -> Result<HashMap<String, Animation>, ImportError> {
204    let root: serde_json::Value =
205        serde_json::from_str(json).map_err(|e| err(format!("bad json: {e}")))?;
206    let map = root
207        .get("animations")
208        .unwrap_or(&root)
209        .as_object()
210        .ok_or_else(|| err("bundle must be an object"))?;
211    let mut out = HashMap::new();
212    for (name, v) in map {
213        out.insert(name.clone(), animation_from_json(name, v)?);
214    }
215    if out.is_empty() {
216        return Err(err("empty bundle"));
217    }
218    Ok(out)
219}
220
221pub fn import_lottie(json: &str) -> Result<HashMap<String, Animation>, ImportError> {
222    let root: serde_json::Value =
223        serde_json::from_str(json).map_err(|e| err(format!("bad lottie json: {e}")))?;
224    let fr = root.get("fr").and_then(get_f64).unwrap_or(60.0).max(1.0);
225    let layers = root
226        .get("layers")
227        .and_then(|l| l.as_array())
228        .ok_or_else(|| err("lottie without layers"))?;
229    let layer = layers
230        .iter()
231        .find(|l| l.get("ks").is_some())
232        .ok_or_else(|| err("lottie without ks"))?;
233    let ks = &layer["ks"];
234
235    fn lottie_track(prop: Option<&serde_json::Value>, fr: f64, scale: f32) -> Option<(Track, f64)> {
236        let prop = prop?;
237        let k = prop.get("k")?;
238
239        if let Some(n) = k.as_f64() {
240            let t = Track::f32(&[(0.0, (n as f32 * scale))])?;
241            return Some((t, 0.0));
242        }
243        if let Some(arr) = k.as_array() {
244            if arr.iter().all(|x| x.as_f64().is_some()) && arr.len() <= 3 {
245                let n = arr[0].as_f64()? as f32 * scale;
246                let t = Track::f32(&[(0.0, n)])?;
247                return Some((t, 0.0));
248            }
249
250            let mut pts: Vec<(f64, f32)> = Vec::new();
251            for kf in arr {
252                let t = kf.get("t")?.as_f64()? / fr;
253                let s = kf.get("s")?.as_array()?;
254                let v = s.first()?.as_f64()? as f32 * scale;
255                pts.push((t, v));
256            }
257            if pts.is_empty() {
258                return None;
259            }
260            let max_t = pts.iter().map(|&(t, _)| t).fold(0.0f64, f64::max);
261
262            if let Some(&(lt, lv)) = pts.last() {
263                pts.push((lt + 1.0 / fr, lv));
264            }
265            let track = Track::f32(&pts.iter().map(|&(t, v)| (t, v)).collect::<Vec<_>>())?;
266            return Some((track, max_t + 1.0 / fr));
267        }
268        None
269    }
270
271    fn lottie_pos(ks: &serde_json::Value, fr: f64, axis: usize) -> Option<(Track, f64)> {
272        let p = ks.get("p")?;
273        let k = p.get("k")?;
274        if let Some(arr) = k.as_array() {
275            if arr.iter().all(|x| x.as_f64().is_some()) && arr.len() >= 2 {
276                let v = arr.get(axis)?.as_f64()? as f32;
277                let pts = vec![(0.0f64, v)];
278                let t = Track::px(&pts)?;
279                return Some((t, 0.0));
280            }
281            let mut pts: Vec<(f64, f32)> = Vec::new();
282            for kf in arr {
283                let t = kf.get("t")?.as_f64()? / fr;
284                let s = kf.get("s")?.as_array()?;
285                let v = s.get(axis)?.as_f64()? as f32;
286                pts.push((t, v));
287            }
288            if pts.is_empty() {
289                return None;
290            }
291            let max_t = pts.iter().map(|&(t, _)| t).fold(0.0f64, f64::max);
292            if let Some(&(lt, lv)) = pts.last() {
293                pts.push((lt + 1.0 / fr, lv));
294            }
295            let track = Track::px(&pts.iter().map(|&(t, v)| (t, v)).collect::<Vec<_>>())?;
296            return Some((track, max_t + 1.0 / fr));
297        }
298        None
299    }
300
301    let mut tracks: Vec<(&str, Track)> = Vec::new();
302    let mut max_t = 0.5f64;
303    if let Some((t, e)) = lottie_track(ks.get("o"), fr, 0.01) {
304        max_t = max_t.max(e);
305        tracks.push(("opacity", t));
306    }
307    if let Some((t, e)) = lottie_pos(ks, fr, 0) {
308        max_t = max_t.max(e);
309        tracks.push(("x", t));
310    }
311    if let Some((t, e)) = lottie_pos(ks, fr, 1) {
312        max_t = max_t.max(e);
313        tracks.push(("y", t));
314    }
315    if tracks.is_empty() {
316        return Err(err("lottie without animatable ks.o/ks.p"));
317    }
318    let mut anim = Animation::new("lottie", max_t.max(0.05));
319    for (name, t) in tracks {
320        anim = anim.with_track(name, t);
321    }
322    let mut out = HashMap::new();
323    out.insert("lottie".to_string(), anim);
324    Ok(out)
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    const BUNDLE: &str = r#"{
332        "animations": {
333            "fade": {
334                "duration": 0.4,
335                "tracks": {
336                    "opacity": [
337                        {"t": 0.0, "v": 0.0},
338                        {"t": 0.4, "v": 1.0, "ease": "easeOut"}
339                    ],
340                    "x": [
341                        {"t": 0.0, "v": {"px": -8.0}, "interp": "hold"},
342                        {"t": 0.4, "v": {"px": 0.0}}
343                    ]
344                }
345            }
346        }
347    }"#;
348
349    #[test]
350    fn bundle_parses_tracks() {
351        let b = bundle_from_json(BUNDLE).expect("bundle");
352        let a = &b["fade"];
353        let f0 = a.sample(0.0);
354        assert_eq!(f0.get("opacity").and_then(|v| v.as_f32()), Some(0.0));
355        let f1 = a.sample(1.0);
356        assert_eq!(f1.get("opacity").and_then(|v| v.as_f32()), Some(1.0));
357        assert_eq!(f1.get("x").and_then(|v| v.as_px()), Some(0.0));
358    }
359
360    #[test]
361    fn bezier_and_color_values() {
362        let json = r##"{"animations": {"c": {"duration": 1.0, "tracks": {
363            "background": [
364                {"t": 0.0, "v": {"color": "#ff0000"}},
365                {"t": 1.0, "v": {"color": [0.0, 0.0, 1.0, 1.0]}, "ease": {"bezier": [0.25, 0.1, 0.25, 1.0]}}
366            ]}}}}"##;
367        let b = bundle_from_json(json).expect("bundle");
368        let f = b["c"].sample(1.0);
369        let c = f.get("background").and_then(|v| v.as_color()).unwrap();
370        assert!((c.b - 1.0).abs() < 0.01, "{c:?}");
371    }
372
373    #[test]
374    fn rejects_garbage() {
375        assert!(bundle_from_json("{}").is_err());
376        assert!(bundle_from_json("not json").is_err());
377        assert!(bundle_from_json(r#"{"animations":{"x":{"duration":-1.0,"tracks":{}}}}"#).is_err());
378    }
379
380    #[test]
381    fn lottie_subset_maps_opacity_and_pos() {
382        let lottie = r#"{
383            "fr": 60.0,
384            "layers": [{"ks": {
385                "o": {"k": [{"t": 0.0, "s": [0.0]}, {"t": 30.0, "s": [100.0]}]},
386                "p": {"k": [10.0, 20.0]}
387            }}]
388        }"#;
389        let b = import_lottie(lottie).expect("lottie");
390        let a = &b["lottie"];
391        let f0 = a.sample(0.0);
392        assert_eq!(f0.get("opacity").and_then(|v| v.as_f32()), Some(0.0));
393        assert_eq!(
394            a.sample(10.0).get("opacity").and_then(|v| v.as_f32()),
395            Some(1.0)
396        );
397        assert_eq!(a.sample(0.0).get("x").and_then(|v| v.as_px()), Some(10.0));
398    }
399}