Skip to main content

animato_driver/
driver.rs

1//! [`AnimationDriver`] — owns and ticks multiple animations simultaneously.
2
3use animato_core::Update;
4
5#[cfg(feature = "std")]
6use crate::recorder::AnimationRecorder;
7
8/// An opaque handle to an animation registered with [`AnimationDriver`].
9///
10/// Returned by [`AnimationDriver::add`]. Use it to cancel or query animations.
11#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
12pub struct AnimationId(u64);
13
14struct Slot {
15    id: AnimationId,
16    animation: Box<dyn Update + Send>,
17    remove: bool,
18    #[cfg(feature = "std")]
19    recorder: Option<RecordedSampler>,
20}
21
22#[cfg(feature = "std")]
23struct RecordedSampler {
24    label: String,
25    sample: Box<dyn Fn() -> f64 + Send + 'static>,
26}
27
28#[cfg(feature = "std")]
29impl std::fmt::Debug for RecordedSampler {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        f.debug_struct("RecordedSampler")
32            .field("label", &self.label)
33            .finish()
34    }
35}
36
37impl std::fmt::Debug for Slot {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.debug_struct("Slot")
40            .field("id", &self.id)
41            .field("remove", &self.remove)
42            .finish()
43    }
44}
45
46/// Manages a collection of animations, ticking them each frame and
47/// automatically removing completed ones.
48///
49/// # Example
50///
51/// ```rust
52/// use animato_driver::{AnimationDriver, MockClock, Clock};
53/// use animato_tween::Tween;
54/// use animato_core::Easing;
55///
56/// let mut driver = AnimationDriver::new();
57/// let id = driver.add(
58///     Tween::new(0.0_f32, 1.0).duration(1.0).build()
59/// );
60///
61/// assert!(driver.is_active(id));
62/// assert_eq!(driver.active_count(), 1);
63///
64/// // Tick past completion:
65/// driver.tick(2.0);
66/// assert!(!driver.is_active(id));
67/// assert_eq!(driver.active_count(), 0);
68/// ```
69#[derive(Debug, Default)]
70pub struct AnimationDriver {
71    slots: Vec<Slot>,
72    next_id: u64,
73}
74
75impl AnimationDriver {
76    /// Create a new, empty driver.
77    pub fn new() -> Self {
78        Self {
79            slots: Vec::new(),
80            next_id: 0,
81        }
82    }
83
84    /// Register an animation and return its [`AnimationId`].
85    ///
86    /// The animation will be ticked on every call to [`tick`](Self::tick)
87    /// and automatically removed when it returns `false` from `update`.
88    pub fn add<A: Update + Send + 'static>(&mut self, anim: A) -> AnimationId {
89        let id = AnimationId(self.next_id);
90        self.next_id += 1;
91        self.slots.push(Slot {
92            id,
93            animation: Box::new(anim),
94            remove: false,
95            #[cfg(feature = "std")]
96            recorder: None,
97        });
98        id
99    }
100
101    /// Register an animation with a scalar sampler for [`tick_recorded`](Self::tick_recorded).
102    #[cfg(feature = "std")]
103    pub fn add_recorded<A, F>(
104        &mut self,
105        label: impl Into<String>,
106        anim: A,
107        sampler: F,
108    ) -> AnimationId
109    where
110        A: Update + Send + 'static,
111        F: Fn() -> f64 + Send + 'static,
112    {
113        let id = AnimationId(self.next_id);
114        self.next_id += 1;
115        self.slots.push(Slot {
116            id,
117            animation: Box::new(anim),
118            remove: false,
119            recorder: Some(RecordedSampler {
120                label: label.into(),
121                sample: Box::new(sampler),
122            }),
123        });
124        id
125    }
126
127    /// Advance all active animations by `dt` seconds.
128    ///
129    /// Animations that return `false` from `update` are marked and removed
130    /// at the end of the tick — no allocation during the tick itself.
131    pub fn tick(&mut self, dt: f32) {
132        for slot in self.slots.iter_mut() {
133            if slot.remove {
134                continue;
135            }
136            let still_running = slot.animation.update(dt);
137            if !still_running {
138                slot.remove = true;
139            }
140        }
141        // Drain completed slots in one pass — O(n), no realloc.
142        self.slots.retain(|s| !s.remove);
143    }
144
145    /// Advance all active animations and record sampled values after the tick.
146    #[cfg(feature = "std")]
147    pub fn tick_recorded(&mut self, dt: f32, time: f32, recorder: &mut AnimationRecorder) {
148        for slot in self.slots.iter_mut() {
149            if slot.remove {
150                continue;
151            }
152            let still_running = slot.animation.update(dt);
153            if let Some(recorded) = &slot.recorder {
154                recorder.record(&recorded.label, time, (recorded.sample)());
155            }
156            if !still_running {
157                slot.remove = true;
158            }
159        }
160        self.slots.retain(|s| !s.remove);
161    }
162
163    /// Cancel an animation by id.
164    ///
165    /// The animation is removed immediately, before the next tick.
166    /// No-op if the id is not found (e.g. already completed).
167    pub fn cancel(&mut self, id: AnimationId) {
168        self.slots.retain(|s| s.id != id);
169    }
170
171    /// Cancel all active animations.
172    pub fn cancel_all(&mut self) {
173        self.slots.clear();
174    }
175
176    /// Number of currently active animations.
177    pub fn active_count(&self) -> usize {
178        self.slots.len()
179    }
180
181    /// `true` if the animation with the given id is still active.
182    pub fn is_active(&self, id: AnimationId) -> bool {
183        self.slots.iter().any(|s| s.id == id)
184    }
185}
186
187// ──────────────────────────────────────────────────────────────────────────────
188// Tests
189// ──────────────────────────────────────────────────────────────────────────────
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use animato_core::Easing;
195    use animato_tween::Tween;
196
197    #[test]
198    fn auto_removes_completed() {
199        let mut driver = AnimationDriver::new();
200        let id = driver.add(Tween::new(0.0_f32, 1.0).duration(1.0).build());
201        assert!(driver.is_active(id));
202        driver.tick(2.0); // well past duration
203        assert!(!driver.is_active(id));
204        assert_eq!(driver.active_count(), 0);
205    }
206
207    #[test]
208    fn cancel_removes_mid_animation() {
209        let mut driver = AnimationDriver::new();
210        let id = driver.add(Tween::new(0.0_f32, 1.0).duration(10.0).build());
211        driver.tick(1.0);
212        assert!(driver.is_active(id));
213        driver.cancel(id);
214        assert!(!driver.is_active(id));
215    }
216
217    #[test]
218    fn cancel_noop_on_missing_id() {
219        let mut driver = AnimationDriver::new();
220        let id = driver.add(Tween::new(0.0_f32, 1.0).duration(1.0).build());
221        driver.tick(2.0); // completes, auto-removed
222        driver.cancel(id); // should not panic
223        assert_eq!(driver.active_count(), 0);
224    }
225
226    #[test]
227    fn active_count_tracks_correctly() {
228        let mut driver = AnimationDriver::new();
229        let _a = driver.add(Tween::new(0.0_f32, 1.0).duration(1.0).build());
230        let _b = driver.add(Tween::new(0.0_f32, 1.0).duration(2.0).build());
231        let _c = driver.add(Tween::new(0.0_f32, 1.0).duration(3.0).build());
232        assert_eq!(driver.active_count(), 3);
233
234        driver.tick(1.5); // first one completes
235        assert_eq!(driver.active_count(), 2);
236
237        driver.tick(1.0); // second completes
238        assert_eq!(driver.active_count(), 1);
239    }
240
241    #[test]
242    fn cancel_all_clears_everything() {
243        let mut driver = AnimationDriver::new();
244        for _ in 0..10 {
245            driver.add(Tween::new(0.0_f32, 1.0).duration(5.0).build());
246        }
247        assert_eq!(driver.active_count(), 10);
248        driver.cancel_all();
249        assert_eq!(driver.active_count(), 0);
250    }
251
252    #[test]
253    fn multiple_concurrent_animations_tick_independently() {
254        let mut driver = AnimationDriver::new();
255        let slow = driver.add(Tween::new(0.0_f32, 1.0).duration(2.0).build());
256        let fast = driver.add(
257            Tween::new(0.0_f32, 1.0)
258                .duration(0.5)
259                .easing(Easing::Linear)
260                .build(),
261        );
262
263        driver.tick(1.0); // fast completes, slow still running
264        assert!(!driver.is_active(fast));
265        assert!(driver.is_active(slow));
266
267        driver.tick(2.0); // slow completes
268        assert!(!driver.is_active(slow));
269    }
270
271    #[test]
272    fn animation_id_is_unique() {
273        let mut driver = AnimationDriver::new();
274        let ids: Vec<_> = (0..100)
275            .map(|_| driver.add(Tween::new(0.0_f32, 1.0).duration(1.0).build()))
276            .collect();
277        let unique: std::collections::HashSet<_> = ids.iter().collect();
278        assert_eq!(unique.len(), 100);
279    }
280}