easing_function/
easings.rs

1//! Built-in [`Easing`] implementations.
2
3use core::f32::consts::PI;
4
5use crate::{Easing, EasingFunction, EasingKind};
6
7macro_rules! declare_easing_function {
8    ($name:ident, $($anchor_name:ident)?, $description:literal, $closure:expr) => {
9        /// An [`Easing`] function that eases
10        #[doc = $description]
11        $(#[doc = concat!("\n\nSee <https://easings.net/#", stringify!($anchor_name), "> for a visualization and more information.")])?
12        #[derive(Clone, Copy, Debug)]
13        pub struct $name;
14
15        impl $name {
16            /// Eases
17            #[doc = $description]
18            $(#[doc = concat!("\n\nSee <https://easings.net/#", stringify!($anchor_name), "> for a visualization and more information.")])?
19            #[must_use]
20            pub fn ease(progress: f32) -> f32 {
21                let closure = force_closure_type($closure);
22                closure(progress)
23            }
24        }
25
26        impl Easing for $name {
27            fn ease(&self, progress: f32) -> f32 {
28                Self::ease(progress)
29            }
30        }
31
32        impl From<$name> for EasingFunction {
33            fn from(_function: $name) -> Self {
34                Self::from_fn($name::ease)
35            }
36        }
37    };
38}
39
40macro_rules! declare_easing_functions {
41    ($(($name:ident, $([$anchor_name:ident],)? $name_no_ease:ident, $description:literal, $closure:expr)),+) => {
42        /// An enumeration of all strandard easings provided.
43        #[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
44        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45        pub enum StandardEasing {
46            $(
47                /// Eases
48                #[doc = $description]
49                $(#[doc = concat!("\n\nSee <https://easings.net/#", stringify!($anchor_name), "> for a visualization and more information.")])?
50                $name_no_ease,
51            )+
52        }
53
54        impl StandardEasing {
55            /// Returns a collection of every variant of this enum.
56            #[must_use]
57            pub fn all() -> &'static [StandardEasing] {
58                static ALL: [StandardEasing; 31] = [
59                    $(StandardEasing::$name_no_ease),+
60                ];
61                &ALL
62            }
63        }
64
65        impl From<StandardEasing> for EasingFunction {
66            fn from(easing: StandardEasing) -> Self {
67                match easing {
68                    $(StandardEasing::$name_no_ease => Self::from($name)),+
69                }
70            }
71        }
72
73        impl TryFrom<EasingFunction> for StandardEasing {
74            type Error = NonStandardEasing;
75
76            fn try_from(func: EasingFunction) -> Result<Self, Self::Error> {
77                let EasingKind::Fn(easing_fn) = &func.0 else {
78                    return Err(NonStandardEasing(func))
79                };
80                let easing_fn = *easing_fn as *const fn(f32) -> f32;
81
82                if false {
83                    unreachable!()
84                } $(else
85                if easing_fn == $name::ease as *const fn(f32) -> f32 {
86                     Ok(Self::$name_no_ease)
87                })+ else  {
88                    Err(NonStandardEasing(func))
89                }
90            }
91        }
92
93        impl Easing for StandardEasing {
94            fn ease(&self, percent: f32) -> f32 {
95                match self {
96                    $(Self::$name_no_ease => $name::ease(percent)),+
97                }
98            }
99        }
100
101        $(
102            declare_easing_function!($name, $($anchor_name)?, $description, $closure);
103        )+
104    };
105}
106
107impl Default for StandardEasing {
108    fn default() -> Self {
109        Self::Linear
110    }
111}
112
113/// An error returned from [`StandardEasing::try_from`] indicating the
114/// [`EasingFunction`] is not a standard easing function.
115#[derive(Debug, Clone, PartialEq)]
116pub struct NonStandardEasing(pub EasingFunction);
117
118// This prevents the closures from requiring the parameter to be type annotated.
119fn force_closure_type(f: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
120    f
121}
122
123declare_easing_functions!(
124    (
125        EaseInSine,
126        [easeInSine],
127        InSine,
128        "in using a sine wave",
129        |percent| 1. - (percent * PI / 2.).cos()
130    ),
131    (
132        EaseOutSine,
133        [easeOutSine],
134        OutSine,
135        "out using a sine wave",
136        |percent| (percent * PI / 2.).sin()
137    ),
138    (
139        EaseInOutSine,
140        [easeInOutSine],
141        InOutSine,
142        "in and out using a sine wave",
143        |percent| -((percent * PI).cos() - 1.) / 2.
144    ),
145    (
146        EaseInQuadradic,
147        [easeInQuad],
148        InQuadradic,
149        "in using a quadradic (x^2) curve",
150        squared
151    ),
152    (
153        EaseOutQuadradic,
154        [easeOutQuad],
155        OutQuadradic,
156        "out using a quadradic (x^2) curve",
157        |percent| 1. - squared(1. - percent)
158    ),
159    (
160        EaseInOutQuadradic,
161        [easeInOutQuad],
162        InOutQuadradic,
163        "in and out using a quadradic (x^2) curve",
164        |percent| {
165            if percent < 0.5 {
166                2. * percent * percent
167            } else {
168                1. - squared(-2. * percent + 2.) / 2.
169            }
170        }
171    ),
172    (
173        EaseInCubic,
174        [easeInCubic],
175        InCubic,
176        "in using a cubic (x^3) curve",
177        cubed
178    ),
179    (
180        EaseOutCubic,
181        [easeOutCubic],
182        OutCubic,
183        "out using a cubic (x^3) curve",
184        |percent| 1. - cubed(1. - percent)
185    ),
186    (
187        EaseInOutCubic,
188        [easeInOutCubic],
189        InOutCubic,
190        "in and out using a cubic (x^3) curve",
191        |percent| {
192            if percent < 0.5 {
193                4. * cubed(percent)
194            } else {
195                1. - cubed(-2. * percent + 2.) / 2.
196            }
197        }
198    ),
199    (
200        EaseInQuartic,
201        [easeInQuart],
202        InQuartic,
203        "in using a quartic (x^4) curve",
204        quarted
205    ),
206    (
207        EaseOutQuartic,
208        [easeOutQuart],
209        OutQuartic,
210        "out using a quartic (x^4) curve",
211        |percent| 1. - quarted(1. - percent)
212    ),
213    (
214        EaseInOutQuartic,
215        [easeInOutQuart],
216        InOutQuartic,
217        "in and out using a quartic (x^4) curve",
218        |percent| {
219            if percent < 0.5 {
220                8. * quarted(percent)
221            } else {
222                1. - quarted(-2. * percent + 2.) / 2.
223            }
224        }
225    ),
226    (
227        EaseInQuintic,
228        [easeInQuint],
229        InQuintic,
230        "in using a quintic (x^5) curve",
231        quinted
232    ),
233    (
234        EaseOutQuintic,
235        [easeOutQuint],
236        OutQuintic,
237        "out using a quintic (x^5) curve",
238        |percent| 1. - quinted(1. - percent)
239    ),
240    (
241        EaseInOutQuintic,
242        [easeInOutQuint],
243        InOutQuintic,
244        "in and out using a quintic (x^5) curve",
245        |percent| {
246            if percent < 0.5 {
247                16. * quinted(percent)
248            } else {
249                1. - quinted(-2. * percent + 2.) / 2.
250            }
251        }
252    ),
253    (
254        EaseInExponential,
255        [easeInExpo],
256        InExponential,
257        "in using an expenential curve",
258        |percent| { 2f32.powf(10. * percent - 10.) }
259    ),
260    (
261        EaseOutExponential,
262        [easeOutExpo],
263        OutExponential,
264        "out using an expenential curve",
265        |percent| { 1. - 2f32.powf(-10. * percent) }
266    ),
267    (
268        EaseInOutExponential,
269        [easeInOutExpo],
270        InOutExponential,
271        "in and out using an expenential curve",
272        |percent| if percent < 0.5 {
273            2f32.powf(20. * percent - 10.) / 2.
274        } else {
275            (2. - 2f32.powf(-20. * percent + 10.)) / 2.
276        }
277    ),
278    (
279        EaseInCircular,
280        [easeInCirc],
281        InCircular,
282        "in using a curve resembling the top-left arc of a circle",
283        |percent| 1. - (1. - squared(percent)).sqrt()
284    ),
285    (
286        EaseOutCircular,
287        [easeOutCirc],
288        OutCircular,
289        "out using a curve resembling the top-left arc of a circle",
290        |percent| (1. - squared(percent - 1.)).sqrt()
291    ),
292    (
293        EaseInOutCircular,
294        [easeInOutCirc],
295        InOutCircular,
296        "in and out using a curve resembling the top-left arc of a circle",
297        |percent| {
298            if percent < 0.5 {
299                (1. - (1. - squared(2. * percent)).sqrt()) / 2.
300            } else {
301                ((1. - squared(-2. * percent + 2.)).sqrt() + 1.) / 2.
302            }
303        }
304    ),
305    (
306        EaseInBack,
307        [easeInBack],
308        InBack,
309        "in using a curve that backs away initially",
310        |percent| {
311            let squared = squared(percent);
312            let cubed = squared * percent;
313            C3 * cubed - C1 * squared
314        }
315    ),
316    (
317        EaseOutBack,
318        [easeOutBack],
319        OutBack,
320        "out using a curve that backs away initially",
321        |percent| {
322            let percent_minus_one = percent - 1.;
323            let squared = squared(percent_minus_one);
324            let cubed = squared * percent_minus_one;
325            1. + C3 * cubed + C1 * squared
326        }
327    ),
328    (
329        EaseInOutBack,
330        [easeInOutBack],
331        InOutBack,
332        "in and out using a curve that backs away initially",
333        |percent| {
334            if percent < 0.5 {
335                (squared(2. * percent) * ((C2 + 1.) * 2. * percent - C2)) / 2.
336            } else {
337                (squared(2. * percent - 2.) * ((C2 + 1.) * (percent * 2. - 2.) + C2) + 2.) / 2.
338            }
339        }
340    ),
341    (
342        EaseInElastic,
343        [easeInElastic],
344        InElastic,
345        "in using a curve that bounces around the start initially then quickly accelerates",
346        |percent| { -(2f32.powf(10. * percent - 10.)) * ((percent * 10. - 10.75) * C4).sin() }
347    ),
348    (
349        EaseOutElastic,
350        [easeOutElastic],
351        OutElastic,
352        "out using a curve that bounces around the start initially then quickly accelerates",
353        |percent| { 2f32.powf(-10. * percent) * ((percent * 10. - 0.75) * C4).sin() + 1. }
354    ),
355    (
356        EaseInOutElastic,
357        [easeInOutElastic],
358        InOutElastic,
359        "in and out using a curve that bounces around the start initially then quickly accelerates",
360        |percent| if percent < 0.5 {
361            -(2f32.powf(20. * percent - 10.)) * ((percent * 20. - 11.125) * C5).sin() / 2.
362        } else {
363            2f32.powf(-20. * percent + 10.) * ((percent * 20. - 11.125) * C5).sin() / 2. + 1.
364        }
365    ),
366    (
367        EaseInBounce,
368        [easeInBounce],
369        InBounce,
370        "in using a curve that bounces progressively closer as it progresses",
371        |percent| 1. - EaseOutBounce.ease(1. - percent)
372    ),
373    (
374        EaseOutBounce,
375        [easeOutBounce],
376        OutBounce,
377        "out using a curve that bounces progressively closer as it progresses",
378        |percent| {
379            const N1: f32 = 7.5625;
380            const D1: f32 = 2.75;
381
382            if percent < 1. / D1 {
383                N1 * squared(percent)
384            } else if percent < 2. / D1 {
385                let percent = percent - 1.5 / D1;
386                N1 * squared(percent) + 0.75
387            } else if percent < 2.5 / D1 {
388                let percent = percent - 2.25 / D1;
389                N1 * squared(percent) + 0.9375
390            } else {
391                let percent = percent - 2.625 / D1;
392                N1 * squared(percent) + 0.984_375
393            }
394        }
395    ),
396    (
397        EaseInOutBounce,
398        [easeInOutBounce],
399        InOutBounce,
400        "in and out using a curve that bounces progressively closer as it progresses",
401        |percent| {
402            if percent < 0.5 {
403                (1. - EaseOutBounce::ease(1. - 2. * percent)) / 2.
404            } else {
405                (1. + EaseOutBounce::ease(2. * percent - 1.)) / 2.
406            }
407        }
408    ),
409    (Linear, Linear, "linearly", |percent| percent)
410);
411
412fn squared(value: f32) -> f32 {
413    value * value
414}
415
416fn cubed(value: f32) -> f32 {
417    value * value * value
418}
419
420fn quarted(value: f32) -> f32 {
421    let sq = squared(value);
422    squared(sq)
423}
424
425fn quinted(value: f32) -> f32 {
426    let squared = squared(value);
427    let cubed = squared * value;
428    squared * cubed
429}
430
431const C1: f32 = 1.70158;
432const C2: f32 = C1 * 1.525;
433const C3: f32 = C1 + 1.;
434const C4: f32 = (2. * PI) / 3.;
435const C5: f32 = (2. * PI) / 4.5;
436
437#[test]
438fn roundtrip() {
439    for &easing in StandardEasing::all() {
440        let f = EasingFunction::from(dbg!(easing));
441        let rt = StandardEasing::try_from(f);
442        assert_eq!(rt, Ok(easing));
443    }
444}