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}