Skip to main content

matrix_gui/
animation.rs

1//! Animation subsystem for the matrix_gui framework.
2//!
3//! This module provides a lightweight animation system inspired by LVGL 8,
4//! designed for immediate-mode embedded GUI applications.
5//!
6//! # Features
7//!
8//! - Multiple easing functions using integer-only math (no floating point)
9//! - Support for value animations with callbacks
10//! - Animation management with play, pause, stop controls
11//! - Memory-efficient design suitable for embedded systems
12//! - `no_std` compatible
13//!
14//! # Core Components
15//!
16//! - [`Anim`]: Animation definition with start/end values, duration, and easing
17//! - [`Easing`]: Easing functions for smooth animations (integer-based)
18//! - [`AnimManager`]: Manages multiple active animations
19//! - [`AnimCallback`]: Callback trait for animation value updates
20
21use core::cell::Cell;
22use core::fmt::Debug;
23use core::time::Duration;
24
25/// Scaling factor for fixed-point calculations.
26/// Values are scaled to 0..=ANIM_SCALE range for integer math.
27pub const ANIM_SCALE: i32 = 1024;
28
29/// Easing functions for animations.
30///
31/// These functions define how animation progress changes over time,
32/// creating smooth and natural-looking motion.
33///
34/// All calculations use integer-only math with fixed-point arithmetic.
35/// The input progress is in range [0, ANIM_SCALE] and output is also
36/// in range [0, ANIM_SCALE].
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum Easing {
39    /// Linear interpolation - constant speed.
40    Linear,
41    /// Ease-in - slow start, fast end.
42    EaseIn,
43    /// Ease-out - fast start, slow end.
44    #[default]
45    EaseOut,
46    /// Ease-in-out - slow start and end.
47    EaseInOut,
48}
49
50impl Easing {
51    /// Calculates the eased value for a given progress using integer math.
52    ///
53    /// # Arguments
54    ///
55    /// * `progress` - Animation progress in range [0, ANIM_SCALE]
56    ///
57    /// # Returns
58    ///
59    /// The eased progress value in range [0, ANIM_SCALE]
60    /// (some easing functions like elastic may slightly exceed this range).
61    pub fn calc(&self, progress: i32) -> i32 {
62        let t = progress.clamp(0, ANIM_SCALE);
63        match self {
64            Easing::Linear => t,
65
66            Easing::EaseIn => mul_div(t, t, ANIM_SCALE),
67
68            Easing::EaseOut => {
69                let inv_t = ANIM_SCALE - t;
70                ANIM_SCALE - mul_div(inv_t, inv_t, ANIM_SCALE)
71            }
72
73            Easing::EaseInOut => {
74                if t < ANIM_SCALE / 2 {
75                    2 * mul_div(t, t, ANIM_SCALE)
76                } else {
77                    let inv_t = ANIM_SCALE - t;
78                    ANIM_SCALE - 2 * mul_div(inv_t, inv_t, ANIM_SCALE)
79                }
80            }
81        }
82    }
83}
84
85/// Safe multiplication with division, avoiding overflow.
86/// Returns (a * b / c) with proper handling.
87#[inline]
88const fn mul_div(a: i32, b: i32, c: i32) -> i32 {
89    (a * b) / c
90}
91
92/// Animation playback state.
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
94pub enum AnimState {
95    /// Animation is not playing.
96    #[default]
97    Stopped,
98    /// Animation is playing.
99    Playing,
100    /// Animation is paused.
101    Paused,
102}
103
104/// Unique identifier for an animation.
105pub type AnimId = u16;
106
107/// Animation playback options.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct AnimOptions {
110    /// Number of times to repeat the animation (0 = infinite).
111    pub repeat_count: u16,
112    /// Whether to reverse the animation on each repeat.
113    pub reverse: bool,
114    /// Delay before starting the animation.
115    pub start_delay: Duration,
116    /// Whether to play the animation in reverse initially.
117    pub play_backward: bool,
118}
119
120impl Default for AnimOptions {
121    fn default() -> Self {
122        Self {
123            repeat_count: 1,
124            reverse: false,
125            start_delay: Duration::ZERO,
126            play_backward: false,
127        }
128    }
129}
130
131impl AnimOptions {
132    /// Creates new animation options with default values.
133    pub const fn new() -> Self {
134        Self {
135            repeat_count: 1,
136            reverse: false,
137            start_delay: Duration::ZERO,
138            play_backward: false,
139        }
140    }
141
142    /// Sets the repeat count (0 = infinite).
143    pub const fn with_repeat(mut self, count: u16) -> Self {
144        self.repeat_count = count;
145        self
146    }
147
148    /// Enables reverse playback on repeat.
149    pub const fn with_reverse(mut self, reverse: bool) -> Self {
150        self.reverse = reverse;
151        self
152    }
153
154    /// Sets the start delay.
155    pub const fn with_start_delay(mut self, delay: Duration) -> Self {
156        self.start_delay = delay;
157        self
158    }
159
160    /// Sets whether to play backward initially.
161    pub const fn with_play_backward(mut self, backward: bool) -> Self {
162        self.play_backward = backward;
163        self
164    }
165}
166
167/// Animation definition.
168///
169/// This struct defines an animation with start/end values, duration,
170/// easing function, and callback.
171#[derive(Debug, Clone)]
172pub struct Anim {
173    /// Starting value of the animation.
174    pub start_value: i32,
175    /// Ending value of the animation.
176    pub end_value: i32,
177    /// Duration of the animation.
178    pub duration: Duration,
179    /// Easing function for the animation.
180    pub easing: Easing,
181    /// Animation options.
182    pub options: AnimOptions,
183}
184
185impl Anim {
186    /// Creates a new animation with the given parameters.
187    ///
188    /// # Arguments
189    ///
190    /// * `start_value` - Starting value
191    /// * `end_value` - Ending value
192    /// * `duration` - Duration of the animation
193    /// * `callback` - Callback for value updates
194    pub const fn new(start_value: i32, end_value: i32, duration: Duration) -> Self {
195        Self {
196            start_value,
197            end_value,
198            duration,
199            easing: Easing::Linear,
200            options: AnimOptions::new(),
201        }
202    }
203
204    /// Sets the easing function.
205    pub const fn with_easing(mut self, easing: Easing) -> Self {
206        self.easing = easing;
207        self
208    }
209
210    /// Sets the animation options.
211    pub const fn with_options(mut self, options: AnimOptions) -> Self {
212        self.options = options;
213        self
214    }
215
216    /// Sets whether to reverse on repeat.
217    pub const fn with_reverse(mut self, reverse: bool) -> Self {
218        self.options.reverse = reverse;
219        self
220    }
221
222    /// Sets the repeat count (0 = infinite).
223    pub const fn with_repeat(mut self, count: u16) -> Self {
224        self.options.repeat_count = count;
225        self
226    }
227
228    /// Sets the start delay.
229    pub const fn with_start_delay(mut self, delay: Duration) -> Self {
230        self.options.start_delay = delay;
231        self
232    }
233
234    /// Calculates the current value based on progress.
235    ///
236    /// # Arguments
237    ///
238    /// * `progress` - Animation progress in range [0, ANIM_SCALE]
239    ///
240    /// # Returns
241    ///
242    /// The interpolated value between start and end.
243    pub fn calc_value(&self, progress: i32) -> i32 {
244        let eased_progress = self.easing.calc(progress);
245        let range = self.end_value - self.start_value;
246        self.start_value + mul_div(range, eased_progress, ANIM_SCALE)
247    }
248}
249
250const INVALID_ANIM_ID: AnimId = AnimId::MAX;
251
252/// Internal state for an active animation.
253#[derive(Debug, Clone)]
254pub struct AnimInstance {
255    /// Animation ID
256    id: AnimId,
257    /// Current playback state.
258    state: AnimState,
259    /// The animation definition.
260    anim: Anim,
261    /// Current time elapsed in the animation.
262    elapsed: Duration,
263    /// Current repeat count.
264    current_repeat: u16,
265    /// Whether currently playing in reverse.
266    is_reversed: bool,
267    /// Whether start delay has passed.
268    delay_passed: bool,
269}
270
271impl AnimInstance {
272    const fn new() -> Self {
273        Self {
274            id: INVALID_ANIM_ID,
275            state: AnimState::Playing,
276            anim: Anim::new(0, 0, Duration::ZERO),
277            elapsed: Duration::ZERO,
278            current_repeat: 0,
279            is_reversed: false,
280            delay_passed: false,
281        }
282    }
283}
284
285#[derive(Debug)]
286pub struct AnimStatus(Cell<Option<i32>>);
287impl AnimStatus {
288    pub fn new() -> Self {
289        Self(Cell::new(None))
290    }
291    pub fn set(&self, value: i32) {
292        self.0.set(Some(value));
293    }
294    pub fn take(&self) -> Option<i32> {
295        self.0.take()
296    }
297    pub fn get(&self) -> Option<i32> {
298        self.0.get()
299    }
300}
301
302pub struct Animations<const N: usize> {
303    animations: [AnimInstance; N],
304    anim_status: [AnimStatus; N],
305}
306
307impl<const N: usize> Animations<N> {
308    /// Creates a new animation manager.
309    pub fn new() -> Self {
310        let animations = core::array::from_fn(|_| AnimInstance::new());
311        let anim_status = core::array::from_fn(|_| AnimStatus::new());
312        Self {
313            animations,
314            anim_status,
315        }
316    }
317
318    pub fn split(self) -> ([AnimInstance; N], [AnimStatus; N]) {
319        (self.animations, self.anim_status)
320    }
321}
322
323/// Animation manager that handles multiple animations.
324///
325/// This struct manages the lifecycle and playback of multiple animations.
326/// It is designed to be memory-efficient for embedded systems.
327///
328/// # Type Parameters
329///
330/// * `C` - The callback type that implements `AnimCallback`
331/// * `N` - The maximum number of simultaneous animations
332pub struct AnimManager<'a> {
333    /// Active animation instances.
334    animations: &'a mut [AnimInstance],
335    anim_status: &'a [AnimStatus],
336    /// Next animation ID.
337    next_id: AnimId,
338}
339
340impl<'a> AnimManager<'a> {
341    /// Creates a new animation manager.
342    pub const fn new(animations: &'a mut [AnimInstance], anim_status: &'a [AnimStatus]) -> Self {
343        Self {
344            animations,
345            anim_status,
346            next_id: 0, //index from 0 to animations.len() - 1
347        }
348    }
349
350    /// Adds an animation to the manager.
351    ///
352    /// # Arguments
353    ///
354    /// * `anim` - The animation to add
355    ///
356    /// # Returns
357    ///
358    /// The animation ID, or `None` if the manager is full.
359    pub fn add(&mut self, anim: Anim) -> Option<AnimId> {
360        if self.next_id as usize >= self.animations.len() {
361            return None;
362        }
363        let id = self.next_id;
364        self.next_id = self.next_id.wrapping_add(1);
365
366        let start_value = anim.start_value;
367        let anim_instance = AnimInstance {
368            id,
369            state: AnimState::Stopped,
370            anim,
371            elapsed: Duration::ZERO,
372            current_repeat: 0,
373            is_reversed: false,
374            delay_passed: false,
375        };
376
377        if let Some(instance) = self.animations.get_mut(id as usize) {
378            if let Some(status) = self.anim_status.get(id as usize) {
379                status.set(start_value);
380                *instance = anim_instance;
381                return Some(id);
382            }
383        };
384
385        None
386    }
387
388    /// Removes an animation from the manager.
389    ///
390    /// # Arguments
391    ///
392    /// * `id` - The animation ID to remove
393    ///
394    /// # Returns
395    ///
396    /// `true` if the animation was found and removed.
397    pub fn remove(&mut self, id: AnimId) -> bool {
398        if let Some(instance) = self.animations.get_mut(id as usize) {
399            instance.id = INVALID_ANIM_ID;
400            return true;
401        }
402
403        false
404    }
405
406    /// Starts playing an animation.
407    ///
408    /// # Arguments
409    ///
410    /// * `id` - The animation ID to play
411    ///
412    /// # Returns
413    ///
414    /// `true` if the animation was found and started.
415    pub fn play(&mut self, id: AnimId) -> bool {
416        if let Some(instance) = self.animations.get_mut(id as usize) {
417            if instance.id == id {
418                instance.state = AnimState::Playing;
419                instance.elapsed = Duration::ZERO;
420                instance.current_repeat = 0;
421                instance.is_reversed = instance.anim.options.play_backward;
422                instance.delay_passed = instance.anim.options.start_delay.is_zero();
423                return true;
424            }
425        }
426        false
427    }
428
429    /// Pauses an animation.
430    ///
431    /// # Arguments
432    ///
433    /// * `id` - The animation ID to pause
434    ///
435    /// # Returns
436    ///
437    /// `true` if the animation was found and paused.
438    pub fn pause(&mut self, id: AnimId) -> bool {
439        if let Some(instance) = self.animations.get_mut(id as usize) {
440            if instance.id == id && instance.state == AnimState::Playing {
441                instance.state = AnimState::Paused;
442                return true;
443            }
444        }
445        false
446    }
447
448    /// Resumes a paused animation.
449    ///
450    /// # Arguments
451    ///
452    /// * `id` - The animation ID to resume
453    ///
454    /// # Returns
455    ///
456    /// `true` if the animation was found and resumed.
457    pub fn resume(&mut self, id: AnimId) -> bool {
458        if let Some(instance) = self.animations.get_mut(id as usize) {
459            if instance.id == id && instance.state == AnimState::Paused {
460                instance.state = AnimState::Playing;
461                return true;
462            }
463        }
464        false
465    }
466
467    /// Stops an animation.
468    ///
469    /// # Arguments
470    ///
471    /// * `id` - The animation ID to stop
472    ///
473    /// # Returns
474    ///
475    /// `true` if the animation was found and stopped.
476    pub fn stop(&mut self, id: AnimId) -> bool {
477        if let Some(instance) = self.animations.get_mut(id as usize) {
478            if instance.id == id {
479                instance.state = AnimState::Stopped;
480                instance.elapsed = Duration::ZERO;
481                instance.current_repeat = 0;
482                return true;
483            }
484        }
485        false
486    }
487
488    /// Gets the state of an animation.
489    ///
490    /// # Arguments
491    ///
492    /// * `id` - The animation ID
493    ///
494    /// # Returns
495    ///
496    /// The animation state, or `None` if not found.
497    pub fn get_state(&self, id: AnimId) -> Option<AnimState> {
498        if let Some(instance) = self.animations.get(id as usize) {
499            if instance.id == id {
500                return Some(instance.state);
501            }
502        }
503        None
504    }
505
506    /// Updates all active animations.
507    ///
508    /// This method should be called regularly (e.g., in the main loop)
509    /// with the elapsed time since the last update.
510    ///
511    /// # Arguments
512    ///
513    /// * `elapsed` - Time elapsed since the last update
514    pub fn tick(&mut self, elapsed: Duration) {
515        for (idx, instance) in self.animations.iter_mut().enumerate() {
516            if idx >= self.next_id as usize {
517                break;
518            }
519            if instance.id == INVALID_ANIM_ID || instance.state != AnimState::Playing {
520                continue;
521            }
522            let Some(status) = self.anim_status.get(instance.id as usize) else {
523                continue;
524            };
525
526            // Handle start delay
527            if !instance.delay_passed {
528                instance.elapsed += elapsed;
529                if instance.elapsed >= instance.anim.options.start_delay {
530                    instance.delay_passed = true;
531                    instance.elapsed = Duration::ZERO;
532                } else {
533                    continue;
534                }
535            } else {
536                instance.elapsed += elapsed;
537            }
538
539            let duration = instance.anim.duration;
540            let duration_ms = duration.as_millis() as u64;
541            let elapsed_ms = instance.elapsed.as_millis() as u64;
542
543            // Calculate progress
544            let progress = if duration_ms == 0 {
545                ANIM_SCALE
546            } else {
547                let effective_elapsed = if instance.is_reversed {
548                    duration_ms.saturating_sub(elapsed_ms)
549                } else {
550                    elapsed_ms.min(duration_ms)
551                };
552                ((effective_elapsed * ANIM_SCALE as u64) / duration_ms) as i32
553            };
554
555            // Calculate and apply value
556            let value = instance.anim.calc_value(progress);
557
558            // Check if animation completed
559            if instance.elapsed < duration {
560                status.set(value);
561            } else {
562                // Ensure final value is set
563                let final_value = if instance.is_reversed {
564                    instance.anim.start_value
565                } else {
566                    instance.anim.end_value
567                };
568                status.set(final_value);
569
570                // Handle repeat
571                let repeat_count = instance.anim.options.repeat_count;
572                let should_repeat = repeat_count == 0 || instance.current_repeat < repeat_count - 1;
573
574                if should_repeat {
575                    instance.current_repeat += 1;
576                    instance.elapsed = Duration::ZERO;
577
578                    // Handle reverse
579                    if instance.anim.options.reverse {
580                        instance.is_reversed = !instance.is_reversed;
581                    }
582                } else {
583                    instance.state = AnimState::Stopped;
584                }
585            }
586        }
587    }
588
589    /// Returns the number of active animations.
590    pub fn count(&self) -> usize {
591        self.animations
592            .iter()
593            .filter(|s| s.id != INVALID_ANIM_ID)
594            .count()
595    }
596
597    /// Returns whether there are any active animations.
598    pub fn is_empty(&self) -> bool {
599        self.animations.iter().all(|s| s.id == INVALID_ANIM_ID)
600    }
601}