1use crate::Message;
12use iced::mouse;
13use iced::widget::canvas::{self, Fill, Frame, Geometry, Path, fill};
14use iced::{Color, Point, Rectangle, Renderer, Theme};
15use std::sync::OnceLock;
16
17const KEYFRAMES: &str = include_str!("../../../assets/identity/animations/keyframes.json");
18
19type Contour = (Point, Vec<[Point; 3]>);
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum Motion {
24 Still,
25 Loading,
26 Working,
27}
28
29struct Animation {
30 duration: f32,
31 frames: Vec<(f32, Vec<Contour>)>,
32}
33
34struct Kit {
35 still: Vec<Contour>,
36 loading: Animation,
37 working: Animation,
38}
39
40fn parse(d: &str) -> Option<Vec<Contour>> {
42 let mut out: Vec<Contour> = vec![];
43 let mut nums: Vec<f32> = vec![];
44 let mut cmd = ' ';
45 let flush = |cmd: char, nums: &mut Vec<f32>, out: &mut Vec<Contour>| -> Option<()> {
46 let pts: Vec<Point> = nums.chunks(2).map(|c| Point::new(c[0], *c.get(1).unwrap_or(&0.0))).collect();
47 match cmd {
48 'M' => out.push((*pts.first()?, vec![])),
49 'C' => {
50 if pts.len() % 3 != 0 {
51 return None;
52 }
53 let segs = &mut out.last_mut()?.1;
54 for c in pts.chunks(3) {
55 segs.push([c[0], c[1], c[2]]);
56 }
57 }
58 ' ' | 'Z' => {}
59 _ => return None,
60 }
61 nums.clear();
62 Some(())
63 };
64 for tok in d.split(|c: char| c.is_whitespace() || c == ',').filter(|t| !t.is_empty()) {
65 let first = tok.chars().next()?;
66 if first.is_ascii_alphabetic() {
67 flush(cmd, &mut nums, &mut out)?;
68 cmd = first;
69 let rest = &tok[1..];
70 if !rest.is_empty() {
71 nums.push(rest.parse().ok()?);
72 }
73 } else {
74 nums.push(tok.parse().ok()?);
75 }
76 }
77 flush(cmd, &mut nums, &mut out)?;
78 Some(out)
79}
80
81fn kit() -> &'static Option<Kit> {
82 static KIT: OnceLock<Option<Kit>> = OnceLock::new();
83 KIT.get_or_init(|| {
84 let v: serde_json::Value = serde_json::from_str(KEYFRAMES).ok()?;
85 let still = parse(v["staticPath"].as_str()?)?;
86 let anim = |name: &str| -> Option<Animation> {
87 let a = v["animations"].as_array()?.iter().find(|a| a["name"] == name)?;
88 let frames = a["keyframes"]
89 .as_array()?
90 .iter()
91 .map(|k| Some((k["offset"].as_f64()? as f32, parse(k["d"].as_str()?)?)))
92 .collect::<Option<Vec<_>>>()?;
93 Some(Animation { duration: a["durationMs"].as_f64()? as f32 / 1000.0, frames })
94 };
95 Some(Kit { still, loading: anim("loading")?, working: anim("working")? })
96 })
97}
98
99fn lerp(a: Point, b: Point, t: f32) -> Point {
100 Point::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t)
101}
102
103fn shape(motion: Motion, seconds: f32) -> Option<Vec<Contour>> {
105 let k = kit().as_ref()?;
106 let a = match motion {
107 Motion::Still => return Some(k.still.clone()),
108 Motion::Loading => &k.loading,
109 Motion::Working => &k.working,
110 };
111 let phase = (seconds / a.duration).rem_euclid(1.0);
112 let i = a.frames.iter().rposition(|(o, _)| *o <= phase).unwrap_or(0);
113 let (o0, f0) = &a.frames[i];
114 let Some((o1, f1)) = a.frames.get(i + 1) else { return Some(f0.clone()) };
115 let t = if o1 > o0 { (phase - o0) / (o1 - o0) } else { 0.0 };
116 if f0.len() != f1.len() || f0.iter().zip(f1).any(|(a, b)| a.1.len() != b.1.len()) {
118 return Some(if t < 0.5 { f0.clone() } else { f1.clone() });
119 }
120 Some(
121 f0.iter()
122 .zip(f1)
123 .map(|((s0, c0), (s1, c1))| {
124 let segs =
125 c0.iter().zip(c1).map(|(a, b)| [lerp(a[0], b[0], t), lerp(a[1], b[1], t), lerp(a[2], b[2], t)]);
126 (lerp(*s0, *s1, t), segs.collect())
127 })
128 .collect(),
129 )
130}
131
132pub struct Mark {
134 pub motion: Motion,
135 pub seconds: f32,
136 pub color: Color,
137}
138
139impl canvas::Program<Message> for Mark {
140 type State = ();
141
142 fn draw(
143 &self,
144 _state: &(),
145 renderer: &Renderer,
146 _theme: &Theme,
147 bounds: Rectangle,
148 _cursor: mouse::Cursor,
149 ) -> Vec<Geometry> {
150 let mut f = Frame::new(renderer, bounds.size());
151 let scale = bounds.width.min(bounds.height) / 1024.0;
152 let at = |p: Point| Point::new(p.x * scale, p.y * scale);
153 if let Some(contours) = shape(self.motion, self.seconds) {
154 let path = Path::new(|b| {
155 for (start, segs) in &contours {
156 b.move_to(at(*start));
157 for [c1, c2, end] in segs {
158 b.bezier_curve_to(at(*c1), at(*c2), at(*end));
159 }
160 b.close();
161 }
162 });
163 f.fill(&path, Fill { style: fill::Style::Solid(self.color), rule: fill::Rule::EvenOdd });
164 }
165 vec![f.into_geometry()]
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn the_kit_parses_and_animates() {
175 let k = kit().as_ref().expect("keyframes.json parses");
176 assert_eq!(k.still.len(), 3, "body and two eyes");
177 for a in [&k.loading, &k.working] {
178 assert!(a.duration > 1.0 && a.frames.len() > 10);
179 }
180 let a = shape(Motion::Working, 0.3).unwrap();
181 let b = shape(Motion::Working, 0.9).unwrap();
182 assert_eq!(a.len(), 3);
183 assert_ne!(a[0].1[5], b[0].1[5], "the body moves");
184 let c = shape(Motion::Working, 0.3 + 2.4).unwrap();
186 assert!((a[0].0.x - c[0].0.x).abs() < 0.5);
187 }
188}