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}