1use std::collections::HashMap;
2
3use sequoia_core::Color;
4
5use crate::Easing;
6use crate::Spring;
7use crate::bezier::CubicBezier;
8use crate::value::Value;
9
10#[derive(Clone, Copy, Debug, PartialEq)]
11#[non_exhaustive]
12pub enum Interpolation {
13 Hold,
14
15 Linear,
16
17 Bezier(CubicBezier),
18
19 Spring(Spring),
20
21 Easing(Easing),
22}
23
24impl Interpolation {
25 pub fn apply(&self, t: f32) -> f32 {
26 match self {
27 Self::Hold => 0.0,
28 Self::Linear => t,
29 Self::Bezier(b) => b.solve(t),
30 Self::Spring(s) => s.value(t),
31 Self::Easing(e) => e.apply(t),
32 }
33 }
34
35 pub fn from_easing(easing: Easing) -> Self {
36 match easing {
37 Easing::CubicBezier(x1, y1, x2, y2) => Self::Bezier(CubicBezier::new(x1, y1, x2, y2)),
38 Easing::Linear => Self::Linear,
39 other => Self::Easing(other),
40 }
41 }
42}
43
44#[derive(Clone, Copy, Debug, PartialEq)]
45pub struct Keyframe {
46 pub time: f64,
47 pub value: Value,
48 pub interpolation: Interpolation,
49}
50
51impl Keyframe {
52 pub fn new(time: f64, value: impl Into<Value>) -> Self {
53 Self {
54 time,
55 value: value.into(),
56 interpolation: Interpolation::Linear,
57 }
58 }
59 pub fn hold(time: f64, value: impl Into<Value>) -> Self {
60 Self {
61 time,
62 value: value.into(),
63 interpolation: Interpolation::Hold,
64 }
65 }
66 pub fn with_interpolation(mut self, interpolation: Interpolation) -> Self {
67 self.interpolation = interpolation;
68 self
69 }
70
71 pub fn with_easing(self, easing: Easing) -> Self {
72 self.with_interpolation(Interpolation::from_easing(easing))
73 }
74}
75
76#[derive(Clone, Debug, PartialEq)]
77pub struct Track {
78 keyframes: Vec<Keyframe>,
79 duration: f64,
80}
81
82impl Track {
83 pub fn try_new(keyframes: Vec<Keyframe>) -> Option<Self> {
84 if keyframes.is_empty() {
85 return None;
86 }
87 if keyframes
88 .iter()
89 .any(|k| !k.time.is_finite() || k.time < 0.0)
90 {
91 return None;
92 }
93 if keyframes.windows(2).any(|w| w[0].time > w[1].time) {
94 return None;
95 }
96 let duration = keyframes.last()?.time;
97 Some(Self {
98 keyframes,
99 duration,
100 })
101 }
102
103 pub fn keyframes(&self) -> &[Keyframe] {
104 &self.keyframes
105 }
106
107 pub fn px(points: &[(f64, f32)]) -> Option<Self> {
108 Self::try_new(
109 points
110 .iter()
111 .map(|&(t, v)| Keyframe::new(t, Value::px(v)))
112 .collect(),
113 )
114 }
115
116 pub fn f32(points: &[(f64, f32)]) -> Option<Self> {
117 Self::try_new(
118 points
119 .iter()
120 .map(|&(t, v)| Keyframe::new(t, Value::f32(v)))
121 .collect(),
122 )
123 }
124
125 pub fn color(points: &[(f64, Color)]) -> Option<Self> {
126 Self::try_new(
127 points
128 .iter()
129 .map(|&(t, c)| Keyframe::new(t, Value::color(c)))
130 .collect(),
131 )
132 }
133
134 pub fn with_easing(mut self, easing: Easing) -> Self {
135 let interpolation = Interpolation::from_easing(easing);
136 for keyframe in &mut self.keyframes {
137 keyframe.interpolation = interpolation;
138 }
139 self
140 }
141
142 pub fn duration(&self) -> f64 {
143 self.duration
144 }
145
146 pub fn sample(&self, t: f64) -> Option<Value> {
147 let t = t.max(0.0);
148 let first = self.keyframes.first()?;
149 if t < first.time {
150 return Some(first.value);
151 }
152 let last = self.keyframes.last()?;
153 if t >= last.time {
154 return Some(last.value);
155 }
156 for pair in self.keyframes.windows(2) {
157 let (a, b) = (&pair[0], &pair[1]);
158 if t >= a.time && t <= b.time {
159 if a.time == b.time {
160 return Some(b.value);
161 }
162 let seg_t = ((t - a.time) / (b.time - a.time)) as f32;
163 return Some(match a.interpolation {
164 Interpolation::Hold => a.value,
165 Interpolation::Spring(s) => {
166 let seg_dur = (b.time - a.time).max(1e-6) as f32;
167 a.value.lerp(&b.value, s.value(seg_t * seg_dur))
168 }
169 _ => a.value.lerp(&b.value, a.interpolation.apply(seg_t)),
170 });
171 }
172 }
173
174 Some(first.value)
175 }
176}
177
178#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
179#[non_exhaustive]
180pub enum LoopMode {
181 #[default]
182 Once,
183
184 Loop,
185
186 PingPong,
187}
188
189#[derive(Clone, Debug, Default, PartialEq)]
190pub struct AnimationFrame {
191 pub properties: HashMap<String, Value>,
192}
193
194impl AnimationFrame {
195 pub fn new() -> Self {
196 Self::default()
197 }
198 pub fn get(&self, name: &str) -> Option<&Value> {
199 self.properties.get(name)
200 }
201 pub fn insert(&mut self, name: impl Into<String>, value: impl Into<Value>) {
202 self.properties.insert(name.into(), value.into());
203 }
204 pub fn len(&self) -> usize {
205 self.properties.len()
206 }
207 pub fn is_empty(&self) -> bool {
208 self.properties.is_empty()
209 }
210}
211
212#[derive(Clone, Debug)]
213pub struct Animation {
214 name: String,
215 duration: f64,
216 speed: f32,
217 loop_mode: LoopMode,
218 tracks: HashMap<String, Track>,
219}
220
221impl Animation {
222 pub fn new(name: impl Into<String>, duration: f64) -> Self {
223 Self {
224 name: name.into(),
225 duration,
226 speed: 1.0,
227 loop_mode: LoopMode::Once,
228 tracks: HashMap::new(),
229 }
230 }
231
232 pub fn with_speed(mut self, speed: f32) -> Self {
233 self.speed = speed;
234 self
235 }
236 pub fn with_loop(mut self, mode: LoopMode) -> Self {
237 self.loop_mode = mode;
238 self
239 }
240 pub fn with_track(mut self, name: impl Into<String>, track: Track) -> Self {
241 self.tracks.insert(name.into(), track);
242 self
243 }
244
245 pub fn name(&self) -> &str {
246 &self.name
247 }
248 pub fn duration(&self) -> f64 {
249 self.duration
250 }
251 pub fn speed(&self) -> f32 {
252 self.speed
253 }
254 pub fn loop_mode(&self) -> LoopMode {
255 self.loop_mode
256 }
257 pub fn tracks(&self) -> &HashMap<String, Track> {
258 &self.tracks
259 }
260
261 pub fn sample(&self, t: f64) -> AnimationFrame {
262 let t = self.wrap_time(t);
263 let mut frame = AnimationFrame::new();
264 for (name, track) in &self.tracks {
265 if let Some(v) = track.sample(t) {
266 frame.properties.insert(name.clone(), v);
267 }
268 }
269 frame
270 }
271
272 pub fn is_finished_at(&self, t: f64) -> bool {
273 self.loop_mode == LoopMode::Once && t >= self.duration
274 }
275
276 fn wrap_time(&self, t: f64) -> f64 {
277 if self.duration <= 0.0 {
278 return 0.0;
279 }
280 match self.loop_mode {
281 LoopMode::Once => t.max(0.0).min(self.duration),
282 LoopMode::Loop => {
283 let t = t.max(0.0);
284 t % self.duration
285 }
286 LoopMode::PingPong => {
287 let period = self.duration * 2.0;
288 let t = t.max(0.0) % period;
289 if t <= self.duration { t } else { period - t }
290 }
291 }
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 fn keyframes() -> Vec<Keyframe> {
300 vec![
301 Keyframe::new(0.0, Value::px(0.0)),
302 Keyframe::new(0.5, Value::px(50.0)),
303 Keyframe::new(1.0, Value::px(100.0)),
304 ]
305 }
306
307 #[test]
308 fn track_rejects_invalid_input() {
309 assert!(Track::try_new(vec![]).is_none());
310 assert!(Track::try_new(vec![Keyframe::new(-1.0, 0.0f32)]).is_none());
311 assert!(Track::try_new(vec![Keyframe::new(f64::NAN, 0.0f32)]).is_none());
312 assert!(
313 Track::try_new(vec![
314 Keyframe::new(0.0, 0.0f32),
315 Keyframe::new(0.1, 1.0f32),
316 Keyframe::new(0.05, 2.0f32),
317 ])
318 .is_none()
319 );
320 }
321
322 #[test]
323 fn track_samples_segments() {
324 let t = Track::try_new(keyframes()).expect("valid");
325 assert_eq!(t.duration(), 1.0);
326 assert_eq!(t.sample(-5.0), Some(Value::px(0.0)));
327 assert_eq!(t.sample(0.25), Some(Value::px(25.0)));
328 assert_eq!(t.sample(1.5), Some(Value::px(100.0)));
329 }
330
331 #[test]
332 fn hold_keyframe_does_not_interpolate() {
333 let t = Track::try_new(vec![
334 Keyframe::hold(0.0, 0.0f32),
335 Keyframe::new(1.0, 100.0f32),
336 ])
337 .expect("valid");
338 assert_eq!(t.sample(0.5), Some(Value::f32(0.0)));
339 assert_eq!(t.sample(0.99), Some(Value::f32(0.0)));
340 assert_eq!(t.sample(1.0), Some(Value::f32(100.0)));
341 }
342
343 #[test]
344 fn spring_keyframes_overshoot_then_settle() {
345 let spring = Spring::new(0.4, 2.0);
346 let t = Track::try_new(vec![
347 Keyframe::new(0.0, 0.0f32).with_interpolation(Interpolation::Spring(spring)),
348 Keyframe::new(1.0, 100.0f32),
349 ])
350 .expect("valid");
351
352 assert!(t.sample(0.1).unwrap().as_f32().unwrap() > 0.0);
353 let mid = t.sample(0.5).unwrap().as_f32().unwrap();
354 assert!(mid != 50.0, "spring must not be linear");
355
356 assert_eq!(t.sample(10.0), Some(Value::f32(100.0)));
357 }
358
359 #[test]
360 fn duplicate_times_do_not_divide_by_zero() {
361 let t = Track::try_new(vec![Keyframe::new(0.0, 1.0f32), Keyframe::new(0.0, 2.0f32)])
362 .expect("duplicate times are non-decreasing");
363 assert_eq!(t.sample(0.0), Some(Value::f32(2.0)));
364 }
365
366 #[test]
367 fn loop_modes_wrap_time() {
368 let a = Animation::new("a", 1.0).with_track("x", Track::try_new(keyframes()).unwrap());
369 assert_eq!(a.sample(1.5).get("x"), Some(&Value::px(100.0)));
370
371 let looped = Animation::new("a", 1.0)
372 .with_loop(LoopMode::Loop)
373 .with_track("x", Track::try_new(keyframes()).unwrap());
374 assert_eq!(looped.sample(1.5).get("x"), Some(&Value::px(50.0)));
375
376 let pingpong = Animation::new("a", 1.0)
377 .with_loop(LoopMode::PingPong)
378 .with_track("x", Track::try_new(keyframes()).unwrap());
379
380 assert_eq!(pingpong.sample(1.5).get("x"), Some(&Value::px(50.0)));
381 }
382
383 #[test]
384 fn once_animation_reports_finished() {
385 let a = Animation::new("a", 1.0);
386 assert!(a.is_finished_at(1.0));
387 assert!(!a.is_finished_at(0.9));
388 let l = a.with_loop(LoopMode::Loop);
389 assert!(!l.is_finished_at(10.0));
390 }
391
392 #[test]
393 fn builder_helpers_make_typed_tracks() {
394 let px = Track::px(&[(0.0, 0.0), (1.0, 100.0)]).unwrap();
395 assert_eq!(px.sample(0.5), Some(Value::px(50.0)));
396 let f = Track::f32(&[(0.0, 1.0), (1.0, 0.0)]).unwrap();
397 assert_eq!(f.sample(0.5), Some(Value::f32(0.5)));
398 let c = Track::color(&[(0.0, Color::BLACK), (1.0, Color::WHITE)]).unwrap();
399 assert!(c.sample(0.5).and_then(|v| v.as_color()).is_some());
400 assert!(Track::px(&[]).is_none(), "still validated");
401 }
402
403 #[test]
404 fn with_easing_reshapes_the_whole_track() {
405 let linear = Track::px(&[(0.0, 0.0), (1.0, 100.0)]).unwrap();
406 let eased = linear.clone().with_easing(Easing::EaseInQuad);
407
408 let v = eased.sample(0.5).unwrap().as_px().unwrap();
409 assert!((v - 25.0).abs() < 1e-3, "eased: {v}");
410 assert_eq!(
411 linear.sample(0.5),
412 Some(Value::px(50.0)),
413 "source unchanged"
414 );
415 }
416
417 #[test]
418 fn interpolation_from_easing_bridges_curves() {
419 assert_eq!(
420 Interpolation::from_easing(Easing::Linear),
421 Interpolation::Linear
422 );
423 assert!(matches!(
424 Interpolation::from_easing(Easing::EaseOutBack),
425 Interpolation::Easing(Easing::EaseOutBack)
426 ));
427 assert!(matches!(
428 Interpolation::from_easing(Easing::CubicBezier(0.3, 1.4, 0.4, 1.0)),
429 Interpolation::Bezier(_)
430 ));
431
432 let kf = Keyframe::new(0.0, Value::px(0.0)).with_easing(Easing::EaseOutQuart);
433 assert!(matches!(kf.interpolation, Interpolation::Easing(_)));
434 }
435}