Skip to main content

agpu/
animation.rs

1//! Animation framework — declarative animations with easing functions.
2//!
3//! Provides [`Animation`], [`Easing`], and [`AnimationManager`] for
4//! smooth property transitions and timed sequences.
5
6use std::time::Duration;
7
8/// Easing function for animation interpolation.
9#[derive(Debug, Clone, Copy, PartialEq, Default)]
10pub enum Easing {
11    Linear,
12    EaseIn,
13    EaseOut,
14    #[default]
15    EaseInOut,
16    EaseInQuad,
17    EaseOutQuad,
18    EaseInOutQuad,
19    EaseInCubic,
20    EaseOutCubic,
21    EaseInOutCubic,
22    Spring {
23        stiffness: f32,
24        damping: f32,
25    },
26}
27
28impl Easing {
29    /// Apply the easing function to a linear progress value in [0.0, 1.0].
30    pub fn apply(&self, t: f32) -> f32 {
31        let t = t.clamp(0.0, 1.0);
32        match self {
33            Self::Linear => t,
34            Self::EaseIn => t * t,
35            Self::EaseOut => t * (2.0 - t),
36            Self::EaseInOut => {
37                if t < 0.5 {
38                    2.0 * t * t
39                } else {
40                    -1.0 + (4.0 - 2.0 * t) * t
41                }
42            }
43            Self::EaseInQuad => t * t,
44            Self::EaseOutQuad => 1.0 - (1.0 - t) * (1.0 - t),
45            Self::EaseInOutQuad => {
46                if t < 0.5 {
47                    2.0 * t * t
48                } else {
49                    1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
50                }
51            }
52            Self::EaseInCubic => t * t * t,
53            Self::EaseOutCubic => 1.0 - (1.0 - t).powi(3),
54            Self::EaseInOutCubic => {
55                if t < 0.5 {
56                    4.0 * t * t * t
57                } else {
58                    1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
59                }
60            }
61            Self::Spring { stiffness, damping } => {
62                let omega = stiffness.sqrt();
63                let zeta = damping / (2.0 * omega);
64                if zeta < 1.0 {
65                    let wd = omega * (1.0 - zeta * zeta).sqrt();
66                    1.0 - (-zeta * omega * t).exp()
67                        * ((wd * t).cos() + zeta / (1.0 - zeta * zeta).sqrt() * (wd * t).sin())
68                } else {
69                    1.0 - (1.0 + omega * t) * (-omega * t).exp()
70                }
71            }
72        }
73    }
74}
75
76/// State of an animation.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum AnimationState {
79    Pending,
80    Running,
81    Paused,
82    Completed,
83}
84
85/// A single animation that interpolates a value over time.
86#[derive(Debug, Clone)]
87pub struct Animation {
88    id: String,
89    from: f64,
90    to: f64,
91    duration: Duration,
92    delay: Duration,
93    easing: Easing,
94    repeat: RepeatMode,
95    state: AnimationState,
96    elapsed: Duration,
97}
98
99/// How an animation repeats.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum RepeatMode {
102    Once,
103    Loop,
104    PingPong,
105    Count(u32),
106}
107
108impl Animation {
109    pub fn new(id: impl Into<String>, from: f64, to: f64, duration: Duration) -> Self {
110        Self {
111            id: id.into(),
112            from,
113            to,
114            duration,
115            delay: Duration::ZERO,
116            easing: Easing::default(),
117            repeat: RepeatMode::Once,
118            state: AnimationState::Pending,
119            elapsed: Duration::ZERO,
120        }
121    }
122
123    pub fn easing(mut self, easing: Easing) -> Self {
124        self.easing = easing;
125        self
126    }
127
128    pub fn delay(mut self, delay: Duration) -> Self {
129        self.delay = delay;
130        self
131    }
132
133    pub fn repeat(mut self, mode: RepeatMode) -> Self {
134        self.repeat = mode;
135        self
136    }
137
138    pub fn id(&self) -> &str {
139        &self.id
140    }
141
142    pub fn state(&self) -> AnimationState {
143        self.state
144    }
145
146    /// Start or resume the animation.
147    pub fn start(&mut self) {
148        self.state = AnimationState::Running;
149    }
150
151    /// Pause the animation.
152    pub fn pause(&mut self) {
153        if self.state == AnimationState::Running {
154            self.state = AnimationState::Paused;
155        }
156    }
157
158    /// Reset to the beginning.
159    pub fn reset(&mut self) {
160        self.elapsed = Duration::ZERO;
161        self.state = AnimationState::Pending;
162    }
163
164    /// Advance the animation by the given delta time. Returns the current value.
165    pub fn tick(&mut self, dt: Duration) -> f64 {
166        if self.state != AnimationState::Running {
167            return self.current_value();
168        }
169
170        self.elapsed += dt;
171
172        // Handle delay
173        if self.elapsed < self.delay {
174            return self.from;
175        }
176
177        let active_elapsed = self.elapsed - self.delay;
178        let raw_t = active_elapsed.as_secs_f64() / self.duration.as_secs_f64();
179
180        let t = match self.repeat {
181            RepeatMode::Once => {
182                if raw_t >= 1.0 {
183                    self.state = AnimationState::Completed;
184                    1.0
185                } else {
186                    raw_t
187                }
188            }
189            RepeatMode::Loop => raw_t.fract(),
190            RepeatMode::PingPong => {
191                let cycle = raw_t % 2.0;
192                if cycle > 1.0 { 2.0 - cycle } else { cycle }
193            }
194            RepeatMode::Count(n) => {
195                if raw_t >= n as f64 {
196                    self.state = AnimationState::Completed;
197                    1.0
198                } else {
199                    raw_t.fract()
200                }
201            }
202        };
203
204        let eased = self.easing.apply(t as f32) as f64;
205        self.from + (self.to - self.from) * eased
206    }
207
208    fn current_value(&self) -> f64 {
209        match self.state {
210            AnimationState::Pending => self.from,
211            AnimationState::Completed => self.to,
212            _ => {
213                if self.elapsed < self.delay {
214                    return self.from;
215                }
216                let active = self.elapsed - self.delay;
217                let t = (active.as_secs_f64() / self.duration.as_secs_f64()).clamp(0.0, 1.0);
218                let eased = self.easing.apply(t as f32) as f64;
219                self.from + (self.to - self.from) * eased
220            }
221        }
222    }
223}
224
225/// Manages a collection of active animations.
226pub struct AnimationManager {
227    animations: Vec<Animation>,
228}
229
230impl AnimationManager {
231    pub fn new() -> Self {
232        Self {
233            animations: Vec::new(),
234        }
235    }
236
237    pub fn add(&mut self, mut anim: Animation) {
238        anim.start();
239        self.animations.push(anim);
240    }
241
242    /// Tick all animations by dt, remove completed ones. Returns values keyed by id.
243    pub fn tick(&mut self, dt: Duration) -> Vec<(String, f64)> {
244        let values: Vec<(String, f64)> = self
245            .animations
246            .iter_mut()
247            .map(|a| {
248                let val = a.tick(dt);
249                (a.id().to_string(), val)
250            })
251            .collect();
252
253        // Remove completed non-repeating animations
254        self.animations
255            .retain(|a| a.state != AnimationState::Completed);
256
257        values
258    }
259
260    pub fn get(&self, id: &str) -> Option<&Animation> {
261        self.animations.iter().find(|a| a.id() == id)
262    }
263
264    pub fn cancel(&mut self, id: &str) {
265        self.animations.retain(|a| a.id() != id);
266    }
267
268    pub fn active_count(&self) -> usize {
269        self.animations.len()
270    }
271}
272
273impl Default for AnimationManager {
274    fn default() -> Self {
275        Self::new()
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn easing_linear() {
285        assert!((Easing::Linear.apply(0.5) - 0.5).abs() < 0.001);
286    }
287
288    #[test]
289    fn easing_endpoints() {
290        for easing in [
291            Easing::Linear,
292            Easing::EaseIn,
293            Easing::EaseOut,
294            Easing::EaseInOut,
295            Easing::EaseInCubic,
296            Easing::EaseOutCubic,
297        ] {
298            assert!((easing.apply(0.0)).abs() < 0.001, "{easing:?} at 0");
299            assert!((easing.apply(1.0) - 1.0).abs() < 0.001, "{easing:?} at 1");
300        }
301    }
302
303    #[test]
304    fn easing_clamps() {
305        assert!((Easing::Linear.apply(-0.5)).abs() < 0.001);
306        assert!((Easing::Linear.apply(1.5) - 1.0).abs() < 0.001);
307    }
308
309    #[test]
310    fn animation_basic() {
311        let mut anim = Animation::new("x", 0.0, 100.0, Duration::from_millis(1000));
312        anim.start();
313        let val = anim.tick(Duration::from_millis(500));
314        assert!(val > 0.0 && val < 100.0);
315    }
316
317    #[test]
318    fn animation_completes() {
319        let mut anim = Animation::new("x", 0.0, 100.0, Duration::from_millis(100));
320        anim.start();
321        let val = anim.tick(Duration::from_millis(200));
322        assert!((val - 100.0).abs() < 0.01);
323        assert_eq!(anim.state(), AnimationState::Completed);
324    }
325
326    #[test]
327    fn animation_delay() {
328        let mut anim = Animation::new("x", 0.0, 1.0, Duration::from_millis(100))
329            .delay(Duration::from_millis(50));
330        anim.start();
331        let val = anim.tick(Duration::from_millis(25));
332        assert!((val - 0.0).abs() < 0.001); // Still in delay
333    }
334
335    #[test]
336    fn animation_pause_resume() {
337        let mut anim = Animation::new("x", 0.0, 100.0, Duration::from_millis(1000));
338        anim.start();
339        anim.tick(Duration::from_millis(300));
340        anim.pause();
341        let val_paused = anim.tick(Duration::from_millis(500));
342        anim.start();
343        let val_resumed = anim.tick(Duration::from_millis(100));
344        assert!(val_resumed > val_paused || (val_resumed - val_paused).abs() < 0.01);
345    }
346
347    #[test]
348    fn animation_reset() {
349        let mut anim = Animation::new("x", 0.0, 100.0, Duration::from_millis(100));
350        anim.start();
351        anim.tick(Duration::from_millis(200));
352        anim.reset();
353        assert_eq!(anim.state(), AnimationState::Pending);
354    }
355
356    #[test]
357    fn animation_loop() {
358        let mut anim =
359            Animation::new("x", 0.0, 1.0, Duration::from_millis(100)).repeat(RepeatMode::Loop);
360        anim.start();
361        anim.tick(Duration::from_millis(150));
362        assert_eq!(anim.state(), AnimationState::Running); // Still going
363    }
364
365    #[test]
366    fn animation_pingpong() {
367        let mut anim = Animation::new("x", 0.0, 1.0, Duration::from_millis(100))
368            .repeat(RepeatMode::PingPong)
369            .easing(Easing::Linear);
370        anim.start();
371        // At t=150ms (1.5 cycles), should be going back towards 0
372        let val = anim.tick(Duration::from_millis(150));
373        assert!(val < 0.6);
374    }
375
376    #[test]
377    fn manager_add_and_tick() {
378        let mut mgr = AnimationManager::new();
379        mgr.add(Animation::new("a", 0.0, 1.0, Duration::from_millis(100)));
380        mgr.add(Animation::new("b", 10.0, 20.0, Duration::from_millis(200)));
381        let vals = mgr.tick(Duration::from_millis(50));
382        assert_eq!(vals.len(), 2);
383    }
384
385    #[test]
386    fn manager_removes_completed() {
387        let mut mgr = AnimationManager::new();
388        mgr.add(Animation::new("short", 0.0, 1.0, Duration::from_millis(50)));
389        mgr.add(Animation::new("long", 0.0, 1.0, Duration::from_millis(500)));
390        mgr.tick(Duration::from_millis(100));
391        assert_eq!(mgr.active_count(), 1);
392    }
393
394    #[test]
395    fn manager_cancel() {
396        let mut mgr = AnimationManager::new();
397        mgr.add(Animation::new("a", 0.0, 1.0, Duration::from_millis(500)));
398        mgr.cancel("a");
399        assert_eq!(mgr.active_count(), 0);
400    }
401
402    #[test]
403    fn easing_spring() {
404        let spring = Easing::Spring {
405            stiffness: 100.0,
406            damping: 10.0,
407        };
408        let val = spring.apply(0.5);
409        assert!(val > 0.0);
410    }
411}