Skip to main content

spanda/
keyframe.rs

1//! Multi-stop keyframe animation tracks.
2//!
3//! A [`KeyframeTrack<T>`] holds a sorted list of `(time, value)` pairs.  At any
4//! time `t`, it interpolates between the two surrounding keyframes using the
5//! segment's [`Easing`] curve.
6//!
7//! # Quick start
8//!
9//! ```rust
10//! use spanda::keyframe::{KeyframeTrack, Loop};
11//! use spanda::traits::Update;
12//!
13//! let mut track = KeyframeTrack::new()
14//!     .push(0.0, 0.0_f32)
15//!     .push(0.5, 1.0)
16//!     .push(1.0, 0.0)
17//!     .looping(Loop::Forever);
18//!
19//! track.update(0.25);
20//! let value = track.value();
21//! assert!(value > 0.0 && value < 1.0);
22//! ```
23
24#[cfg(not(feature = "std"))]
25use alloc::vec::Vec;
26
27use crate::easing::Easing;
28use crate::traits::{Animatable, Update};
29
30#[cfg(feature = "serde")]
31use serde::{Deserialize, Serialize};
32
33// ── Loop ─────────────────────────────────────────────────────────────────────
34
35/// How a [`KeyframeTrack`] repeats after reaching the end.
36#[derive(Debug, Clone, PartialEq)]
37#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
38pub enum Loop {
39    /// Play once and stop at the last keyframe.
40    Once,
41    /// Play a fixed number of times.
42    Times(u32),
43    /// Loop forever.
44    Forever,
45    /// Play forward then backward, repeating.
46    PingPong,
47}
48
49// ── Keyframe ─────────────────────────────────────────────────────────────────
50
51/// A single keyframe — a value at a specific time with an easing to the next.
52#[derive(Clone)]
53pub struct Keyframe<T: Animatable> {
54    /// Time in seconds from track start.
55    pub time: f32,
56    /// Value at this keyframe.
57    pub value: T,
58    /// Easing used from THIS keyframe to the NEXT one.
59    pub easing: Easing,
60}
61
62impl<T: Animatable + core::fmt::Debug> core::fmt::Debug for Keyframe<T> {
63    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
64        f.debug_struct("Keyframe")
65            .field("time", &self.time)
66            .field("value", &self.value)
67            .field("easing", &self.easing)
68            .finish()
69    }
70}
71
72// ── KeyframeTrack ────────────────────────────────────────────────────────────
73
74/// A sorted sequence of keyframes that can be evaluated at any time.
75pub struct KeyframeTrack<T: Animatable> {
76    frames: Vec<Keyframe<T>>,
77    elapsed: f32,
78    looping: Loop,
79    completed: bool,
80    loop_count: u32,
81}
82
83impl<T: Animatable + core::fmt::Debug> core::fmt::Debug for KeyframeTrack<T> {
84    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
85        f.debug_struct("KeyframeTrack")
86            .field("frames", &self.frames)
87            .field("elapsed", &self.elapsed)
88            .field("looping", &self.looping)
89            .field("completed", &self.completed)
90            .finish()
91    }
92}
93
94impl<T: Animatable> KeyframeTrack<T> {
95    /// Create an empty track.
96    pub fn new() -> Self {
97        Self {
98            frames: Vec::new(),
99            elapsed: 0.0,
100            looping: Loop::Once,
101            completed: false,
102            loop_count: 0,
103        }
104    }
105
106    /// Add a keyframe at `time` with the given `value`.
107    ///
108    /// Uses [`Easing::Linear`] for the segment from this frame to the next.
109    /// Frames are kept sorted by time internally.
110    pub fn push(mut self, time: f32, value: T) -> Self {
111        self.frames.push(Keyframe {
112            time,
113            value,
114            easing: Easing::Linear,
115        });
116        self.frames.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
117        self
118    }
119
120    /// Add a keyframe with a specific easing to the next frame.
121    pub fn push_with_easing(mut self, time: f32, value: T, easing: Easing) -> Self {
122        self.frames.push(Keyframe {
123            time,
124            value,
125            easing,
126        });
127        self.frames.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
128        self
129    }
130
131    /// Set the loop mode.
132    pub fn looping(mut self, mode: Loop) -> Self {
133        self.looping = mode;
134        self
135    }
136
137    /// Total duration — time of the last keyframe.
138    pub fn duration(&self) -> f32 {
139        self.frames.last().map_or(0.0, |f| f.time)
140    }
141
142    /// Evaluate the track at an arbitrary time `t` (pure, ignores `elapsed`).
143    pub fn value_at(&self, t: f32) -> T {
144        if self.frames.is_empty() {
145            panic!("KeyframeTrack::value_at called on empty track");
146        }
147
148        if self.frames.len() == 1 {
149            return self.frames[0].value.clone();
150        }
151
152        // Clamp to valid range
153        let t = t.clamp(0.0, self.duration());
154
155        // Find the segment: last frame where frame.time <= t
156        let idx = self
157            .frames
158            .iter()
159            .rposition(|f| f.time <= t)
160            .unwrap_or(0);
161
162        // If at or past the last frame, return last value
163        if idx >= self.frames.len() - 1 {
164            return self.frames.last().unwrap().value.clone();
165        }
166
167        let a = &self.frames[idx];
168        let b = &self.frames[idx + 1];
169        let segment_duration = b.time - a.time;
170
171        if segment_duration <= 0.0 {
172            return b.value.clone();
173        }
174
175        let local_t = ((t - a.time) / segment_duration).clamp(0.0, 1.0);
176        let curved_t = a.easing.apply(local_t);
177        a.value.lerp(&b.value, curved_t)
178    }
179
180    /// Current value based on internal `elapsed` time.
181    pub fn value(&self) -> T {
182        let t = self.effective_time();
183        self.value_at(t)
184    }
185
186    /// Whether the track has finished playing.
187    pub fn is_complete(&self) -> bool {
188        self.completed
189    }
190
191    /// Reset to the beginning.
192    pub fn reset(&mut self) {
193        self.elapsed = 0.0;
194        self.completed = false;
195        self.loop_count = 0;
196    }
197
198    /// Compute the effective time considering loop mode.
199    fn effective_time(&self) -> f32 {
200        let dur = self.duration();
201        if dur <= 0.0 {
202            return 0.0;
203        }
204
205        match &self.looping {
206            Loop::Once => self.elapsed.clamp(0.0, dur),
207            Loop::Times(_) | Loop::Forever => {
208                self.elapsed % dur
209            }
210            Loop::PingPong => {
211                let cycle = 2.0 * dur;
212                let cycle_t = self.elapsed % cycle;
213                if cycle_t <= dur {
214                    cycle_t
215                } else {
216                    2.0 * dur - cycle_t
217                }
218            }
219        }
220    }
221}
222
223impl<T: Animatable> Update for KeyframeTrack<T> {
224    fn update(&mut self, dt: f32) -> bool {
225        if self.completed {
226            return false;
227        }
228
229        let dt = dt.max(0.0);
230        self.elapsed += dt;
231
232        let dur = self.duration();
233        if dur <= 0.0 {
234            self.completed = true;
235            return false;
236        }
237
238        match &self.looping {
239            Loop::Once => {
240                if self.elapsed >= dur {
241                    self.elapsed = dur;
242                    self.completed = true;
243                }
244            }
245            Loop::Times(n) => {
246                let loops_done = (self.elapsed / dur).floor() as u32;
247                if loops_done >= *n {
248                    self.elapsed = dur * (*n as f32);
249                    self.completed = true;
250                }
251            }
252            Loop::Forever | Loop::PingPong => {
253                // Never completes
254            }
255        }
256
257        !self.completed
258    }
259}
260
261// ── Tests ────────────────────────────────────────────────────────────────────
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn single_frame_returns_its_value() {
269        let track = KeyframeTrack::new().push(0.0, 42.0_f32);
270        assert!((track.value_at(0.0) - 42.0).abs() < 1e-6);
271        assert!((track.value_at(999.0) - 42.0).abs() < 1e-6);
272    }
273
274    #[test]
275    fn two_frames_interpolate() {
276        let track = KeyframeTrack::new()
277            .push(0.0, 0.0_f32)
278            .push(1.0, 100.0);
279        assert!((track.value_at(0.5) - 50.0).abs() < 1e-4);
280    }
281
282    #[test]
283    fn three_frames_two_segments() {
284        let track = KeyframeTrack::new()
285            .push(0.0, 0.0_f32)
286            .push(1.0, 100.0)
287            .push(2.0, 0.0);
288        assert!((track.value_at(0.5) - 50.0).abs() < 1e-4);
289        assert!((track.value_at(1.5) - 50.0).abs() < 1e-4);
290    }
291
292    #[test]
293    fn loop_once_completes() {
294        let mut track = KeyframeTrack::new()
295            .push(0.0, 0.0_f32)
296            .push(1.0, 100.0)
297            .looping(Loop::Once);
298
299        assert!(track.update(0.5));
300        assert!(!track.is_complete());
301        assert!(!track.update(0.5));
302        assert!(track.is_complete());
303    }
304
305    #[test]
306    fn loop_forever_never_completes() {
307        let mut track = KeyframeTrack::new()
308            .push(0.0, 0.0_f32)
309            .push(1.0, 100.0)
310            .looping(Loop::Forever);
311
312        for _ in 0..100 {
313            assert!(track.update(0.5));
314        }
315        assert!(!track.is_complete());
316    }
317
318    #[test]
319    fn ping_pong_reverses() {
320        let track = KeyframeTrack::new()
321            .push(0.0, 0.0_f32)
322            .push(1.0, 100.0)
323            .looping(Loop::PingPong);
324
325        // At t=1.5 in ping-pong: cycle = 2.0, cycle_t = 1.5, backward → t = 0.5
326        assert!((track.value_at(0.5) - 50.0).abs() < 1e-4);
327    }
328
329    #[test]
330    fn loop_times_completes_after_n() {
331        let mut track = KeyframeTrack::new()
332            .push(0.0, 0.0_f32)
333            .push(1.0, 100.0)
334            .looping(Loop::Times(2));
335
336        assert!(track.update(1.0)); // first loop done
337        assert!(!track.update(1.0)); // second loop done
338        assert!(track.is_complete());
339    }
340
341    #[test]
342    fn out_of_bounds_clamps() {
343        let track = KeyframeTrack::new()
344            .push(0.0, 0.0_f32)
345            .push(1.0, 100.0);
346        assert!((track.value_at(-5.0) - 0.0).abs() < 1e-6);
347        assert!((track.value_at(99.0) - 100.0).abs() < 1e-6);
348    }
349
350    #[test]
351    fn with_easing() {
352        let track = KeyframeTrack::new()
353            .push_with_easing(0.0, 0.0_f32, Easing::EaseInQuad)
354            .push(1.0, 100.0);
355        // EaseInQuad at t=0.5 → 0.25, so value ≈ 25.0
356        assert!((track.value_at(0.5) - 25.0).abs() < 1e-4);
357    }
358
359    #[test]
360    fn update_advances_value() {
361        let mut track = KeyframeTrack::new()
362            .push(0.0, 0.0_f32)
363            .push(1.0, 100.0);
364        track.update(0.5);
365        assert!((track.value() - 50.0).abs() < 1e-4);
366    }
367}