Skip to main content

embedded_gui/
animation.rs

1//! Core animation primitives for `embedded-gui`.
2//! Designed for deterministic, fixed-capacity operation.
3#[cfg(not(feature = "std"))]
4use crate::math::F32Ext as _;
5
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
7pub enum Easing {
8    #[default]
9    Linear,
10    EaseIn,
11    EaseOut,
12    EaseInOut,
13    Smoothstep,
14    InCubic,
15    OutCubic,
16    InOutCubic,
17    InQuart,
18    OutQuart,
19    InOutQuart,
20    InQuint,
21    OutQuint,
22    InOutQuint,
23    InSine,
24    OutSine,
25    InOutSine,
26    InExpo,
27    OutExpo,
28    InOutExpo,
29    InCirc,
30    OutCirc,
31    InOutCirc,
32    InBack,
33    OutBack,
34    InOutBack,
35    InBounce,
36    OutBounce,
37    InOutBounce,
38    InElastic,
39    OutElastic,
40    InOutElastic,
41    /// Spatial curve for stack push/pop (`interpolate_moook`).
42    Moook,
43}
44
45#[inline]
46pub fn apply_easing(t: f32, easing: Easing) -> f32 {
47    let t = t.clamp(0.0, 1.0);
48    const PI: f32 = core::f32::consts::PI;
49    #[inline]
50    fn out_bounce(t: f32) -> f32 {
51        const N1: f32 = 7.5625;
52        const D1: f32 = 2.75;
53        if t < 1.0 / D1 {
54            N1 * t * t
55        } else if t < 2.0 / D1 {
56            let t = t - 1.5 / D1;
57            N1 * t * t + 0.75
58        } else if t < 2.5 / D1 {
59            let t = t - 2.25 / D1;
60            N1 * t * t + 0.9375
61        } else {
62            let t = t - 2.625 / D1;
63            N1 * t * t + 0.984375
64        }
65    }
66    match easing {
67        Easing::Linear => t,
68        Easing::EaseIn => t * t,
69        Easing::EaseOut => 1.0 - (1.0 - t) * (1.0 - t),
70        Easing::EaseInOut => {
71            if t < 0.5 {
72                2.0 * t * t
73            } else {
74                1.0 - (-2.0 * t + 2.0).powi(2) * 0.5
75            }
76        }
77        Easing::Smoothstep => t * t * (3.0 - 2.0 * t),
78        Easing::InCubic => t.powi(3),
79        Easing::OutCubic => 1.0 - (1.0 - t).powi(3),
80        Easing::InOutCubic => {
81            if t < 0.5 {
82                4.0 * t.powi(3)
83            } else {
84                1.0 - (-2.0 * t + 2.0).powi(3) * 0.5
85            }
86        }
87        Easing::InQuart => t.powi(4),
88        Easing::OutQuart => 1.0 - (1.0 - t).powi(4),
89        Easing::InOutQuart => {
90            if t < 0.5 {
91                8.0 * t.powi(4)
92            } else {
93                1.0 - (-2.0 * t + 2.0).powi(4) * 0.5
94            }
95        }
96        Easing::InQuint => t.powi(5),
97        Easing::OutQuint => 1.0 - (1.0 - t).powi(5),
98        Easing::InOutQuint => {
99            if t < 0.5 {
100                16.0 * t.powi(5)
101            } else {
102                1.0 - (-2.0 * t + 2.0).powi(5) * 0.5
103            }
104        }
105        Easing::InSine => 1.0 - ((t * PI) / 2.0).cos(),
106        Easing::OutSine => ((t * PI) / 2.0).sin(),
107        Easing::InOutSine => -(PI * t).cos() * 0.5 + 0.5,
108        Easing::InExpo => {
109            if t <= 0.0 {
110                0.0
111            } else {
112                (2.0_f32).powf(10.0 * t - 10.0)
113            }
114        }
115        Easing::OutExpo => {
116            if t >= 1.0 {
117                1.0
118            } else {
119                1.0 - (2.0_f32).powf(-10.0 * t)
120            }
121        }
122        Easing::InOutExpo => {
123            if t <= 0.0 {
124                0.0
125            } else if t >= 1.0 {
126                1.0
127            } else if t < 0.5 {
128                (2.0_f32).powf(20.0 * t - 10.0) * 0.5
129            } else {
130                (2.0 - (2.0_f32).powf(-20.0 * t + 10.0)) * 0.5
131            }
132        }
133        Easing::InCirc => 1.0 - (1.0 - t * t).sqrt(),
134        Easing::OutCirc => (1.0 - (t - 1.0).powi(2)).sqrt(),
135        Easing::InOutCirc => {
136            if t < 0.5 {
137                (1.0 - (1.0 - (2.0 * t).powi(2)).sqrt()) * 0.5
138            } else {
139                ((1.0 - (-2.0 * t + 2.0).powi(2)).sqrt() + 1.0) * 0.5
140            }
141        }
142        Easing::InBack => {
143            let c1 = 1.70158;
144            let c3 = c1 + 1.0;
145            c3 * t.powi(3) - c1 * t.powi(2)
146        }
147        Easing::OutBack => {
148            let c1 = 1.70158;
149            let c3 = c1 + 1.0;
150            1.0 + c3 * (t - 1.0).powi(3) + c1 * (t - 1.0).powi(2)
151        }
152        Easing::InOutBack => {
153            let c1 = 1.70158;
154            let c2 = c1 * 1.525;
155            if t < 0.5 {
156                ((2.0 * t).powi(2) * ((c2 + 1.0) * 2.0 * t - c2)) * 0.5
157            } else {
158                ((2.0 * t - 2.0).powi(2) * ((c2 + 1.0) * (2.0 * t - 2.0) + c2) + 2.0) * 0.5
159            }
160        }
161        Easing::InBounce => 1.0 - out_bounce(1.0 - t),
162        Easing::OutBounce => out_bounce(t),
163        Easing::InOutBounce => {
164            if t < 0.5 {
165                (1.0 - out_bounce(1.0 - 2.0 * t)) * 0.5
166            } else {
167                (1.0 + out_bounce(2.0 * t - 1.0)) * 0.5
168            }
169        }
170        Easing::InElastic => {
171            if t <= 0.0 {
172                0.0
173            } else if t >= 1.0 {
174                1.0
175            } else {
176                let c4 = (2.0 * PI) / 3.0;
177                -(2.0_f32).powf(10.0 * t - 10.0) * ((t * 10.0 - 10.75) * c4).sin()
178            }
179        }
180        Easing::OutElastic => {
181            if t <= 0.0 {
182                0.0
183            } else if t >= 1.0 {
184                1.0
185            } else {
186                let c4 = (2.0 * PI) / 3.0;
187                (2.0_f32).powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
188            }
189        }
190        Easing::InOutElastic => {
191            if t <= 0.0 {
192                0.0
193            } else if t >= 1.0 {
194                1.0
195            } else {
196                let c5 = (2.0 * PI) / 4.5;
197                if t < 0.5 {
198                    -(2.0_f32).powf(20.0 * t - 10.0) * ((20.0 * t - 11.125) * c5).sin() * 0.5
199                } else {
200                    (2.0_f32).powf(-20.0 * t + 10.0) * ((20.0 * t - 11.125) * c5).sin() * 0.5 + 1.0
201                }
202            }
203        }
204        Easing::Moook => crate::animation_timing::moook_curve(t),
205    }
206}
207
208#[derive(Clone, Copy, Debug, PartialEq, Eq)]
209pub struct AnimationId(u16);
210
211impl AnimationId {
212    pub const fn new(id: u16) -> Self {
213        Self(id)
214    }
215
216    pub const fn raw(self) -> u16 {
217        self.0
218    }
219}
220
221#[derive(Clone, Copy, Debug, PartialEq, Eq)]
222pub enum AnimationError {
223    Full,
224}
225
226#[derive(Clone, Copy, Debug, PartialEq)]
227pub struct Timer {
228    pub duration_ms: u32,
229    pub elapsed_ms: u32,
230    pub repeating: bool,
231}
232
233impl Timer {
234    pub const fn new(duration_ms: u32) -> Self {
235        Self {
236            duration_ms,
237            elapsed_ms: 0,
238            repeating: false,
239        }
240    }
241
242    pub const fn repeating(duration_ms: u32) -> Self {
243        Self {
244            duration_ms,
245            elapsed_ms: 0,
246            repeating: true,
247        }
248    }
249
250    pub fn reset(&mut self) {
251        self.elapsed_ms = 0;
252    }
253
254    pub fn tick(&mut self, dt_ms: u32) -> bool {
255        self.elapsed_ms = self.elapsed_ms.saturating_add(dt_ms);
256        if self.elapsed_ms >= self.duration_ms {
257            if self.repeating && self.duration_ms > 0 {
258                self.elapsed_ms %= self.duration_ms;
259            }
260            true
261        } else {
262            false
263        }
264    }
265
266    pub fn progress(&self) -> f32 {
267        if self.duration_ms == 0 {
268            return 1.0;
269        }
270        (self.elapsed_ms as f32 / self.duration_ms as f32).min(1.0)
271    }
272}
273
274#[derive(Clone, Copy, Debug, PartialEq)]
275pub struct Tween {
276    pub from: f32,
277    pub to: f32,
278    pub timer: Timer,
279    pub easing: Easing,
280}
281
282impl Tween {
283    pub const fn new(from: f32, to: f32, duration_ms: u32, easing: Easing) -> Self {
284        Self {
285            from,
286            to,
287            timer: Timer::new(duration_ms),
288            easing,
289        }
290    }
291
292    pub fn reset(&mut self) {
293        self.timer.reset();
294    }
295
296    pub fn tick(&mut self, dt_ms: u32) -> bool {
297        self.timer.tick(dt_ms)
298    }
299
300    pub fn value(&self) -> f32 {
301        let t = apply_easing(self.timer.progress(), self.easing);
302        self.from + (self.to - self.from) * t
303    }
304
305    pub fn is_done(&self) -> bool {
306        self.timer.progress() >= 1.0
307    }
308}
309
310#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
311pub enum RepeatMode {
312    #[default]
313    Once,
314    Loop,
315    PingPong,
316}
317
318#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
319pub enum AnimationState {
320    #[default]
321    Running,
322    Finished,
323}
324
325#[allow(unpredictable_function_pointer_comparisons)]
326#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
327pub struct AnimationHandlers {
328    pub on_started: Option<fn()>,
329    pub on_stopped: Option<fn(bool)>,
330}
331
332#[allow(unpredictable_function_pointer_comparisons)]
333#[derive(Clone, Copy, Debug, PartialEq)]
334pub struct Animation {
335    pub from: f32,
336    pub to: f32,
337    pub duration_ms: u32,
338    pub easing: Easing,
339    pub custom_curve: Option<fn(f32) -> f32>,
340    pub custom_interpolator: Option<fn(f32, f32, f32) -> f32>,
341    pub handlers: AnimationHandlers,
342    pub delay_ms: u32,
343    pub repeat_mode: RepeatMode,
344    pub repeat_count: Option<u16>,
345    elapsed_ms: u32,
346    iteration: u16,
347    reversed: bool,
348    started: bool,
349    finished: bool,
350}
351
352impl Animation {
353    pub const fn new(from: f32, to: f32, duration_ms: u32, easing: Easing) -> Self {
354        Self {
355            from,
356            to,
357            duration_ms,
358            easing,
359            custom_curve: None,
360            custom_interpolator: None,
361            handlers: AnimationHandlers {
362                on_started: None,
363                on_stopped: None,
364            },
365            delay_ms: 0,
366            repeat_mode: RepeatMode::Once,
367            repeat_count: None,
368            elapsed_ms: 0,
369            iteration: 0,
370            reversed: false,
371            started: false,
372            finished: false,
373        }
374    }
375
376    pub const fn with_delay(mut self, delay_ms: u32) -> Self {
377        self.delay_ms = delay_ms;
378        self
379    }
380
381    pub const fn with_repeat_mode(mut self, repeat_mode: RepeatMode) -> Self {
382        self.repeat_mode = repeat_mode;
383        self
384    }
385
386    pub const fn with_repeat_count(mut self, repeat_count: Option<u16>) -> Self {
387        self.repeat_count = repeat_count;
388        self
389    }
390
391    pub fn with_custom_curve(mut self, curve: fn(f32) -> f32) -> Self {
392        self.custom_curve = Some(curve);
393        self
394    }
395
396    pub fn clear_custom_curve(&mut self) {
397        self.custom_curve = None;
398    }
399
400    pub fn with_custom_interpolator(mut self, interpolator: fn(f32, f32, f32) -> f32) -> Self {
401        self.custom_interpolator = Some(interpolator);
402        self
403    }
404
405    pub fn clear_custom_interpolator(&mut self) {
406        self.custom_interpolator = None;
407    }
408
409    pub fn set_reversed(&mut self, reversed: bool) {
410        self.reversed = reversed;
411    }
412
413    pub fn set_handlers(&mut self, handlers: AnimationHandlers) {
414        self.handlers = handlers;
415    }
416
417    pub fn reset(&mut self) {
418        self.elapsed_ms = 0;
419        self.iteration = 0;
420        self.started = false;
421        self.finished = false;
422    }
423
424    pub fn set_elapsed(&mut self, elapsed_ms: u32) {
425        self.elapsed_ms = elapsed_ms;
426        self.finished = self.resolve_finished();
427    }
428
429    pub fn tick(&mut self, dt_ms: u32) -> AnimationState {
430        if self.finished {
431            return AnimationState::Finished;
432        }
433        self.elapsed_ms = self.elapsed_ms.saturating_add(dt_ms);
434        self.emit_started_if_ready();
435        self.finished = self.resolve_finished();
436        if self.finished {
437            if let Some(cb) = self.handlers.on_stopped {
438                cb(true);
439            }
440        }
441        if self.finished {
442            AnimationState::Finished
443        } else {
444            AnimationState::Running
445        }
446    }
447
448    pub fn value(&self) -> f32 {
449        if self.delay_ms > 0 && self.elapsed_ms < self.delay_ms {
450            return if self.reversed { self.to } else { self.from };
451        }
452
453        let duration = self.duration_ms.max(1);
454        let active_elapsed = self.elapsed_ms.saturating_sub(self.delay_ms);
455        let local_time = active_elapsed % duration;
456        let mut progress = local_time as f32 / duration as f32;
457
458        if self.finished && self.repeat_mode == RepeatMode::Once {
459            progress = 1.0;
460        }
461
462        let iteration = self.current_iteration();
463        let ping_pong_reverse = self.repeat_mode == RepeatMode::PingPong && (iteration % 2 == 1);
464        if ping_pong_reverse {
465            progress = 1.0 - progress;
466        }
467        if self.reversed {
468            progress = 1.0 - progress;
469        }
470
471        let t = if let Some(curve) = self.custom_curve {
472            curve(progress)
473        } else {
474            apply_easing(progress, self.easing)
475        };
476        if let Some(interpolator) = self.custom_interpolator {
477            interpolator(self.from, self.to, t)
478        } else {
479            self.from + (self.to - self.from) * t
480        }
481    }
482
483    pub fn is_done(&self) -> bool {
484        self.finished
485    }
486
487    pub fn elapsed_ms(&self) -> u32 {
488        self.elapsed_ms
489    }
490
491    pub fn iteration(&self) -> u16 {
492        self.current_iteration()
493    }
494
495    fn current_iteration(&self) -> u16 {
496        if self.delay_ms > 0 && self.elapsed_ms < self.delay_ms {
497            return 0;
498        }
499        let duration = self.duration_ms.max(1);
500        let active_elapsed = self.elapsed_ms.saturating_sub(self.delay_ms);
501        (active_elapsed / duration) as u16
502    }
503
504    fn resolve_finished(&mut self) -> bool {
505        if self.repeat_mode != RepeatMode::Once {
506            if let Some(limit) = self.repeat_count {
507                let iteration = self.current_iteration();
508                self.iteration = iteration;
509                return iteration >= limit;
510            }
511            return false;
512        }
513
514        let total = self.delay_ms.saturating_add(self.duration_ms);
515        self.elapsed_ms >= total
516    }
517
518    pub(crate) fn notify_stopped(&mut self, finished: bool) {
519        if let Some(cb) = self.handlers.on_stopped {
520            cb(finished);
521        }
522    }
523
524    fn emit_started_if_ready(&mut self) {
525        if self.started {
526            return;
527        }
528        if self.delay_ms > 0 && self.elapsed_ms < self.delay_ms {
529            return;
530        }
531        self.started = true;
532        if let Some(cb) = self.handlers.on_started {
533            cb();
534        }
535    }
536
537    pub fn duration_from_speed(delta: f32, units_per_second: f32) -> u32 {
538        if delta <= 0.0 || units_per_second <= 0.0 {
539            return 0;
540        }
541        ((delta / units_per_second) * 1000.0).ceil() as u32
542    }
543
544    pub fn total_duration_ms(
545        &self,
546        include_delay: bool,
547        include_repeat_count: bool,
548    ) -> Option<u32> {
549        let base = if include_delay {
550            self.duration_ms.saturating_add(self.delay_ms)
551        } else {
552            self.duration_ms
553        };
554        if !include_repeat_count || self.repeat_mode == RepeatMode::Once {
555            return Some(base);
556        }
557        self.repeat_count
558            .map(|count| base.saturating_mul(count as u32))
559    }
560}
561
562#[derive(Clone, Copy, Debug, PartialEq)]
563struct AnimationTrack {
564    id: AnimationId,
565    animation: Animation,
566    last_iteration: u16,
567}
568
569#[allow(unpredictable_function_pointer_comparisons)]
570#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
571pub struct AnimationManagerCallbacks {
572    pub on_start: Option<fn(AnimationId)>,
573    pub on_repeat: Option<fn(AnimationId, u16)>,
574    pub on_complete: Option<fn(AnimationId, bool)>,
575}
576
577#[derive(Clone, Copy, Debug, PartialEq)]
578pub struct AnimationManager<const N: usize> {
579    tracks: [Option<AnimationTrack>; N],
580    next_id: u16,
581    paused: bool,
582    callbacks: AnimationManagerCallbacks,
583}
584
585impl<const N: usize> Default for AnimationManager<N> {
586    fn default() -> Self {
587        Self::new()
588    }
589}
590
591impl<const N: usize> AnimationManager<N> {
592    pub const fn new() -> Self {
593        Self {
594            tracks: [None; N],
595            next_id: 1,
596            paused: false,
597            callbacks: AnimationManagerCallbacks {
598                on_start: None,
599                on_repeat: None,
600                on_complete: None,
601            },
602        }
603    }
604
605    pub fn set_callbacks(&mut self, callbacks: AnimationManagerCallbacks) {
606        self.callbacks = callbacks;
607    }
608
609    pub fn start(&mut self, animation: Animation) -> Result<AnimationId, AnimationError> {
610        let id = AnimationId::new(self.next_id);
611        self.next_id = self.next_id.wrapping_add(1).max(1);
612
613        if let Some(slot) = self.tracks.iter_mut().find(|slot| slot.is_none()) {
614            *slot = Some(AnimationTrack {
615                id,
616                animation,
617                last_iteration: 0,
618            });
619            if let Some(cb) = self.callbacks.on_start {
620                cb(id);
621            }
622            Ok(id)
623        } else {
624            Err(AnimationError::Full)
625        }
626    }
627
628    pub fn stop(&mut self, id: AnimationId) -> bool {
629        for slot in &mut self.tracks {
630            if slot.as_ref().is_some_and(|track| track.id == id) {
631                if let Some(track) = slot.as_mut() {
632                    track.animation.notify_stopped(false);
633                }
634                *slot = None;
635                if let Some(cb) = self.callbacks.on_complete {
636                    cb(id, false);
637                }
638                return true;
639            }
640        }
641        false
642    }
643
644    pub fn tick(&mut self, dt_ms: u32) {
645        if self.paused {
646            return;
647        }
648        for slot in &mut self.tracks {
649            if let Some(track) = slot.as_mut() {
650                track.animation.tick(dt_ms);
651                let iteration = track.animation.iteration();
652                if iteration > track.last_iteration {
653                    track.last_iteration = iteration;
654                    if let Some(cb) = self.callbacks.on_repeat {
655                        cb(track.id, iteration);
656                    }
657                }
658                if track.animation.is_done() {
659                    if let Some(cb) = self.callbacks.on_complete {
660                        cb(track.id, true);
661                    }
662                    *slot = None;
663                }
664            }
665        }
666    }
667
668    pub fn value(&self, id: AnimationId) -> Option<f32> {
669        self.tracks
670            .iter()
671            .flatten()
672            .find(|track| track.id == id)
673            .map(|track| track.animation.value())
674    }
675
676    pub fn animation(&self, id: AnimationId) -> Option<&Animation> {
677        self.tracks
678            .iter()
679            .flatten()
680            .find(|track| track.id == id)
681            .map(|track| &track.animation)
682    }
683
684    pub fn animation_mut(&mut self, id: AnimationId) -> Option<&mut Animation> {
685        self.tracks
686            .iter_mut()
687            .flatten()
688            .find(|track| track.id == id)
689            .map(|track| &mut track.animation)
690    }
691
692    pub fn active_count(&self) -> usize {
693        self.tracks.iter().flatten().count()
694    }
695
696    pub fn set_paused(&mut self, paused: bool) {
697        self.paused = paused;
698    }
699
700    pub fn is_paused(&self) -> bool {
701        self.paused
702    }
703
704    pub fn seek(&mut self, id: AnimationId, elapsed_ms: u32) -> bool {
705        if let Some(track) = self
706            .tracks
707            .iter_mut()
708            .flatten()
709            .find(|track| track.id == id)
710        {
711            track.animation.set_elapsed(elapsed_ms);
712            track.last_iteration = track.animation.iteration();
713            true
714        } else {
715            false
716        }
717    }
718
719    pub fn seek_stepped(&mut self, id: AnimationId, elapsed_ms: u32, step_ms: u32) -> bool {
720        let Some(track) = self
721            .tracks
722            .iter_mut()
723            .flatten()
724            .find(|track| track.id == id)
725        else {
726            return false;
727        };
728        let step = step_ms.max(1);
729        let current = track.animation.elapsed_ms();
730        if elapsed_ms <= current {
731            track.animation.set_elapsed(elapsed_ms);
732            track.last_iteration = track.animation.iteration();
733            return true;
734        }
735        let mut cursor = current;
736        while cursor < elapsed_ms {
737            cursor = core::cmp::min(cursor.saturating_add(step), elapsed_ms);
738            track.animation.set_elapsed(cursor);
739        }
740        track.last_iteration = track.animation.iteration();
741        true
742    }
743
744    pub fn replay_stepped<F>(
745        &mut self,
746        id: AnimationId,
747        elapsed_ms: u32,
748        step_ms: u32,
749        mut on_sample: F,
750    ) -> bool
751    where
752        F: FnMut(f32),
753    {
754        let Some(track) = self
755            .tracks
756            .iter_mut()
757            .flatten()
758            .find(|track| track.id == id)
759        else {
760            return false;
761        };
762        let step = step_ms.max(1);
763        let current = track.animation.elapsed_ms();
764        if elapsed_ms <= current {
765            track.animation.set_elapsed(elapsed_ms);
766            on_sample(track.animation.value());
767            track.last_iteration = track.animation.iteration();
768            return true;
769        }
770        let mut cursor = current;
771        while cursor < elapsed_ms {
772            cursor = core::cmp::min(cursor.saturating_add(step), elapsed_ms);
773            track.animation.set_elapsed(cursor);
774            on_sample(track.animation.value());
775        }
776        track.last_iteration = track.animation.iteration();
777        true
778    }
779
780    pub fn set_next_id_for_test(&mut self, id: u16) {
781        self.next_id = id.max(1);
782    }
783}
784
785#[derive(Clone, Copy, Debug, PartialEq)]
786pub struct SpringAnimator {
787    pub value: f32,
788    pub velocity: f32,
789    pub target: f32,
790    pub stiffness: f32,
791    pub damping: f32,
792}
793
794impl SpringAnimator {
795    pub const fn new(value: f32, target: f32) -> Self {
796        Self {
797            value,
798            velocity: 0.0,
799            target,
800            stiffness: 120.0,
801            damping: 16.0,
802        }
803    }
804
805    pub fn tick(&mut self, dt_ms: u32) -> f32 {
806        let dt = (dt_ms as f32 / 1000.0).max(0.001);
807        let force = self.stiffness * (self.target - self.value) - self.damping * self.velocity;
808        self.velocity += force * dt;
809        self.value += self.velocity * dt;
810        self.value
811    }
812}
813
814#[derive(Clone, Copy, Debug, PartialEq)]
815pub struct InertiaAnimator {
816    pub value: f32,
817    pub velocity: f32,
818    pub friction_per_second: f32,
819}
820
821impl InertiaAnimator {
822    pub const fn new(value: f32, velocity: f32) -> Self {
823        Self {
824            value,
825            velocity,
826            friction_per_second: 0.88,
827        }
828    }
829
830    pub fn tick(&mut self, dt_ms: u32) -> f32 {
831        let dt = (dt_ms as f32 / 1000.0).max(0.001);
832        self.value += self.velocity * dt;
833        self.velocity *= self.friction_per_second.powf(dt);
834        self.value
835    }
836}
837
838#[derive(Clone, Copy, Debug, PartialEq)]
839pub struct PathPoint {
840    pub x: f32,
841    pub y: f32,
842}
843
844impl PathPoint {
845    pub const fn new(x: f32, y: f32) -> Self {
846        Self { x, y }
847    }
848}
849
850#[derive(Clone, Copy, Debug, PartialEq)]
851pub struct PathAnimator<const N: usize> {
852    points: [Option<PathPoint>; N],
853    len: usize,
854    pub timer: Timer,
855    pub easing: Easing,
856}
857
858impl<const N: usize> PathAnimator<N> {
859    pub const fn new(duration_ms: u32, easing: Easing) -> Self {
860        Self {
861            points: [None; N],
862            len: 0,
863            timer: Timer::new(duration_ms),
864            easing,
865        }
866    }
867
868    pub fn push_point(&mut self, point: PathPoint) -> Result<(), AnimationError> {
869        if self.len >= N {
870            return Err(AnimationError::Full);
871        }
872        self.points[self.len] = Some(point);
873        self.len += 1;
874        Ok(())
875    }
876
877    pub fn reset(&mut self) {
878        self.timer.reset();
879    }
880
881    pub fn tick(&mut self, dt_ms: u32) -> bool {
882        self.timer.tick(dt_ms)
883    }
884
885    pub fn value(&self) -> Option<PathPoint> {
886        if self.len == 0 {
887            return None;
888        }
889        if self.len == 1 {
890            return self.points[0];
891        }
892        let t = apply_easing(self.timer.progress(), self.easing);
893        let segs = (self.len - 1) as f32;
894        let pos = (t * segs).clamp(0.0, segs);
895        let idx = pos.floor() as usize;
896        let local = pos - idx as f32;
897        let a = self.points[idx]?;
898        let b = self.points[(idx + 1).min(self.len - 1)]?;
899        Some(PathPoint {
900            x: a.x + (b.x - a.x) * local,
901            y: a.y + (b.y - a.y) * local,
902        })
903    }
904}