Skip to main content

rlvgl_core/
animation.rs

1//! Animation primitives: easing, looping, and legacy wall-clock animators.
2//!
3//! # Pure-math utilities (not deprecated)
4//!
5//! These types are the shared mathematical vocabulary reused by both this
6//! module and the tick-driven [`crate::anim`] substrate (ANIM-00). They are
7//! **not deprecated** and will not be removed:
8//!
9//! - [`Easing`] — easing curve enum (re-exported by `crate::anim`).
10//! - [`LoopMode`] — loop/repeat/ping-pong enum (re-exported by `crate::anim`).
11//! - [`loop_progress`] — internal `(t, finished)` helper used by both surfaces.
12//!
13//! All easing functions use polynomial/piecewise math with no `libm` dependency
14//! and are safe for `no_std` targets.
15//!
16//! # Deprecated wall-clock animators
17//!
18//! The following types advance animations via `tick(delta_ms: u32)` and
19//! capture raw `*mut` pointers to their targets. They are **deprecated** as of
20//! LPAR-06 and will be removed in a future release. New code MUST use the
21//! tick-driven [`crate::anim`] types instead (see ANIM-00 concepts doc).
22//!
23//! - [`Fade`] — background color fade via `*mut Style`.
24//! - [`Slide`] — rect slide via `*mut Rect`.
25//! - [`Motion`] — position/size motion via `*mut Rect`.
26//! - [`FadeTransition`] — two-point alpha transition via `*mut u8`.
27//! - [`KeyFade`] — multi-keyframe alpha animation via `*mut u8`.
28//! - [`Timeline`] — multi-animation container for the above types.
29//!
30//! Existing consumers (e.g. `core/tests/animation.rs`) may use
31//! `#[allow(deprecated)]` to continue compiling. Removal is deferred-Coupled
32//! to a SemVer major/minor release plan.
33
34use crate::style::Style;
35use crate::widget::{Color, Rect};
36
37// ---------------------------------------------------------------------------
38// Easing
39// ---------------------------------------------------------------------------
40
41/// Easing curve that maps linear progress `t ∈ [0,1]` to curved progress.
42///
43/// All variants use polynomial or piecewise-polynomial math and require no
44/// trigonometric functions or `libm`.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum Easing {
47    /// No easing — constant speed.
48    #[default]
49    Linear,
50    /// Accelerate from zero velocity (quadratic).
51    EaseIn,
52    /// Decelerate to zero velocity (quadratic).
53    EaseOut,
54    /// Accelerate then decelerate (cubic hermite: `3t² − 2t³`).
55    EaseInOut,
56    /// Accelerate from zero velocity (cubic).
57    EaseInCubic,
58    /// Decelerate to zero velocity (cubic).
59    EaseOutCubic,
60    /// Accelerate then decelerate (two piecewise cubics).
61    EaseInOutCubic,
62    /// Bounce effect at the end.
63    Bounce,
64    /// Quantize into `n` discrete steps.
65    Step(u8),
66}
67
68impl Easing {
69    /// Apply the easing curve to a linear progress value `t` in `[0, 1]`.
70    ///
71    /// Returns the eased value, also in `[0, 1]`.
72    pub fn apply(self, t: f32) -> f32 {
73        let t = t.clamp(0.0, 1.0);
74        match self {
75            Easing::Linear => t,
76            Easing::EaseIn => t * t,
77            Easing::EaseOut => {
78                let inv = 1.0 - t;
79                1.0 - inv * inv
80            }
81            Easing::EaseInOut => t * t * (3.0 - 2.0 * t),
82            Easing::EaseInCubic => t * t * t,
83            Easing::EaseOutCubic => {
84                let inv = 1.0 - t;
85                1.0 - inv * inv * inv
86            }
87            Easing::EaseInOutCubic => {
88                if t < 0.5 {
89                    4.0 * t * t * t
90                } else {
91                    let inv = -2.0 * t + 2.0;
92                    1.0 - inv * inv * inv / 2.0
93                }
94            }
95            Easing::Bounce => {
96                let t = 1.0 - t;
97                let out = if t < 1.0 / 2.75 {
98                    7.5625 * t * t
99                } else if t < 2.0 / 2.75 {
100                    let t = t - 1.5 / 2.75;
101                    7.5625 * t * t + 0.75
102                } else if t < 2.5 / 2.75 {
103                    let t = t - 2.25 / 2.75;
104                    7.5625 * t * t + 0.9375
105                } else {
106                    let t = t - 2.625 / 2.75;
107                    7.5625 * t * t + 0.984375
108                };
109                1.0 - out
110            }
111            Easing::Step(n) => {
112                if n == 0 {
113                    t
114                } else {
115                    let steps = n as f32;
116                    ((t * steps) as u32 as f32) / steps
117                }
118            }
119        }
120    }
121}
122
123// ---------------------------------------------------------------------------
124// LoopMode
125// ---------------------------------------------------------------------------
126
127/// Controls how an animation repeats after completing one cycle.
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
129pub enum LoopMode {
130    /// Play once and stop (default).
131    #[default]
132    Once,
133    /// Repeat from the start. `0` means infinite.
134    Repeat(u16),
135    /// Play forward then backward. `0` means infinite round-trips.
136    PingPong(u16),
137}
138
139/// Compute the effective progress `t ∈ [0,1]` and whether the animation is
140/// finished, given elapsed time, duration, and loop mode.
141///
142/// Unit-agnostic: callers in this module pass milliseconds; the tick-driven
143/// [`anim`](crate::anim) module passes ticks. The folding math is identical.
144pub(crate) fn loop_progress(elapsed: u32, duration_ms: u32, loop_mode: LoopMode) -> (f32, bool) {
145    if duration_ms == 0 {
146        return (1.0, true);
147    }
148    match loop_mode {
149        LoopMode::Once => {
150            let e = if elapsed > duration_ms {
151                duration_ms
152            } else {
153                elapsed
154            };
155            (e as f32 / duration_ms as f32, elapsed >= duration_ms)
156        }
157        LoopMode::Repeat(n) => {
158            let cycle = elapsed / duration_ms;
159            let finished = n > 0 && cycle >= n as u32;
160            if finished {
161                (1.0, true)
162            } else {
163                let within = elapsed % duration_ms;
164                (within as f32 / duration_ms as f32, false)
165            }
166        }
167        LoopMode::PingPong(n) => {
168            // One round-trip = 2 × duration
169            let half_cycle = elapsed / duration_ms;
170            let finished = n > 0 && half_cycle >= (n as u32) * 2;
171            if finished {
172                (0.0, true)
173            } else {
174                let within = elapsed % duration_ms;
175                let t = within as f32 / duration_ms as f32;
176                if half_cycle.is_multiple_of(2) {
177                    (t, false)
178                } else {
179                    (1.0 - t, false)
180                }
181            }
182        }
183    }
184}
185
186// ---------------------------------------------------------------------------
187// Fade (existing — color transition, now with easing + loop)
188// ---------------------------------------------------------------------------
189
190/// Linear (or eased) fade animation for a style's background color.
191///
192/// The animation owns a mutable pointer to the [`Style`] being modified. This
193/// keeps the API lightweight for `no_std` targets at the cost of requiring
194/// unsafe access internally.
195#[deprecated(note = "ms/wall-clock animation is superseded by tick-driven core::anim; see LPAR-06")]
196pub struct Fade {
197    style: *mut Style,
198    start: Color,
199    end: Color,
200    duration_ms: u32,
201    elapsed: u32,
202    easing: Easing,
203    loop_mode: LoopMode,
204}
205
206#[allow(deprecated)]
207impl Fade {
208    /// Create a new fade animation.
209    pub fn new(style: &mut Style, start: Color, end: Color, duration_ms: u32) -> Self {
210        Self {
211            style: style as *mut Style,
212            start,
213            end,
214            duration_ms,
215            elapsed: 0,
216            easing: Easing::Linear,
217            loop_mode: LoopMode::Once,
218        }
219    }
220
221    /// Set the easing curve (builder pattern).
222    pub fn with_easing(mut self, easing: Easing) -> Self {
223        self.easing = easing;
224        self
225    }
226
227    /// Set the loop mode (builder pattern).
228    pub fn with_loop(mut self, loop_mode: LoopMode) -> Self {
229        self.loop_mode = loop_mode;
230        self
231    }
232
233    /// Advance the animation by `delta_ms` milliseconds.
234    pub fn tick(&mut self, delta_ms: u32) {
235        self.elapsed = self.elapsed.saturating_add(delta_ms);
236        let (raw_t, _) = loop_progress(self.elapsed, self.duration_ms, self.loop_mode);
237        let t = self.easing.apply(raw_t);
238        let lerp = |a: u8, b: u8| a as f32 + (b as f32 - a as f32) * t;
239        unsafe {
240            (*self.style).bg_color = Color(
241                lerp(self.start.0, self.end.0) as u8,
242                lerp(self.start.1, self.end.1) as u8,
243                lerp(self.start.2, self.end.2) as u8,
244                lerp(self.start.3, self.end.3) as u8,
245            );
246        }
247    }
248
249    /// Returns `true` when the animation has reached its end point.
250    pub fn finished(&self) -> bool {
251        let (_, done) = loop_progress(self.elapsed, self.duration_ms, self.loop_mode);
252        done
253    }
254}
255
256// ---------------------------------------------------------------------------
257// Slide (existing — linear rect transition, now with easing + loop)
258// ---------------------------------------------------------------------------
259
260/// Linear (or eased) slide animation for a [`Rect`].
261#[deprecated(note = "ms/wall-clock animation is superseded by tick-driven core::anim; see LPAR-06")]
262pub struct Slide {
263    rect: *mut Rect,
264    start: Rect,
265    end: Rect,
266    duration_ms: u32,
267    elapsed: u32,
268    easing: Easing,
269    loop_mode: LoopMode,
270}
271
272#[allow(deprecated)]
273impl Slide {
274    /// Create a new slide animation.
275    pub fn new(rect: &mut Rect, start: Rect, end: Rect, duration_ms: u32) -> Self {
276        Self {
277            rect: rect as *mut Rect,
278            start,
279            end,
280            duration_ms,
281            elapsed: 0,
282            easing: Easing::Linear,
283            loop_mode: LoopMode::Once,
284        }
285    }
286
287    /// Set the easing curve (builder pattern).
288    pub fn with_easing(mut self, easing: Easing) -> Self {
289        self.easing = easing;
290        self
291    }
292
293    /// Set the loop mode (builder pattern).
294    pub fn with_loop(mut self, loop_mode: LoopMode) -> Self {
295        self.loop_mode = loop_mode;
296        self
297    }
298
299    /// Advance the animation by `delta_ms` milliseconds.
300    pub fn tick(&mut self, delta_ms: u32) {
301        self.elapsed = self.elapsed.saturating_add(delta_ms);
302        let (raw_t, _) = loop_progress(self.elapsed, self.duration_ms, self.loop_mode);
303        let t = self.easing.apply(raw_t);
304        let lerp = |a: i32, b: i32| a as f32 + (b as f32 - a as f32) * t;
305        unsafe {
306            *self.rect = Rect {
307                x: lerp(self.start.x, self.end.x) as i32,
308                y: lerp(self.start.y, self.end.y) as i32,
309                width: lerp(self.start.width, self.end.width) as i32,
310                height: lerp(self.start.height, self.end.height) as i32,
311            };
312        }
313    }
314
315    /// Returns `true` once the slide has finished.
316    pub fn finished(&self) -> bool {
317        let (_, done) = loop_progress(self.elapsed, self.duration_ms, self.loop_mode);
318        done
319    }
320}
321
322// ---------------------------------------------------------------------------
323// Motion (new — position/size with easing + looping)
324// ---------------------------------------------------------------------------
325
326/// Animated position and size transition with easing and looping.
327///
328/// This is the primary way to move widgets across the screen, including
329/// off-screen positions and oversized dimensions.
330#[deprecated(note = "ms/wall-clock animation is superseded by tick-driven core::anim; see LPAR-06")]
331pub struct Motion {
332    rect: *mut Rect,
333    start: Rect,
334    end: Rect,
335    duration_ms: u32,
336    elapsed: u32,
337    easing: Easing,
338    loop_mode: LoopMode,
339}
340
341#[allow(deprecated)]
342impl Motion {
343    /// Create a new motion animation.
344    pub fn new(rect: &mut Rect, start: Rect, end: Rect, duration_ms: u32) -> Self {
345        Self {
346            rect: rect as *mut Rect,
347            start,
348            end,
349            duration_ms,
350            elapsed: 0,
351            easing: Easing::Linear,
352            loop_mode: LoopMode::Once,
353        }
354    }
355
356    /// Set the easing curve (builder pattern).
357    pub fn with_easing(mut self, easing: Easing) -> Self {
358        self.easing = easing;
359        self
360    }
361
362    /// Set the loop mode (builder pattern).
363    pub fn with_loop(mut self, loop_mode: LoopMode) -> Self {
364        self.loop_mode = loop_mode;
365        self
366    }
367
368    /// Advance the animation by `delta_ms` milliseconds.
369    pub fn tick(&mut self, delta_ms: u32) {
370        self.elapsed = self.elapsed.saturating_add(delta_ms);
371        let (raw_t, _) = loop_progress(self.elapsed, self.duration_ms, self.loop_mode);
372        let t = self.easing.apply(raw_t);
373        let lerp = |a: i32, b: i32| a as f32 + (b as f32 - a as f32) * t;
374        unsafe {
375            *self.rect = Rect {
376                x: lerp(self.start.x, self.end.x) as i32,
377                y: lerp(self.start.y, self.end.y) as i32,
378                width: lerp(self.start.width, self.end.width) as i32,
379                height: lerp(self.start.height, self.end.height) as i32,
380            };
381        }
382    }
383
384    /// Returns `true` once the motion has finished.
385    pub fn finished(&self) -> bool {
386        let (_, done) = loop_progress(self.elapsed, self.duration_ms, self.loop_mode);
387        done
388    }
389}
390
391// ---------------------------------------------------------------------------
392// FadeTransition (new — simple two-point alpha animation)
393// ---------------------------------------------------------------------------
394
395/// Two-point alpha animation targeting a `u8` value (typically [`Style::alpha`]).
396///
397/// Interpolates alpha from `start` to `end` over `duration_ms` with optional
398/// easing and looping.
399#[deprecated(note = "ms/wall-clock animation is superseded by tick-driven core::anim; see LPAR-06")]
400pub struct FadeTransition {
401    alpha: *mut u8,
402    start: u8,
403    end: u8,
404    duration_ms: u32,
405    elapsed: u32,
406    easing: Easing,
407    loop_mode: LoopMode,
408}
409
410#[allow(deprecated)]
411impl FadeTransition {
412    /// Create a new fade transition targeting the given alpha value.
413    pub fn new(alpha: &mut u8, start: u8, end: u8, duration_ms: u32) -> Self {
414        Self {
415            alpha: alpha as *mut u8,
416            start,
417            end,
418            duration_ms,
419            elapsed: 0,
420            easing: Easing::Linear,
421            loop_mode: LoopMode::Once,
422        }
423    }
424
425    /// Set the easing curve (builder pattern).
426    pub fn with_easing(mut self, easing: Easing) -> Self {
427        self.easing = easing;
428        self
429    }
430
431    /// Set the loop mode (builder pattern).
432    pub fn with_loop(mut self, loop_mode: LoopMode) -> Self {
433        self.loop_mode = loop_mode;
434        self
435    }
436
437    /// Advance the animation by `delta_ms` milliseconds.
438    pub fn tick(&mut self, delta_ms: u32) {
439        self.elapsed = self.elapsed.saturating_add(delta_ms);
440        let (raw_t, _) = loop_progress(self.elapsed, self.duration_ms, self.loop_mode);
441        let t = self.easing.apply(raw_t);
442        let v = self.start as f32 + (self.end as f32 - self.start as f32) * t;
443        unsafe {
444            *self.alpha = v as u8;
445        }
446    }
447
448    /// Returns `true` once the fade transition has finished.
449    pub fn finished(&self) -> bool {
450        let (_, done) = loop_progress(self.elapsed, self.duration_ms, self.loop_mode);
451        done
452    }
453}
454
455// ---------------------------------------------------------------------------
456// KeyFade (new — multi-keyframe alpha animation)
457// ---------------------------------------------------------------------------
458
459/// A single alpha keyframe within a [`KeyFade`] animation.
460#[derive(Debug, Clone, Copy)]
461pub struct AlphaKey {
462    /// Absolute time offset from the animation start, in milliseconds.
463    pub time_ms: u32,
464    /// Alpha value at this keyframe (`0` = transparent, `255` = opaque).
465    pub alpha: u8,
466    /// Easing curve applied from this keyframe to the next one.
467    pub easing: Easing,
468}
469
470/// Multi-keyframe alpha animation with per-segment easing and looping.
471///
472/// A `KeyFade` with two keyframes is equivalent to a [`FadeTransition`].
473/// With more keyframes you can create complex fade sequences such as
474/// pulse, flash, or staged reveal effects.
475#[deprecated(note = "ms/wall-clock animation is superseded by tick-driven core::anim; see LPAR-06")]
476pub struct KeyFade {
477    alpha: *mut u8,
478    keys: alloc::vec::Vec<AlphaKey>,
479    total_ms: u32,
480    elapsed: u32,
481    loop_mode: LoopMode,
482}
483
484#[allow(deprecated)]
485impl KeyFade {
486    /// Start building a keyframe alpha animation targeting the given value.
487    ///
488    /// Add keyframes with [`key`](Self::key) and then optionally set the loop
489    /// mode with [`with_loop`](Self::with_loop).
490    pub fn new(alpha: &mut u8) -> Self {
491        Self {
492            alpha: alpha as *mut u8,
493            keys: alloc::vec::Vec::new(),
494            total_ms: 0,
495            elapsed: 0,
496            loop_mode: LoopMode::Once,
497        }
498    }
499
500    /// Add a keyframe (builder pattern, chainable).
501    ///
502    /// `time_ms` is the absolute time from the animation start. `easing` is
503    /// the curve applied from this keyframe to the next one (ignored on the
504    /// last keyframe).
505    ///
506    /// Keyframes must be added in ascending `time_ms` order.
507    pub fn key(mut self, time_ms: u32, alpha: u8, easing: Easing) -> Self {
508        self.keys.push(AlphaKey {
509            time_ms,
510            alpha,
511            easing,
512        });
513        if time_ms > self.total_ms {
514            self.total_ms = time_ms;
515        }
516        self
517    }
518
519    /// Set the loop mode (builder pattern).
520    pub fn with_loop(mut self, loop_mode: LoopMode) -> Self {
521        self.loop_mode = loop_mode;
522        self
523    }
524
525    /// Advance the animation by `delta_ms` milliseconds.
526    pub fn tick(&mut self, delta_ms: u32) {
527        if self.keys.len() < 2 {
528            return;
529        }
530        self.elapsed = self.elapsed.saturating_add(delta_ms);
531        let (raw_t, _) = loop_progress(self.elapsed, self.total_ms, self.loop_mode);
532        let time = raw_t * self.total_ms as f32;
533
534        // Find the surrounding keyframes.
535        let mut k0 = &self.keys[0];
536        let mut k1 = &self.keys[1];
537        for i in 1..self.keys.len() {
538            if self.keys[i].time_ms as f32 >= time {
539                k1 = &self.keys[i];
540                k0 = &self.keys[i - 1];
541                break;
542            }
543            // Past the last found pair — use the last two.
544            k0 = &self.keys[i - 1];
545            k1 = &self.keys[i];
546        }
547
548        let seg_dur = k1.time_ms as f32 - k0.time_ms as f32;
549        let local_t = if seg_dur > 0.0 {
550            (time - k0.time_ms as f32) / seg_dur
551        } else {
552            1.0
553        };
554        let eased = k0.easing.apply(local_t);
555        let v = k0.alpha as f32 + (k1.alpha as f32 - k0.alpha as f32) * eased;
556        unsafe {
557            *self.alpha = v as u8;
558        }
559    }
560
561    /// Returns `true` once the keyframe animation has finished.
562    pub fn finished(&self) -> bool {
563        if self.keys.len() < 2 {
564            return true;
565        }
566        let (_, done) = loop_progress(self.elapsed, self.total_ms, self.loop_mode);
567        done
568    }
569}
570
571// ---------------------------------------------------------------------------
572// Timeline
573// ---------------------------------------------------------------------------
574
575/// Animation timeline that updates multiple animations at once.
576#[allow(deprecated)]
577#[deprecated(note = "ms/wall-clock animation is superseded by tick-driven core::anim; see LPAR-06")]
578pub struct Timeline {
579    fades: alloc::vec::Vec<Fade>,
580    slides: alloc::vec::Vec<Slide>,
581    motions: alloc::vec::Vec<Motion>,
582    fade_transitions: alloc::vec::Vec<FadeTransition>,
583    key_fades: alloc::vec::Vec<KeyFade>,
584}
585
586#[allow(deprecated)]
587impl Timeline {
588    /// Create an empty timeline.
589    pub fn new() -> Self {
590        Self {
591            fades: alloc::vec::Vec::new(),
592            slides: alloc::vec::Vec::new(),
593            motions: alloc::vec::Vec::new(),
594            fade_transitions: alloc::vec::Vec::new(),
595            key_fades: alloc::vec::Vec::new(),
596        }
597    }
598
599    /// Add a [`Fade`] animation (color transition) to the timeline.
600    pub fn add_fade(&mut self, fade: Fade) {
601        self.fades.push(fade);
602    }
603
604    /// Add a [`Slide`] animation (linear rect transition) to the timeline.
605    pub fn add_slide(&mut self, slide: Slide) {
606        self.slides.push(slide);
607    }
608
609    /// Add a [`Motion`] animation (eased position/size) to the timeline.
610    pub fn add_motion(&mut self, motion: Motion) {
611        self.motions.push(motion);
612    }
613
614    /// Add a [`FadeTransition`] (two-point alpha) to the timeline.
615    pub fn add_fade_transition(&mut self, ft: FadeTransition) {
616        self.fade_transitions.push(ft);
617    }
618
619    /// Add a [`KeyFade`] (multi-keyframe alpha) to the timeline.
620    pub fn add_key_fade(&mut self, kf: KeyFade) {
621        self.key_fades.push(kf);
622    }
623
624    /// Advance all animations by `delta_ms` milliseconds.
625    pub fn tick(&mut self, delta_ms: u32) {
626        for f in &mut self.fades {
627            f.tick(delta_ms);
628        }
629        for s in &mut self.slides {
630            s.tick(delta_ms);
631        }
632        for m in &mut self.motions {
633            m.tick(delta_ms);
634        }
635        for ft in &mut self.fade_transitions {
636            ft.tick(delta_ms);
637        }
638        for kf in &mut self.key_fades {
639            kf.tick(delta_ms);
640        }
641        self.fades.retain(|f| !f.finished());
642        self.slides.retain(|s| !s.finished());
643        self.motions.retain(|m| !m.finished());
644        self.fade_transitions.retain(|ft| !ft.finished());
645        self.key_fades.retain(|kf| !kf.finished());
646    }
647
648    /// Returns `true` if no animations remain in the timeline.
649    pub fn is_empty(&self) -> bool {
650        self.fades.is_empty()
651            && self.slides.is_empty()
652            && self.motions.is_empty()
653            && self.fade_transitions.is_empty()
654            && self.key_fades.is_empty()
655    }
656}
657
658#[allow(deprecated)]
659impl Default for Timeline {
660    fn default() -> Self {
661        Self::new()
662    }
663}