leptos_motion_core/
easing.rs

1//! Easing functions for smooth animation transitions
2
3use crate::{Easing, SpringConfig};
4
5/// Easing function type
6pub type EasingFn = fn(f64) -> f64;
7
8impl Easing {
9    /// Evaluate the easing function at time t (0.0 to 1.0)
10    pub fn evaluate(&self, t: f64) -> f64 {
11        let t = t.clamp(0.0, 1.0);
12
13        match self {
14            Easing::Linear => linear(t),
15            Easing::EaseIn => ease_in_quad(t),
16            Easing::EaseOut => ease_out_quad(t),
17            Easing::EaseInOut => ease_in_out_quad(t),
18            Easing::CircIn => circ_in(t),
19            Easing::CircOut => circ_out(t),
20            Easing::CircInOut => circ_in_out(t),
21            Easing::BackIn => back_in(t),
22            Easing::BackOut => back_out(t),
23            Easing::BackInOut => back_in_out(t),
24            Easing::Spring(config) => {
25                // For UI purposes, we approximate spring with a ease-out curve
26                // Real spring physics is handled by the spring module
27                let factor = (config.stiffness / 100.0).clamp(0.5, 2.0);
28                ease_out_expo(t).powf(1.0 / factor)
29            }
30            Easing::Bezier(x1, y1, x2, y2) => cubic_bezier(*x1, *y1, *x2, *y2, t),
31            Easing::CubicBezier(cb) => cubic_bezier(cb.0, cb.1, cb.2, cb.3, t),
32        }
33    }
34
35    /// Get a function pointer for this easing
36    pub fn as_fn(&self) -> Box<dyn Fn(f64) -> f64> {
37        match self {
38            Easing::Linear => Box::new(linear),
39            Easing::EaseIn => Box::new(ease_in_quad),
40            Easing::EaseOut => Box::new(ease_out_quad),
41            Easing::EaseInOut => Box::new(ease_in_out_quad),
42            Easing::CircIn => Box::new(circ_in),
43            Easing::CircOut => Box::new(circ_out),
44            Easing::CircInOut => Box::new(circ_in_out),
45            Easing::BackIn => Box::new(back_in),
46            Easing::BackOut => Box::new(back_out),
47            Easing::BackInOut => Box::new(back_in_out),
48            Easing::Spring(config) => {
49                let factor = (config.stiffness / 100.0).clamp(0.5, 2.0);
50                Box::new(move |t| ease_out_expo(t).powf(1.0 / factor))
51            }
52            Easing::Bezier(x1, y1, x2, y2) => {
53                let x1 = *x1;
54                let y1 = *y1;
55                let x2 = *x2;
56                let y2 = *y2;
57                Box::new(move |t| cubic_bezier(x1, y1, x2, y2, t))
58            }
59            Easing::CubicBezier(cb) => {
60                let x1 = cb.0;
61                let y1 = cb.1;
62                let x2 = cb.2;
63                let y2 = cb.3;
64                Box::new(move |t| cubic_bezier(x1, y1, x2, y2, t))
65            }
66        }
67    }
68}
69
70// Basic easing functions
71
72/// Linear interpolation (no easing)
73pub fn linear(t: f64) -> f64 {
74    t
75}
76
77/// Quadratic ease in (accelerating from zero velocity)
78pub fn ease_in_quad(t: f64) -> f64 {
79    t * t
80}
81
82/// Quadratic ease out (decelerating to zero velocity)
83pub fn ease_out_quad(t: f64) -> f64 {
84    t * (2.0 - t)
85}
86
87/// Quadratic ease in and out
88pub fn ease_in_out_quad(t: f64) -> f64 {
89    if t < 0.5 {
90        2.0 * t * t
91    } else {
92        -1.0 + (4.0 - 2.0 * t) * t
93    }
94}
95
96/// Cubic ease in
97pub fn ease_in_cubic(t: f64) -> f64 {
98    t * t * t
99}
100
101/// Cubic ease out
102pub fn ease_out_cubic(t: f64) -> f64 {
103    let t = t - 1.0;
104    t * t * t + 1.0
105}
106
107/// Cubic ease in and out
108pub fn ease_in_out_cubic(t: f64) -> f64 {
109    if t < 0.5 {
110        4.0 * t * t * t
111    } else {
112        let t = 2.0 * t - 2.0;
113        1.0 + t * t * t / 2.0
114    }
115}
116
117/// Exponential ease out
118pub fn ease_out_expo(t: f64) -> f64 {
119    if t == 1.0 {
120        1.0
121    } else {
122        1.0 - 2.0_f64.powf(-10.0 * t)
123    }
124}
125
126// Circular easing functions
127
128/// Circular ease in
129pub fn circ_in(t: f64) -> f64 {
130    1.0 - (1.0 - t * t).sqrt()
131}
132
133/// Circular ease out
134pub fn circ_out(t: f64) -> f64 {
135    let t = t - 1.0;
136    (1.0 - t * t).sqrt()
137}
138
139/// Circular ease in and out
140pub fn circ_in_out(t: f64) -> f64 {
141    if t < 0.5 {
142        (1.0 - (1.0 - 4.0 * t * t).sqrt()) / 2.0
143    } else {
144        let t = -2.0 * t + 2.0;
145        ((1.0 - t * t).sqrt() + 1.0) / 2.0
146    }
147}
148
149// Back easing functions (overshoot)
150
151const BACK_CONSTANT: f64 = 1.70158;
152
153/// Back ease in (overshoot at start)
154pub fn back_in(t: f64) -> f64 {
155    let c1 = BACK_CONSTANT;
156    let c3 = c1 + 1.0;
157    c3 * t * t * t - c1 * t * t
158}
159
160/// Back ease out (overshoot at end)
161pub fn back_out(t: f64) -> f64 {
162    let c1 = BACK_CONSTANT;
163    let c3 = c1 + 1.0;
164    let t = t - 1.0;
165    1.0 + c3 * t * t * t + c1 * t * t
166}
167
168/// Back ease in and out (overshoot at both ends)
169pub fn back_in_out(t: f64) -> f64 {
170    let c1 = BACK_CONSTANT;
171    let c2 = c1 * 1.525;
172
173    if t < 0.5 {
174        let t = 2.0 * t;
175        (t * t * ((c2 + 1.0) * t - c2)) / 2.0
176    } else {
177        let t = 2.0 * t - 2.0;
178        (t * t * ((c2 + 1.0) * t + c2) + 2.0) / 2.0
179    }
180}
181
182/// Cubic bezier easing function
183pub fn cubic_bezier(_x1: f64, y1: f64, _x2: f64, y2: f64, t: f64) -> f64 {
184    // Simplified cubic bezier approximation
185    // For production, we'd use Newton-Raphson method for precision
186    let t2 = t * t;
187    let t3 = t2 * t;
188    let mt = 1.0 - t;
189    let mt2 = mt * mt;
190    let _mt3 = mt2 * mt;
191
192    // Cubic bezier curve: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃
193    // P₀ = (0,0), P₃ = (1,1), P₁ = (x1,y1), P₂ = (x2,y2)
194    3.0 * mt2 * t * y1 + 3.0 * mt * t2 * y2 + t3
195}
196
197/// Common easing presets
198pub mod presets {
199    use super::*;
200
201    /// Material Design ease
202    pub const EASE: Easing = Easing::Bezier(0.4, 0.0, 0.2, 1.0);
203
204    /// Material Design ease in
205    pub const EASE_IN: Easing = Easing::Bezier(0.4, 0.0, 1.0, 1.0);
206
207    /// Material Design ease out
208    pub const EASE_OUT: Easing = Easing::Bezier(0.0, 0.0, 0.2, 1.0);
209
210    /// Material Design ease in out
211    pub const EASE_IN_OUT: Easing = Easing::Bezier(0.4, 0.0, 0.2, 1.0);
212
213    /// Smooth spring
214    pub const SPRING_SMOOTH: Easing = Easing::Spring(SpringConfig {
215        stiffness: 100.0,
216        damping: 20.0,
217        mass: 1.0,
218        velocity: 0.0,
219        rest_delta: 0.01,
220        rest_speed: 0.01,
221    });
222
223    /// Bouncy spring
224    pub const SPRING_BOUNCY: Easing = Easing::Spring(SpringConfig {
225        stiffness: 200.0,
226        damping: 10.0,
227        mass: 1.0,
228        velocity: 0.0,
229        rest_delta: 0.01,
230        rest_speed: 0.01,
231    });
232
233    /// Gentle spring
234    pub const SPRING_GENTLE: Easing = Easing::Spring(SpringConfig {
235        stiffness: 50.0,
236        damping: 15.0,
237        mass: 1.0,
238        velocity: 0.0,
239        rest_delta: 0.01,
240        rest_speed: 0.01,
241    });
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    #[cfg(feature = "approx")]
248    use approx::assert_relative_eq;
249
250    #[test]
251    fn test_linear_easing() {
252        assert_eq!(linear(0.0), 0.0);
253        assert_eq!(linear(0.5), 0.5);
254        assert_eq!(linear(1.0), 1.0);
255    }
256
257    #[cfg(feature = "approx")]
258    #[test]
259    fn test_ease_bounds() {
260        let easings = vec![
261            ease_in_quad,
262            ease_out_quad,
263            ease_in_out_quad,
264            ease_in_cubic,
265            ease_out_cubic,
266            ease_in_out_cubic,
267            circ_in,
268            circ_out,
269            circ_in_out,
270        ];
271
272        for easing in easings {
273            // All easing functions should start at 0 and end at 1
274            assert_relative_eq!(easing(0.0), 0.0, epsilon = 1e-10);
275            assert_relative_eq!(easing(1.0), 1.0, epsilon = 1e-10);
276
277            // Should be monotonic (non-decreasing)
278            let mut prev = easing(0.0);
279            for i in 1..=10 {
280                let t = i as f64 / 10.0;
281                let current = easing(t);
282                assert!(current >= prev - 1e-10, "Easing function not monotonic");
283                prev = current;
284            }
285        }
286    }
287
288    #[test]
289    fn test_back_easing_overshoot() {
290        // Back easing should overshoot (go beyond 1.0 before settling)
291        let mut max: f64 = 0.0;
292        for i in 0..=100 {
293            let t = i as f64 / 100.0;
294            let value = back_out(t);
295            max = max.max(value);
296        }
297        assert!(max > 1.0, "Back easing should overshoot");
298    }
299
300    #[cfg(feature = "approx")]
301    #[test]
302    fn test_cubic_bezier() {
303        // Standard ease
304        let ease = |t| cubic_bezier(0.25, 0.1, 0.25, 1.0, t);
305        assert_relative_eq!(ease(0.0), 0.0, epsilon = 1e-3);
306        assert_relative_eq!(ease(1.0), 1.0, epsilon = 1e-3);
307    }
308
309    #[test]
310    fn test_easing_evaluate() {
311        let easing = Easing::EaseInOut;
312        assert_eq!(easing.evaluate(0.0), 0.0);
313        assert_eq!(easing.evaluate(1.0), 1.0);
314
315        // Test clamping
316        assert_eq!(easing.evaluate(-0.1), 0.0);
317        assert_eq!(easing.evaluate(1.1), 1.0);
318    }
319}