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
302impl Default for AnimStatus {
303    fn default() -> Self {
304        Self::new()
305    }
306}
307
308pub struct Animations<const N: usize> {
309    animations: [AnimInstance; N],
310    anim_status: [AnimStatus; N],
311}
312
313impl<const N: usize> Animations<N> {
314    pub fn new() -> Self {
315        let animations = core::array::from_fn(|_| AnimInstance::new());
316        let anim_status = core::array::from_fn(|_| AnimStatus::new());
317        Self {
318            animations,
319            anim_status,
320        }
321    }
322
323    pub fn split(self) -> ([AnimInstance; N], [AnimStatus; N]) {
324        (self.animations, self.anim_status)
325    }
326
327    pub fn as_mut(&mut self) -> (&mut [AnimInstance; N], &[AnimStatus; N]) {
328        (&mut self.animations, &self.anim_status)
329    }
330}
331
332impl<const N: usize> Default for Animations<N> {
333    fn default() -> Self {
334        Self::new()
335    }
336}
337
338/// Animation manager that handles multiple animations.
339///
340/// This struct manages the lifecycle and playback of multiple animations.
341/// It is designed to be memory-efficient for embedded systems.
342///
343/// # Type Parameters
344///
345/// * `C` - The callback type that implements `AnimCallback`
346/// * `N` - The maximum number of simultaneous animations
347pub struct AnimManager<'a> {
348    /// Active animation instances.
349    animations: &'a mut [AnimInstance],
350    anim_status: &'a [AnimStatus],
351    /// Next animation ID.
352    next_id: AnimId,
353}
354
355impl<'a> AnimManager<'a> {
356    /// Creates a new animation manager.
357    pub const fn new(animations: &'a mut [AnimInstance], anim_status: &'a [AnimStatus]) -> Self {
358        Self {
359            animations,
360            anim_status,
361            next_id: 0, //index from 0 to animations.len() - 1
362        }
363    }
364
365    /// Adds an animation to the manager.
366    ///
367    /// # Arguments
368    ///
369    /// * `anim` - The animation to add
370    ///
371    /// # Returns
372    ///
373    /// The animation ID, or `None` if the manager is full.
374    pub fn add(&mut self, anim: Anim) -> Option<AnimId> {
375        if self.next_id as usize >= self.animations.len() {
376            return None;
377        }
378        let id = self.next_id;
379        self.next_id = self.next_id.wrapping_add(1);
380
381        let anim_instance = AnimInstance {
382            id,
383            state: AnimState::Stopped,
384            anim,
385            elapsed: Duration::ZERO,
386            current_repeat: 0,
387            is_reversed: false,
388            delay_passed: false,
389        };
390
391        if let Some(instance) = self.animations.get_mut(id as usize) {
392            *instance = anim_instance;
393            return Some(id);
394        };
395
396        None
397    }
398
399    /// Removes an animation from the manager.
400    ///
401    /// # Arguments
402    ///
403    /// * `id` - The animation ID to remove
404    ///
405    /// # Returns
406    ///
407    /// `true` if the animation was found and removed.
408    pub fn remove(&mut self, id: AnimId) -> bool {
409        if let Some(instance) = self.animations.get_mut(id as usize) {
410            instance.id = INVALID_ANIM_ID;
411            return true;
412        }
413
414        false
415    }
416
417    /// Starts playing an animation.
418    ///
419    /// # Arguments
420    ///
421    /// * `id` - The animation ID to play
422    ///
423    /// # Returns
424    ///
425    /// `true` if the animation was found and started.
426    pub fn play(&mut self, id: AnimId) -> bool {
427        if let Some(instance) = self.animations.get_mut(id as usize)
428            && instance.id == id
429        {
430            instance.state = AnimState::Playing;
431            instance.elapsed = Duration::ZERO;
432            instance.current_repeat = 0;
433            instance.is_reversed = instance.anim.options.play_backward;
434            instance.delay_passed = instance.anim.options.start_delay.is_zero();
435
436            if let Some(status) = self.anim_status.get(id as usize) {
437                status.set(instance.anim.start_value);
438            }
439            return true;
440        }
441        false
442    }
443
444    /// Pauses an animation.
445    ///
446    /// # Arguments
447    ///
448    /// * `id` - The animation ID to pause
449    ///
450    /// # Returns
451    ///
452    /// `true` if the animation was found and paused.
453    pub fn pause(&mut self, id: AnimId) -> bool {
454        if let Some(instance) = self.animations.get_mut(id as usize)
455            && instance.id == id
456            && instance.state == AnimState::Playing
457        {
458            instance.state = AnimState::Paused;
459            return true;
460        }
461        false
462    }
463
464    /// Resumes a paused animation.
465    ///
466    /// # Arguments
467    ///
468    /// * `id` - The animation ID to resume
469    ///
470    /// # Returns
471    ///
472    /// `true` if the animation was found and resumed.
473    pub fn resume(&mut self, id: AnimId) -> bool {
474        if let Some(instance) = self.animations.get_mut(id as usize)
475            && instance.id == id
476            && instance.state == AnimState::Paused
477        {
478            instance.state = AnimState::Playing;
479            return true;
480        }
481        false
482    }
483
484    /// Stops an animation.
485    ///
486    /// # Arguments
487    ///
488    /// * `id` - The animation ID to stop
489    ///
490    /// # Returns
491    ///
492    /// `true` if the animation was found and stopped.
493    pub fn stop(&mut self, id: AnimId) -> bool {
494        if let Some(instance) = self.animations.get_mut(id as usize)
495            && instance.id == id
496        {
497            instance.state = AnimState::Stopped;
498            instance.elapsed = Duration::ZERO;
499            instance.current_repeat = 0;
500            return true;
501        }
502        false
503    }
504
505    /// Gets the state of an animation.
506    ///
507    /// # Arguments
508    ///
509    /// * `id` - The animation ID
510    ///
511    /// # Returns
512    ///
513    /// The animation state, or `None` if not found.
514    pub fn get_state(&self, id: AnimId) -> Option<AnimState> {
515        if let Some(instance) = self.animations.get(id as usize)
516            && instance.id == id
517        {
518            return Some(instance.state);
519        }
520        None
521    }
522
523    /// Updates all active animations.
524    ///
525    /// This method should be called regularly (e.g., in the main loop)
526    /// with the elapsed time since the last update.
527    ///
528    /// # Arguments
529    ///
530    /// * `elapsed` - Time elapsed since the last update
531    pub fn tick(&mut self, elapsed: Duration) -> bool {
532        let mut updated = false;
533
534        for (idx, instance) in self.animations.iter_mut().enumerate() {
535            if idx >= self.next_id as usize {
536                break;
537            }
538            if instance.id == INVALID_ANIM_ID || instance.state != AnimState::Playing {
539                continue;
540            }
541            let Some(status) = self.anim_status.get(instance.id as usize) else {
542                continue;
543            };
544
545            // Handle start delay
546            if !instance.delay_passed {
547                instance.elapsed += elapsed;
548                if instance.elapsed >= instance.anim.options.start_delay {
549                    instance.delay_passed = true;
550                    instance.elapsed = Duration::ZERO;
551                } else {
552                    continue;
553                }
554            } else {
555                instance.elapsed += elapsed;
556            }
557
558            let duration = instance.anim.duration;
559            let duration_ms = duration.as_millis() as u64;
560            let elapsed_ms = instance.elapsed.as_millis() as u64;
561
562            // Calculate progress
563            let progress = if duration_ms == 0 {
564                ANIM_SCALE
565            } else {
566                let effective_elapsed = if instance.is_reversed {
567                    duration_ms.saturating_sub(elapsed_ms)
568                } else {
569                    elapsed_ms.min(duration_ms)
570                };
571                ((effective_elapsed * ANIM_SCALE as u64) / duration_ms) as i32
572            };
573
574            // Calculate and apply value
575            let value = instance.anim.calc_value(progress);
576
577            // Check if animation completed
578            if instance.elapsed < duration {
579                status.set(value);
580            } else {
581                // Ensure final value is set
582                let final_value = if instance.is_reversed {
583                    instance.anim.start_value
584                } else {
585                    instance.anim.end_value
586                };
587                status.set(final_value);
588
589                // Handle repeat
590                let repeat_count = instance.anim.options.repeat_count;
591                let should_repeat = repeat_count == 0 || instance.current_repeat < repeat_count - 1;
592
593                if should_repeat {
594                    instance.current_repeat += 1;
595                    instance.elapsed = Duration::ZERO;
596
597                    // Handle reverse
598                    if instance.anim.options.reverse {
599                        instance.is_reversed = !instance.is_reversed;
600                    }
601                } else {
602                    instance.state = AnimState::Stopped;
603                }
604            }
605            updated = true;
606        }
607
608        updated
609    }
610
611    /// Returns the number of active animations.
612    pub fn count(&self) -> usize {
613        self.animations
614            .iter()
615            .filter(|s| s.id != INVALID_ANIM_ID)
616            .count()
617    }
618
619    /// Returns whether there are any active animations.
620    pub fn is_empty(&self) -> bool {
621        self.animations.iter().all(|s| s.id == INVALID_ANIM_ID)
622    }
623}