Skip to main content

dinamika_core/
easing.rs

1//! Easing functions for interpolating animations.
2//!
3//! [`Easing`] is an enumeration of ready-made curves. Each takes a normalized
4//! time `0.0..=1.0` and returns the adjusted progress (usually also `0..=1`,
5//! but `Back`/`Elastic` may overshoot the bounds — that's expected).
6
7use std::f32::consts::PI;
8
9/// Animation easing curve.
10#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
11pub enum Easing {
12    /// Linear (no smoothing).
13    #[default]
14    Linear,
15
16    QuadIn,
17    QuadOut,
18    QuadInOut,
19
20    CubicIn,
21    CubicOut,
22    CubicInOut,
23
24    QuartIn,
25    QuartOut,
26    QuartInOut,
27
28    SineIn,
29    SineOut,
30    SineInOut,
31
32    ExpoIn,
33    ExpoOut,
34    ExpoInOut,
35
36    /// Slight backward "overshoot" at the start.
37    BackIn,
38    /// Slight "overshoot" at the end.
39    BackOut,
40    BackInOut,
41
42    /// Damped "bounce" at the start.
43    BounceIn,
44    /// Damped "bounce" at the end.
45    BounceOut,
46    BounceInOut,
47
48    /// Spring at the start.
49    ElasticIn,
50    /// Spring at the end.
51    ElasticOut,
52    ElasticInOut,
53}
54
55impl Easing {
56    /// Applies the curve to the normalized time `t` (clamped to `0..=1`).
57    pub fn apply(self, t: f32) -> f32 {
58        let t = t.clamp(0.0, 1.0);
59        match self {
60            Easing::Linear => t,
61
62            Easing::QuadIn => t * t,
63            Easing::QuadOut => 1.0 - (1.0 - t) * (1.0 - t),
64            Easing::QuadInOut => {
65                if t < 0.5 {
66                    2.0 * t * t
67                } else {
68                    1.0 - 2.0 * (1.0 - t) * (1.0 - t)
69                }
70            }
71
72            Easing::CubicIn => t * t * t,
73            Easing::CubicOut => {
74                let u = 1.0 - t;
75                1.0 - u * u * u
76            }
77            Easing::CubicInOut => {
78                if t < 0.5 {
79                    4.0 * t * t * t
80                } else {
81                    let u = -2.0 * t + 2.0;
82                    1.0 - u * u * u / 2.0
83                }
84            }
85
86            Easing::QuartIn => t * t * t * t,
87            Easing::QuartOut => {
88                let u = 1.0 - t;
89                1.0 - u * u * u * u
90            }
91            Easing::QuartInOut => {
92                if t < 0.5 {
93                    8.0 * t * t * t * t
94                } else {
95                    let u = -2.0 * t + 2.0;
96                    1.0 - u * u * u * u / 2.0
97                }
98            }
99
100            Easing::SineIn => 1.0 - (t * PI / 2.0).cos(),
101            Easing::SineOut => (t * PI / 2.0).sin(),
102            Easing::SineInOut => -((PI * t).cos() - 1.0) / 2.0,
103
104            Easing::ExpoIn => {
105                if t <= 0.0 {
106                    0.0
107                } else {
108                    2f32.powf(10.0 * t - 10.0)
109                }
110            }
111            Easing::ExpoOut => {
112                if t >= 1.0 {
113                    1.0
114                } else {
115                    1.0 - 2f32.powf(-10.0 * t)
116                }
117            }
118            Easing::ExpoInOut => {
119                if t <= 0.0 {
120                    0.0
121                } else if t >= 1.0 {
122                    1.0
123                } else if t < 0.5 {
124                    2f32.powf(20.0 * t - 10.0) / 2.0
125                } else {
126                    (2.0 - 2f32.powf(-20.0 * t + 10.0)) / 2.0
127                }
128            }
129
130            Easing::BackIn => {
131                let c1 = 1.70158;
132                let c3 = c1 + 1.0;
133                c3 * t * t * t - c1 * t * t
134            }
135            Easing::BackOut => {
136                let c1 = 1.70158;
137                let c3 = c1 + 1.0;
138                let u = t - 1.0;
139                1.0 + c3 * u * u * u + c1 * u * u
140            }
141            Easing::BackInOut => {
142                let c1 = 1.70158;
143                let c2 = c1 * 1.525;
144                if t < 0.5 {
145                    let u = 2.0 * t;
146                    (u * u * ((c2 + 1.0) * u - c2)) / 2.0
147                } else {
148                    let u = 2.0 * t - 2.0;
149                    (u * u * ((c2 + 1.0) * u + c2) + 2.0) / 2.0
150                }
151            }
152
153            Easing::BounceIn => 1.0 - bounce_out(1.0 - t),
154            Easing::BounceOut => bounce_out(t),
155            Easing::BounceInOut => {
156                if t < 0.5 {
157                    (1.0 - bounce_out(1.0 - 2.0 * t)) / 2.0
158                } else {
159                    (1.0 + bounce_out(2.0 * t - 1.0)) / 2.0
160                }
161            }
162
163            Easing::ElasticIn => {
164                if t <= 0.0 {
165                    0.0
166                } else if t >= 1.0 {
167                    1.0
168                } else {
169                    let c4 = (2.0 * PI) / 3.0;
170                    -(2f32.powf(10.0 * t - 10.0)) * ((10.0 * t - 10.75) * c4).sin()
171                }
172            }
173            Easing::ElasticOut => {
174                if t <= 0.0 {
175                    0.0
176                } else if t >= 1.0 {
177                    1.0
178                } else {
179                    let c4 = (2.0 * PI) / 3.0;
180                    2f32.powf(-10.0 * t) * ((10.0 * t - 0.75) * c4).sin() + 1.0
181                }
182            }
183            Easing::ElasticInOut => {
184                if t <= 0.0 {
185                    0.0
186                } else if t >= 1.0 {
187                    1.0
188                } else {
189                    let c5 = (2.0 * PI) / 4.5;
190                    if t < 0.5 {
191                        -(2f32.powf(20.0 * t - 10.0) * ((20.0 * t - 11.125) * c5).sin()) / 2.0
192                    } else {
193                        (2f32.powf(-20.0 * t + 10.0) * ((20.0 * t - 11.125) * c5).sin()) / 2.0 + 1.0
194                    }
195                }
196            }
197        }
198    }
199}
200
201fn bounce_out(t: f32) -> f32 {
202    let n1 = 7.5625;
203    let d1 = 2.75;
204    if t < 1.0 / d1 {
205        n1 * t * t
206    } else if t < 2.0 / d1 {
207        let t = t - 1.5 / d1;
208        n1 * t * t + 0.75
209    } else if t < 2.5 / d1 {
210        let t = t - 2.25 / d1;
211        n1 * t * t + 0.9375
212    } else {
213        let t = t - 2.625 / d1;
214        n1 * t * t + 0.984375
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn endpoints_are_anchored() {
224        for e in [
225            Easing::Linear,
226            Easing::QuadInOut,
227            Easing::CubicInOut,
228            Easing::SineInOut,
229            Easing::ExpoInOut,
230            Easing::BounceOut,
231            Easing::ElasticOut,
232        ] {
233            assert!((e.apply(0.0) - 0.0).abs() < 1e-3, "{e:?} at 0");
234            assert!((e.apply(1.0) - 1.0).abs() < 1e-3, "{e:?} at 1");
235        }
236    }
237
238    #[test]
239    fn linear_is_identity() {
240        assert!((Easing::Linear.apply(0.37) - 0.37).abs() < 1e-6);
241    }
242
243    #[test]
244    fn clamps_out_of_range() {
245        assert_eq!(Easing::Linear.apply(-1.0), 0.0);
246        assert_eq!(Easing::Linear.apply(2.0), 1.0);
247    }
248}