Skip to main content

goud_engine/core/math/
tween.rs

1//! Tween utilities for interpolating values with easing.
2//!
3//! The [`Tweenable`] trait provides a generic interface for types that can be
4//! interpolated. Built-in implementations cover `f32`, [`Vec2`], [`Vec3`],
5//! and [`Color`].
6
7use super::color::Color;
8use super::easing::EasingFn;
9use super::vec2::Vec2;
10use super::vec3::Vec3;
11
12/// A type that can be interpolated between two values.
13///
14/// The default `tween` method applies an easing function to the parameter `t`
15/// before delegating to `lerp`.
16pub trait Tweenable: Sized {
17    /// Linearly interpolates between `self` and `other`.
18    ///
19    /// When `t = 0.0`, returns `self`. When `t = 1.0`, returns `other`.
20    fn lerp(self, other: Self, t: f32) -> Self;
21
22    /// Interpolates between `self` and `other` using an easing function.
23    ///
24    /// The easing function is applied to `t` before interpolation,
25    /// producing non-linear motion curves.
26    #[inline]
27    fn tween(self, other: Self, t: f32, easing: EasingFn) -> Self {
28        self.lerp(other, easing(t))
29    }
30}
31
32impl Tweenable for f32 {
33    #[inline]
34    fn lerp(self, other: Self, t: f32) -> Self {
35        self + (other - self) * t
36    }
37}
38
39impl Tweenable for Vec2 {
40    #[inline]
41    fn lerp(self, other: Self, t: f32) -> Self {
42        self.lerp(other, t)
43    }
44}
45
46impl Tweenable for Vec3 {
47    #[inline]
48    fn lerp(self, other: Self, t: f32) -> Self {
49        self.lerp(other, t)
50    }
51}
52
53impl Tweenable for Color {
54    #[inline]
55    fn lerp(self, other: Self, t: f32) -> Self {
56        self.lerp(other, t)
57    }
58}
59
60/// Convenience function to tween a single `f32` value with an easing function.
61#[inline]
62pub fn tween(from: f32, to: f32, t: f32, easing: EasingFn) -> f32 {
63    from + (to - from) * easing(t)
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use crate::core::math::easing::{ease_in, linear};
70
71    const EPSILON: f32 = 1e-5;
72
73    // =====================================================================
74    // f32 Tweenable
75    // =====================================================================
76
77    #[test]
78    fn test_f32_lerp_boundaries() {
79        let result_0 = Tweenable::lerp(0.0_f32, 10.0, 0.0);
80        let result_1 = Tweenable::lerp(0.0_f32, 10.0, 1.0);
81        assert!((result_0).abs() < EPSILON);
82        assert!((result_1 - 10.0).abs() < EPSILON);
83    }
84
85    #[test]
86    fn test_f32_lerp_midpoint() {
87        let result = Tweenable::lerp(0.0_f32, 10.0, 0.5);
88        assert!((result - 5.0).abs() < EPSILON);
89    }
90
91    #[test]
92    fn test_f32_tween_linear() {
93        let result = 0.0_f32.tween(10.0, 0.5, linear);
94        assert!((result - 5.0).abs() < EPSILON);
95    }
96
97    #[test]
98    fn test_f32_tween_ease_in() {
99        // ease_in(0.5) = 0.25, so result = 0 + 10 * 0.25 = 2.5
100        let result = 0.0_f32.tween(10.0, 0.5, ease_in);
101        assert!((result - 2.5).abs() < EPSILON);
102    }
103
104    // =====================================================================
105    // Vec2 Tweenable
106    // =====================================================================
107
108    #[test]
109    fn test_vec2_lerp_boundaries() {
110        let a = Vec2::new(0.0, 0.0);
111        let b = Vec2::new(10.0, 20.0);
112        let r0 = Tweenable::lerp(a, b, 0.0);
113        let r1 = Tweenable::lerp(a, b, 1.0);
114        assert_eq!(r0, a);
115        assert_eq!(r1, b);
116    }
117
118    #[test]
119    fn test_vec2_tween_linear() {
120        let a = Vec2::new(0.0, 0.0);
121        let b = Vec2::new(10.0, 20.0);
122        let result = a.tween(b, 0.5, linear);
123        assert!((result.x - 5.0).abs() < EPSILON);
124        assert!((result.y - 10.0).abs() < EPSILON);
125    }
126
127    #[test]
128    fn test_vec2_tween_ease_in() {
129        let a = Vec2::new(0.0, 0.0);
130        let b = Vec2::new(10.0, 20.0);
131        let result = a.tween(b, 0.5, ease_in);
132        // ease_in(0.5) = 0.25
133        assert!((result.x - 2.5).abs() < EPSILON);
134        assert!((result.y - 5.0).abs() < EPSILON);
135    }
136
137    // =====================================================================
138    // Vec3 Tweenable
139    // =====================================================================
140
141    #[test]
142    fn test_vec3_lerp_boundaries() {
143        let a = Vec3::new(0.0, 0.0, 0.0);
144        let b = Vec3::new(10.0, 20.0, 30.0);
145        let r0 = Tweenable::lerp(a, b, 0.0);
146        let r1 = Tweenable::lerp(a, b, 1.0);
147        assert_eq!(r0, a);
148        assert_eq!(r1, b);
149    }
150
151    #[test]
152    fn test_vec3_tween_linear() {
153        let a = Vec3::new(0.0, 0.0, 0.0);
154        let b = Vec3::new(10.0, 20.0, 30.0);
155        let result = a.tween(b, 0.5, linear);
156        assert!((result.x - 5.0).abs() < EPSILON);
157        assert!((result.y - 10.0).abs() < EPSILON);
158        assert!((result.z - 15.0).abs() < EPSILON);
159    }
160
161    #[test]
162    fn test_vec3_tween_ease_in() {
163        let a = Vec3::new(0.0, 0.0, 0.0);
164        let b = Vec3::new(10.0, 20.0, 30.0);
165        let result = a.tween(b, 0.5, ease_in);
166        assert!((result.x - 2.5).abs() < EPSILON);
167        assert!((result.y - 5.0).abs() < EPSILON);
168        assert!((result.z - 7.5).abs() < EPSILON);
169    }
170
171    // =====================================================================
172    // Color Tweenable
173    // =====================================================================
174
175    #[test]
176    fn test_color_lerp_boundaries() {
177        let a = Color::BLACK;
178        let b = Color::WHITE;
179        let r0 = Tweenable::lerp(a, b, 0.0);
180        let r1 = Tweenable::lerp(a, b, 1.0);
181        assert_eq!(r0, a);
182        assert_eq!(r1, b);
183    }
184
185    #[test]
186    fn test_color_tween_linear() {
187        let a = Color::BLACK;
188        let b = Color::WHITE;
189        let result = a.tween(b, 0.5, linear);
190        assert!((result.r - 0.5).abs() < EPSILON);
191        assert!((result.g - 0.5).abs() < EPSILON);
192        assert!((result.b - 0.5).abs() < EPSILON);
193    }
194
195    #[test]
196    fn test_color_tween_ease_in() {
197        let a = Color::BLACK;
198        let b = Color::WHITE;
199        let result = a.tween(b, 0.5, ease_in);
200        // ease_in(0.5) = 0.25
201        assert!((result.r - 0.25).abs() < EPSILON);
202        assert!((result.g - 0.25).abs() < EPSILON);
203        assert!((result.b - 0.25).abs() < EPSILON);
204    }
205
206    // =====================================================================
207    // Standalone tween function
208    // =====================================================================
209
210    #[test]
211    fn test_tween_fn_linear() {
212        let result = tween(0.0, 10.0, 0.5, linear);
213        assert!((result - 5.0).abs() < EPSILON);
214    }
215
216    #[test]
217    fn test_tween_fn_ease_in() {
218        let result = tween(0.0, 10.0, 0.5, ease_in);
219        assert!((result - 2.5).abs() < EPSILON);
220    }
221
222    #[test]
223    fn test_tween_fn_with_offset() {
224        let result = tween(5.0, 15.0, 0.5, linear);
225        assert!((result - 10.0).abs() < EPSILON);
226    }
227}