Skip to main content

armas_basic/animation/
easing.rs

1//! Easing functions for animations
2//!
3//! These functions transform a linear time value (0.0 to 1.0) into an eased value
4//! that creates more natural-looking animations.
5
6/// Easing function type
7///
8/// # Example
9///
10/// ```rust
11/// use armas_basic::animation::EasingFunction;
12///
13/// let t = 0.5; // halfway through animation
14/// let eased = EasingFunction::CubicOut.apply(t);
15/// assert!(eased > 0.5); // CubicOut is faster at start
16/// ```
17#[derive(Debug, Clone, Copy, PartialEq, Default)]
18pub enum EasingFunction {
19    /// No easing, linear interpolation
20    Linear,
21    /// Accelerating from zero velocity
22    EaseIn,
23    /// Decelerating to zero velocity
24    EaseOut,
25    /// Acceleration until halfway, then deceleration
26    #[default]
27    EaseInOut,
28    /// Quadratic ease in
29    QuadIn,
30    /// Quadratic ease out
31    QuadOut,
32    /// Quadratic ease in-out
33    QuadInOut,
34    /// Cubic ease in
35    CubicIn,
36    /// Cubic ease out
37    CubicOut,
38    /// Cubic ease in-out
39    CubicInOut,
40    /// Exponential ease in
41    ExpoIn,
42    /// Exponential ease out
43    ExpoOut,
44    /// Exponential ease in-out
45    ExpoInOut,
46    /// Elastic ease in (spring effect)
47    ElasticIn,
48    /// Elastic ease out (spring effect)
49    ElasticOut,
50    /// Bounce ease out
51    BounceOut,
52    /// Custom cubic bezier curve
53    Cubic {
54        /// First control point X
55        x1: f32,
56        /// First control point Y
57        y1: f32,
58        /// Second control point X
59        x2: f32,
60        /// Second control point Y
61        y2: f32,
62    },
63}
64
65impl EasingFunction {
66    /// Apply the easing function to a time value (0.0 to 1.0)
67    #[must_use]
68    pub fn apply(&self, t: f32) -> f32 {
69        let t = t.clamp(0.0, 1.0);
70
71        match self {
72            Self::Linear => t,
73            Self::EaseIn | Self::QuadIn => quad_in(t),
74            Self::EaseOut | Self::QuadOut => quad_out(t),
75            Self::EaseInOut | Self::QuadInOut => quad_in_out(t),
76            Self::CubicIn => cubic_in(t),
77            Self::CubicOut => cubic_out(t),
78            Self::CubicInOut => cubic_in_out(t),
79            Self::ExpoIn => expo_in(t),
80            Self::ExpoOut => expo_out(t),
81            Self::ExpoInOut => expo_in_out(t),
82            Self::ElasticIn => elastic_in(t),
83            Self::ElasticOut => elastic_out(t),
84            Self::BounceOut => bounce_out(t),
85            Self::Cubic { x1, y1, x2, y2 } => cubic_bezier(t, *x1, *y1, *x2, *y2),
86        }
87    }
88}
89
90// Quadratic easing functions
91fn quad_in(t: f32) -> f32 {
92    t * t
93}
94
95fn quad_out(t: f32) -> f32 {
96    t * (2.0 - t)
97}
98
99fn quad_in_out(t: f32) -> f32 {
100    if t < 0.5 {
101        2.0 * t * t
102    } else {
103        -1.0 + (4.0 - 2.0 * t) * t
104    }
105}
106
107// Cubic easing functions
108fn cubic_in(t: f32) -> f32 {
109    t * t * t
110}
111
112fn cubic_out(t: f32) -> f32 {
113    let t = t - 1.0;
114    t * t * t + 1.0
115}
116
117fn cubic_in_out(t: f32) -> f32 {
118    if t < 0.5 {
119        4.0 * t * t * t
120    } else {
121        let t = 2.0 * t - 2.0;
122        1.0 + t * t * t / 2.0
123    }
124}
125
126// Exponential easing functions
127fn expo_in(t: f32) -> f32 {
128    if t == 0.0 {
129        0.0
130    } else {
131        2.0f32.powf(10.0 * (t - 1.0))
132    }
133}
134
135fn expo_out(t: f32) -> f32 {
136    if t == 1.0 {
137        1.0
138    } else {
139        1.0 - 2.0f32.powf(-10.0 * t)
140    }
141}
142
143fn expo_in_out(t: f32) -> f32 {
144    if t == 0.0 {
145        return 0.0;
146    }
147    if t == 1.0 {
148        return 1.0;
149    }
150
151    if t < 0.5 {
152        2.0f32.powf(20.0 * t - 10.0) / 2.0
153    } else {
154        (2.0 - 2.0f32.powf(-20.0 * t + 10.0)) / 2.0
155    }
156}
157
158// Elastic easing (spring effect)
159fn elastic_in(t: f32) -> f32 {
160    if t == 0.0 || t == 1.0 {
161        return t;
162    }
163
164    let p = 0.3;
165    let s = p / 4.0;
166    let t = t - 1.0;
167
168    -(2.0f32.powf(10.0 * t) * ((t - s) * (2.0 * std::f32::consts::PI) / p).sin())
169}
170
171fn elastic_out(t: f32) -> f32 {
172    if t == 0.0 || t == 1.0 {
173        return t;
174    }
175
176    let p = 0.3;
177    let s = p / 4.0;
178
179    2.0f32.powf(-10.0 * t) * ((t - s) * (2.0 * std::f32::consts::PI) / p).sin() + 1.0
180}
181
182// Bounce easing
183fn bounce_out(t: f32) -> f32 {
184    if t < 1.0 / 2.75 {
185        7.5625 * t * t
186    } else if t < 2.0 / 2.75 {
187        let t = t - 1.5 / 2.75;
188        7.5625 * t * t + 0.75
189    } else if t < 2.5 / 2.75 {
190        let t = t - 2.25 / 2.75;
191        7.5625 * t * t + 0.9375
192    } else {
193        let t = t - 2.625 / 2.75;
194        7.5625 * t * t + 0.984_375
195    }
196}
197
198// Cubic bezier using Newton-Raphson to solve for t given x
199fn cubic_bezier(t: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
200    // First, we need to solve for the parameter value that gives us our input t
201    // in the x-dimension, then use that to calculate the y value
202
203    // For standard easing curves, x1 and x2 should be in [0, 1]
204    let x1 = x1.clamp(0.0, 1.0);
205    let x2 = x2.clamp(0.0, 1.0);
206
207    // If the curve is linear in x, we can directly compute y
208    if x1 == 0.0 && x2 == 1.0 {
209        return t;
210    }
211
212    // Use Newton-Raphson to find the t value for the given x
213    let mut guess = t;
214    for _ in 0..8 {
215        // Calculate x for current guess using cubic bezier formula
216        let guess_x = cubic_bezier_x(guess, x1, x2);
217        let error = guess_x - t;
218
219        if error.abs() < 0.001 {
220            break;
221        }
222
223        // Calculate derivative
224        let slope = cubic_bezier_x_derivative(guess, x1, x2);
225        if slope.abs() < 0.000_001 {
226            break;
227        }
228
229        // Newton-Raphson step
230        guess -= error / slope;
231        guess = guess.clamp(0.0, 1.0);
232    }
233
234    // Now calculate y using the found t value
235    cubic_bezier_y(guess, y1, y2)
236}
237
238// Calculate x coordinate of cubic bezier at parameter t
239// B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃
240// Where P₀ = (0,0) and P₃ = (1,1)
241fn cubic_bezier_x(t: f32, x1: f32, x2: f32) -> f32 {
242    let t2 = t * t;
243    let t3 = t2 * t;
244    let mt = 1.0 - t;
245    let mt2 = mt * mt;
246
247    3.0 * mt2 * t * x1 + 3.0 * mt * t2 * x2 + t3
248}
249
250// Calculate y coordinate of cubic bezier at parameter t
251fn cubic_bezier_y(t: f32, y1: f32, y2: f32) -> f32 {
252    let t2 = t * t;
253    let t3 = t2 * t;
254    let mt = 1.0 - t;
255    let mt2 = mt * mt;
256
257    3.0 * mt2 * t * y1 + 3.0 * mt * t2 * y2 + t3
258}
259
260// Derivative of cubic bezier x with respect to t
261fn cubic_bezier_x_derivative(t: f32, x1: f32, x2: f32) -> f32 {
262    let mt = 1.0 - t;
263    let mt2 = mt * mt;
264    let t2 = t * t;
265
266    3.0 * mt2 * x1 + 6.0 * mt * t * (x2 - x1) + 3.0 * t2 * (1.0 - x2)
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_linear() {
275        assert_eq!(EasingFunction::Linear.apply(0.0), 0.0);
276        assert_eq!(EasingFunction::Linear.apply(0.5), 0.5);
277        assert_eq!(EasingFunction::Linear.apply(1.0), 1.0);
278    }
279
280    #[test]
281    fn test_ease_in_out_bounds() {
282        let result = EasingFunction::EaseInOut.apply(0.0);
283        assert!((0.0..=1.0).contains(&result));
284
285        let result = EasingFunction::EaseInOut.apply(1.0);
286        assert!((0.0..=1.0).contains(&result));
287    }
288
289    #[test]
290    fn test_all_easing_functions() {
291        let functions = [
292            EasingFunction::Linear,
293            EasingFunction::EaseIn,
294            EasingFunction::EaseOut,
295            EasingFunction::EaseInOut,
296            EasingFunction::QuadIn,
297            EasingFunction::QuadOut,
298            EasingFunction::QuadInOut,
299            EasingFunction::CubicIn,
300            EasingFunction::CubicOut,
301            EasingFunction::CubicInOut,
302        ];
303
304        for func in &functions {
305            // All functions should start at 0 and end at 1
306            assert!((func.apply(0.0) - 0.0).abs() < 0.001);
307            assert!((func.apply(1.0) - 1.0).abs() < 0.001);
308
309            // All results should be in valid range
310            for i in 0..=10 {
311                let t = i as f32 / 10.0;
312                let result = func.apply(t);
313                assert!(
314                    (-0.1..=1.1).contains(&result),
315                    "Easing {func:?} at t={t} gave {result}"
316                );
317            }
318        }
319    }
320}