Skip to main content

ply_engine/
easing.rs

1use core::f32::consts::PI;
2
3#[inline]
4pub fn ease_in_quad(t: f32) -> f32 {
5    t * t
6}
7
8#[inline]
9pub fn ease_out_quad(t: f32) -> f32 {
10    1.0 - (1.0 - t) * (1.0 - t)
11}
12
13#[inline]
14pub fn ease_in_out_quad(t: f32) -> f32 {
15    if t < 0.5 {
16        2.0 * t * t
17    } else {
18        1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
19    }
20}
21
22#[inline]
23pub fn ease_in_cubic(t: f32) -> f32 {
24    t * t * t
25}
26
27#[inline]
28pub fn ease_out_cubic(t: f32) -> f32 {
29    1.0 - (1.0 - t).powi(3)
30}
31
32#[inline]
33pub fn ease_in_out_cubic(t: f32) -> f32 {
34    if t < 0.5 {
35        4.0 * t * t * t
36    } else {
37        1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
38    }
39}
40
41#[inline]
42pub fn ease_in_quart(t: f32) -> f32 {
43    t * t * t * t
44}
45
46#[inline]
47pub fn ease_out_quart(t: f32) -> f32 {
48    1.0 - (1.0 - t).powi(4)
49}
50
51#[inline]
52pub fn ease_in_out_quart(t: f32) -> f32 {
53    if t < 0.5 {
54        8.0 * t * t * t * t
55    } else {
56        1.0 - (-2.0 * t + 2.0).powi(4) / 2.0
57    }
58}
59
60#[inline]
61pub fn ease_in_sine(t: f32) -> f32 {
62    1.0 - ((t * PI) / 2.0).cos()
63}
64
65#[inline]
66pub fn ease_out_sine(t: f32) -> f32 {
67    ((t * PI) / 2.0).sin()
68}
69
70#[inline]
71pub fn ease_in_out_sine(t: f32) -> f32 {
72    -((PI * t).cos() - 1.0) / 2.0
73}
74
75#[inline]
76pub fn ease_in_expo(t: f32) -> f32 {
77    if t <= 0.0 {
78        0.0
79    } else if t >= 1.0 {
80        1.0
81    } else {
82        2.0_f32.powf(10.0 * t - 10.0)
83    }
84}
85
86#[inline]
87pub fn ease_out_expo(t: f32) -> f32 {
88    if t <= 0.0 {
89        0.0
90    } else if t >= 1.0 {
91        1.0
92    } else {
93        1.0 - 2.0_f32.powf(-10.0 * t)
94    }
95}
96
97#[inline]
98pub fn ease_in_out_expo(t: f32) -> f32 {
99    if t <= 0.0 {
100        0.0
101    } else if t >= 1.0 {
102        1.0
103    } else if t < 0.5 {
104        2.0_f32.powf(20.0 * t - 10.0) / 2.0
105    } else {
106        (2.0 - 2.0_f32.powf(-20.0 * t + 10.0)) / 2.0
107    }
108}
109
110#[inline]
111pub fn ease_in_back(t: f32) -> f32 {
112    let c1 = 1.70158;
113    let c3 = c1 + 1.0;
114    c3 * t * t * t - c1 * t * t
115}
116
117#[inline]
118pub fn ease_out_back(t: f32) -> f32 {
119    let c1 = 1.70158;
120    let c3 = c1 + 1.0;
121    1.0 + c3 * (t - 1.0).powi(3) + c1 * (t - 1.0).powi(2)
122}
123
124#[inline]
125pub fn ease_in_out_back(t: f32) -> f32 {
126    let c1 = 1.70158;
127    let c2 = c1 * 1.525;
128
129    if t < 0.5 {
130        ((2.0 * t).powi(2) * ((c2 + 1.0) * 2.0 * t - c2)) / 2.0
131    } else {
132        ((2.0 * t - 2.0).powi(2) * ((c2 + 1.0) * (2.0 * t - 2.0) + c2) + 2.0)
133            / 2.0
134    }
135}
136
137#[inline]
138pub fn ease_in_elastic(t: f32) -> f32 {
139    let c4 = (2.0 * PI) / 3.0;
140
141    if t <= 0.0 {
142        0.0
143    } else if t >= 1.0 {
144        1.0
145    } else {
146        -(2.0_f32.powf(10.0 * t - 10.0)) * ((t * 10.0 - 10.75) * c4).sin()
147    }
148}
149
150#[inline]
151pub fn ease_out_elastic(t: f32) -> f32 {
152    let c4 = (2.0 * PI) / 3.0;
153
154    if t <= 0.0 {
155        0.0
156    } else if t >= 1.0 {
157        1.0
158    } else {
159        2.0_f32.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
160    }
161}
162
163#[inline]
164pub fn ease_in_out_elastic(t: f32) -> f32 {
165    let c5 = (2.0 * PI) / 4.5;
166
167    if t <= 0.0 {
168        0.0
169    } else if t >= 1.0 {
170        1.0
171    } else if t < 0.5 {
172        -(2.0_f32.powf(20.0 * t - 10.0) * ((20.0 * t - 11.125) * c5).sin()) / 2.0
173    } else {
174        (2.0_f32.powf(-20.0 * t + 10.0) * ((20.0 * t - 11.125) * c5).sin()) / 2.0 + 1.0
175    }
176}
177
178#[inline]
179fn ease_out_bounce_internal(t: f32) -> f32 {
180    let n1 = 7.5625;
181    let d1 = 2.75;
182
183    if t < 1.0 / d1 {
184        n1 * t * t
185    } else if t < 2.0 / d1 {
186        let x = t - 1.5 / d1;
187        n1 * x * x + 0.75
188    } else if t < 2.5 / d1 {
189        let x = t - 2.25 / d1;
190        n1 * x * x + 0.9375
191    } else {
192        let x = t - 2.625 / d1;
193        n1 * x * x + 0.984375
194    }
195}
196
197#[inline]
198pub fn ease_out_bounce(t: f32) -> f32 {
199    ease_out_bounce_internal(t)
200}
201
202#[inline]
203pub fn ease_in_bounce(t: f32) -> f32 {
204    1.0 - ease_out_bounce_internal(1.0 - t)
205}
206
207#[inline]
208pub fn ease_in_out_bounce(t: f32) -> f32 {
209    if t < 0.5 {
210        (1.0 - ease_out_bounce_internal(1.0 - 2.0 * t)) / 2.0
211    } else {
212        (1.0 + ease_out_bounce_internal(2.0 * t - 1.0)) / 2.0
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    type EaseFn = fn(f32) -> f32;
221
222    fn assert_close(a: f32, b: f32) {
223        assert!(
224            (a - b).abs() <= 0.0001,
225            "expected {} ~= {} (delta {})",
226            a,
227            b,
228            (a - b).abs()
229        );
230    }
231
232    #[test]
233    fn test_all_easing_endpoints() {
234        let all: [EaseFn; 24] = [
235            ease_in_quad,
236            ease_out_quad,
237            ease_in_out_quad,
238            ease_in_cubic,
239            ease_out_cubic,
240            ease_in_out_cubic,
241            ease_in_quart,
242            ease_out_quart,
243            ease_in_out_quart,
244            ease_in_sine,
245            ease_out_sine,
246            ease_in_out_sine,
247            ease_in_expo,
248            ease_out_expo,
249            ease_in_out_expo,
250            ease_in_elastic,
251            ease_out_elastic,
252            ease_in_out_elastic,
253            ease_in_bounce,
254            ease_out_bounce,
255            ease_in_out_bounce,
256            ease_in_back,
257            ease_out_back,
258            ease_in_out_back,
259        ];
260
261        for ease in all {
262            assert_close(ease(0.0), 0.0);
263            assert_close(ease(1.0), 1.0);
264        }
265    }
266
267    #[test]
268    fn test_simple_midpoints() {
269        assert_close(ease_in_quad(0.5), 0.25);
270        assert_close(ease_out_quad(0.5), 0.75);
271        assert_close(ease_in_out_quad(0.5), 0.5);
272
273        assert_close(ease_in_cubic(0.5), 0.125);
274        assert_close(ease_out_cubic(0.5), 0.875);
275        assert_close(ease_in_out_cubic(0.5), 0.5);
276
277        assert_close(ease_in_quart(0.5), 0.0625);
278        assert_close(ease_out_quart(0.5), 0.9375);
279        assert_close(ease_in_out_quart(0.5), 0.5);
280
281        assert_close(ease_in_sine(0.5), 0.29289323);
282        assert_close(ease_out_sine(0.5), 0.70710677);
283        assert_close(ease_in_out_sine(0.5), 0.5);
284
285        assert_close(ease_in_expo(0.5), 0.03125);
286        assert_close(ease_out_expo(0.5), 0.96875);
287        assert_close(ease_in_out_expo(0.5), 0.5);
288    }
289
290    #[test]
291    fn test_non_overshoot_easing_stays_in_unit_range() {
292        let non_overshoot: [EaseFn; 18] = [
293            ease_in_quad,
294            ease_out_quad,
295            ease_in_out_quad,
296            ease_in_cubic,
297            ease_out_cubic,
298            ease_in_out_cubic,
299            ease_in_quart,
300            ease_out_quart,
301            ease_in_out_quart,
302            ease_in_sine,
303            ease_out_sine,
304            ease_in_out_sine,
305            ease_in_expo,
306            ease_out_expo,
307            ease_in_out_expo,
308            ease_in_bounce,
309            ease_out_bounce,
310            ease_in_out_bounce,
311        ];
312
313        for ease in non_overshoot {
314            for i in 0..=1000 {
315                let t = i as f32 / 1000.0;
316                let y = ease(t);
317                assert!(
318                    (0.0..=1.0).contains(&y),
319                    "value {} out of range for t={}",
320                    y,
321                    t
322                );
323            }
324        }
325    }
326
327    #[test]
328    fn test_back_and_elastic_overshoot() {
329        let overshooting: [EaseFn; 6] = [
330            ease_in_back,
331            ease_out_back,
332            ease_in_out_back,
333            ease_in_elastic,
334            ease_out_elastic,
335            ease_in_out_elastic,
336        ];
337
338        for ease in overshooting {
339            let mut found = false;
340            for i in 0..=1000 {
341                let t = i as f32 / 1000.0;
342                let y = ease(t);
343                if !(0.0..=1.0).contains(&y) {
344                    found = true;
345                    break;
346                }
347            }
348            assert!(found, "expected overshoot for easing function");
349        }
350
351        assert!(ease_in_back(0.5) < 0.0);
352        assert!(ease_out_back(0.5) > 1.0);
353    }
354}