Skip to main content

embedded_3dgfx/
tween.rs

1//! Time-based interpolation and easing for boot sequences and UI motion.
2//!
3//! All functions are `no_std` and heap-free. Pair with [`crate::transform_anim::AnimationPlayer`]
4//! for keyframed motion, or drive [`K3dMesh`](crate::mesh::K3dMesh) transforms directly each frame.
5
6use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
7
8/// Easing curve applied to normalized time `t` in \[0, 1\].
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum Easing {
11    #[default]
12    Linear,
13    EaseInCubic,
14    EaseOutCubic,
15    EaseInOutCubic,
16    Smoothstep,
17}
18
19/// Map raw progress `t` (clamped to \[0, 1\]) through an easing curve.
20#[inline]
21pub fn apply_easing(t: f32, easing: Easing) -> f32 {
22    let t = t.clamp(0.0, 1.0);
23    match easing {
24        Easing::Linear => t,
25        Easing::EaseInCubic => t * t * t,
26        Easing::EaseOutCubic => {
27            let u = 1.0 - t;
28            1.0 - u * u * u
29        }
30        Easing::EaseInOutCubic => {
31            if t < 0.5 {
32                4.0 * t * t * t
33            } else {
34                let u = -2.0 * t + 2.0;
35                1.0 - u * u * u * 0.5
36            }
37        }
38        Easing::Smoothstep => t * t * (3.0 - 2.0 * t),
39    }
40}
41
42/// Linear interpolation between two scalars.
43#[inline(always)]
44pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
45    a + (b - a) * t
46}
47
48/// Linear interpolation between two 3D points.
49#[inline(always)]
50pub fn lerp3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
51    [
52        lerp(a[0], b[0], t),
53        lerp(a[1], b[1], t),
54        lerp(a[2], b[2], t),
55    ]
56}
57
58/// Scalar tween from `from` to `to` over `duration` seconds.
59#[derive(Debug, Clone, Copy)]
60pub struct Tween {
61    pub from: f32,
62    pub to: f32,
63    pub duration: f32,
64    pub elapsed: f32,
65    pub easing: Easing,
66}
67
68impl Tween {
69    /// Create a tween. `duration` must be positive for motion; zero duration snaps to `to` immediately.
70    pub const fn new(from: f32, to: f32, duration: f32, easing: Easing) -> Self {
71        Self {
72            from,
73            to,
74            duration,
75            elapsed: 0.0,
76            easing,
77        }
78    }
79
80    /// Reset elapsed time to zero (replay animation).
81    pub fn reset(&mut self) {
82        self.elapsed = 0.0;
83    }
84
85    /// Advance the clock by `dt` seconds.
86    pub fn advance(&mut self, dt: f32) {
87        if dt > 0.0 {
88            self.elapsed += dt;
89        }
90    }
91
92    /// Normalized progress in \[0, 1\] before easing.
93    #[inline]
94    pub fn progress(&self) -> f32 {
95        if self.duration <= 0.0 {
96            return 1.0;
97        }
98        (self.elapsed / self.duration).min(1.0)
99    }
100
101    /// Current interpolated value.
102    #[inline]
103    pub fn value(&self) -> f32 {
104        let t = apply_easing(self.progress(), self.easing);
105        lerp(self.from, self.to, t)
106    }
107
108    /// `true` when elapsed time has reached `duration`.
109    #[inline]
110    pub fn is_done(&self) -> bool {
111        self.duration <= 0.0 || self.elapsed >= self.duration
112    }
113}
114
115/// 3D position (or any vec3) tween.
116#[derive(Debug, Clone, Copy)]
117pub struct Tween3 {
118    pub from: [f32; 3],
119    pub to: [f32; 3],
120    pub duration: f32,
121    pub elapsed: f32,
122    pub easing: Easing,
123}
124
125impl Tween3 {
126    pub const fn new(from: [f32; 3], to: [f32; 3], duration: f32, easing: Easing) -> Self {
127        Self {
128            from,
129            to,
130            duration,
131            elapsed: 0.0,
132            easing,
133        }
134    }
135
136    pub fn reset(&mut self) {
137        self.elapsed = 0.0;
138    }
139
140    pub fn advance(&mut self, dt: f32) {
141        if dt > 0.0 {
142            self.elapsed += dt;
143        }
144    }
145
146    #[inline]
147    pub fn progress(&self) -> f32 {
148        if self.duration <= 0.0 {
149            return 1.0;
150        }
151        (self.elapsed / self.duration).min(1.0)
152    }
153
154    #[inline]
155    pub fn value(&self) -> [f32; 3] {
156        let t = apply_easing(self.progress(), self.easing);
157        lerp3(self.from, self.to, t)
158    }
159
160    #[inline]
161    pub fn is_done(&self) -> bool {
162        self.duration <= 0.0 || self.elapsed >= self.duration
163    }
164}
165
166/// Scale an RGB565 color toward black (`factor` = 0 → black, 1 → unchanged).
167///
168/// Useful for full-screen fade in/out on a finished framebuffer.
169#[inline]
170pub fn scale_rgb565(color: Rgb565, factor: f32) -> Rgb565 {
171    let f = factor.clamp(0.0, 1.0);
172    Rgb565::new(
173        (color.r() as f32 * f) as u8,
174        (color.g() as f32 * f) as u8,
175        (color.b() as f32 * f) as u8,
176    )
177}
178
179#[cfg(test)]
180mod tests {
181    extern crate std;
182
183    use super::*;
184    use embedded_graphics_core::pixelcolor::WebColors;
185
186    #[test]
187    fn test_ease_out_cubic_endpoints() {
188        assert!((apply_easing(0.0, Easing::EaseOutCubic) - 0.0).abs() < 1e-5);
189        assert!((apply_easing(1.0, Easing::EaseOutCubic) - 1.0).abs() < 1e-5);
190    }
191
192    #[test]
193    fn test_tween_completes() {
194        let mut tw = Tween::new(0.0, 10.0, 1.0, Easing::Linear);
195        assert!(!tw.is_done());
196        tw.advance(0.5);
197        assert!((tw.value() - 5.0).abs() < 1e-5);
198        tw.advance(0.5);
199        assert!(tw.is_done());
200        assert!((tw.value() - 10.0).abs() < 1e-5);
201    }
202
203    #[test]
204    fn test_scale_rgb565() {
205        let c = Rgb565::new(30, 60, 30);
206        assert_eq!(scale_rgb565(c, 0.0), Rgb565::BLACK);
207        assert_eq!(scale_rgb565(c, 1.0), c);
208    }
209
210    #[test]
211    fn test_tween3_lerp() {
212        let mut tw = Tween3::new([0.0, 0.0, 0.0], [2.0, 4.0, 6.0], 2.0, Easing::Linear);
213        tw.advance(1.0);
214        let v = tw.value();
215        assert!((v[0] - 1.0).abs() < 1e-5);
216        assert!((v[1] - 2.0).abs() < 1e-5);
217        assert!((v[2] - 3.0).abs() < 1e-5);
218    }
219}