Skip to main content

sequoia_animation/
animation.rs

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}