bevy_math/curve/
easing.rs

1//! Module containing different [easing functions] to control the transition between two values and
2//! the [`EasingCurve`] struct to make use of them.
3//!
4//! [easing functions]: EaseFunction
5
6use crate::{
7    curve::{Curve, CurveExt, FunctionCurve, Interval},
8    Dir2, Dir3, Dir3A, Isometry2d, Isometry3d, Quat, Rot2, VectorSpace,
9};
10
11#[cfg(feature = "bevy_reflect")]
12use bevy_reflect::std_traits::ReflectDefault;
13
14use variadics_please::all_tuples_enumerated;
15
16// TODO: Think about merging `Ease` with `StableInterpolate`
17
18/// A type whose values can be eased between.
19///
20/// This requires the construction of an interpolation curve that actually extends
21/// beyond the curve segment that connects two values, because an easing curve may
22/// extrapolate before the starting value and after the ending value. This is
23/// especially common in easing functions that mimic elastic or springlike behavior.
24pub trait Ease: Sized {
25    /// Given `start` and `end` values, produce a curve with [unlimited domain]
26    /// that:
27    /// - takes a value equivalent to `start` at `t = 0`
28    /// - takes a value equivalent to `end` at `t = 1`
29    /// - has constant speed everywhere, including outside of `[0, 1]`
30    ///
31    /// [unlimited domain]: Interval::EVERYWHERE
32    fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self>;
33}
34
35impl<V: VectorSpace> Ease for V {
36    fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
37        FunctionCurve::new(Interval::EVERYWHERE, move |t| V::lerp(start, end, t))
38    }
39}
40
41impl Ease for Rot2 {
42    fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
43        FunctionCurve::new(Interval::EVERYWHERE, move |t| Rot2::slerp(start, end, t))
44    }
45}
46
47impl Ease for Quat {
48    fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
49        let dot = start.dot(end);
50        let end_adjusted = if dot < 0.0 { -end } else { end };
51        let difference = end_adjusted * start.inverse();
52        let (axis, angle) = difference.to_axis_angle();
53        FunctionCurve::new(Interval::EVERYWHERE, move |s| {
54            Quat::from_axis_angle(axis, angle * s) * start
55        })
56    }
57}
58
59impl Ease for Dir2 {
60    fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
61        FunctionCurve::new(Interval::EVERYWHERE, move |t| Dir2::slerp(start, end, t))
62    }
63}
64
65impl Ease for Dir3 {
66    fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
67        let difference_quat = Quat::from_rotation_arc(start.as_vec3(), end.as_vec3());
68        Quat::interpolating_curve_unbounded(Quat::IDENTITY, difference_quat).map(move |q| q * start)
69    }
70}
71
72impl Ease for Dir3A {
73    fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
74        let difference_quat =
75            Quat::from_rotation_arc(start.as_vec3a().into(), end.as_vec3a().into());
76        Quat::interpolating_curve_unbounded(Quat::IDENTITY, difference_quat).map(move |q| q * start)
77    }
78}
79
80impl Ease for Isometry3d {
81    fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
82        FunctionCurve::new(Interval::EVERYWHERE, move |t| {
83            // we can use sample_unchecked here, since both interpolating_curve_unbounded impls
84            // used are defined on the whole domain
85            Isometry3d {
86                rotation: Quat::interpolating_curve_unbounded(start.rotation, end.rotation)
87                    .sample_unchecked(t),
88                translation: crate::Vec3A::interpolating_curve_unbounded(
89                    start.translation,
90                    end.translation,
91                )
92                .sample_unchecked(t),
93            }
94        })
95    }
96}
97
98impl Ease for Isometry2d {
99    fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
100        FunctionCurve::new(Interval::EVERYWHERE, move |t| {
101            // we can use sample_unchecked here, since both interpolating_curve_unbounded impls
102            // used are defined on the whole domain
103            Isometry2d {
104                rotation: Rot2::interpolating_curve_unbounded(start.rotation, end.rotation)
105                    .sample_unchecked(t),
106                translation: crate::Vec2::interpolating_curve_unbounded(
107                    start.translation,
108                    end.translation,
109                )
110                .sample_unchecked(t),
111            }
112        })
113    }
114}
115
116macro_rules! impl_ease_tuple {
117    ($(#[$meta:meta])* $(($n:tt, $T:ident)),*) => {
118        $(#[$meta])*
119        impl<$($T: Ease),*> Ease for ($($T,)*) {
120            fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
121                let curve_tuple =
122                (
123                    $(
124                        <$T as Ease>::interpolating_curve_unbounded(start.$n, end.$n),
125                    )*
126                );
127
128                FunctionCurve::new(Interval::EVERYWHERE, move |t|
129                    (
130                        $(
131                            curve_tuple.$n.sample_unchecked(t),
132                        )*
133                    )
134                )
135            }
136        }
137    };
138}
139
140all_tuples_enumerated!(
141    #[doc(fake_variadic)]
142    impl_ease_tuple,
143    1,
144    11,
145    T
146);
147
148/// A [`Curve`] that is defined by
149///
150/// - an initial `start` sample value at `t = 0`
151/// - a final `end` sample value at `t = 1`
152/// - an [easing function] to interpolate between the two values.
153///
154/// The resulting curve's domain is always [the unit interval].
155///
156/// # Example
157///
158/// Create a linear curve that interpolates between `2.0` and `4.0`.
159///
160/// ```
161/// # use bevy_math::prelude::*;
162/// let c = EasingCurve::new(2.0, 4.0, EaseFunction::Linear);
163/// ```
164///
165/// [`sample`] the curve at various points. This will return `None` if the parameter
166/// is outside the unit interval.
167///
168/// ```
169/// # use bevy_math::prelude::*;
170/// # let c = EasingCurve::new(2.0, 4.0, EaseFunction::Linear);
171/// assert_eq!(c.sample(-1.0), None);
172/// assert_eq!(c.sample(0.0), Some(2.0));
173/// assert_eq!(c.sample(0.5), Some(3.0));
174/// assert_eq!(c.sample(1.0), Some(4.0));
175/// assert_eq!(c.sample(2.0), None);
176/// ```
177///
178/// [`sample_clamped`] will clamp the parameter to the unit interval, so it
179/// always returns a value.
180///
181/// ```
182/// # use bevy_math::prelude::*;
183/// # let c = EasingCurve::new(2.0, 4.0, EaseFunction::Linear);
184/// assert_eq!(c.sample_clamped(-1.0), 2.0);
185/// assert_eq!(c.sample_clamped(0.0), 2.0);
186/// assert_eq!(c.sample_clamped(0.5), 3.0);
187/// assert_eq!(c.sample_clamped(1.0), 4.0);
188/// assert_eq!(c.sample_clamped(2.0), 4.0);
189/// ```
190///
191/// `EasingCurve` can be used with any type that implements the [`Ease`] trait.
192/// This includes many math types, like vectors and rotations.
193///
194/// ```
195/// # use bevy_math::prelude::*;
196/// let c = EasingCurve::new(
197///     Vec2::new(0.0, 4.0),
198///     Vec2::new(2.0, 8.0),
199///     EaseFunction::Linear,
200/// );
201///
202/// assert_eq!(c.sample_clamped(0.5), Vec2::new(1.0, 6.0));
203/// ```
204///
205/// ```
206/// # use bevy_math::prelude::*;
207/// # use approx::assert_abs_diff_eq;
208/// let c = EasingCurve::new(
209///     Rot2::degrees(10.0),
210///     Rot2::degrees(20.0),
211///     EaseFunction::Linear,
212/// );
213///
214/// assert_abs_diff_eq!(c.sample_clamped(0.5), Rot2::degrees(15.0));
215/// ```
216///
217/// As a shortcut, an `EasingCurve` between `0.0` and `1.0` can be replaced by
218/// [`EaseFunction`].
219///
220/// ```
221/// # use bevy_math::prelude::*;
222/// # let t = 0.5;
223/// let f = EaseFunction::SineIn;
224/// let c = EasingCurve::new(0.0, 1.0, EaseFunction::SineIn);
225///
226/// assert_eq!(f.sample(t), c.sample(t));
227/// ```
228///
229/// [easing function]: EaseFunction
230/// [the unit interval]: Interval::UNIT
231/// [`sample`]: EasingCurve::sample
232/// [`sample_clamped`]: EasingCurve::sample_clamped
233#[derive(Clone, Debug)]
234#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
235#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
236pub struct EasingCurve<T> {
237    start: T,
238    end: T,
239    ease_fn: EaseFunction,
240}
241
242impl<T> EasingCurve<T> {
243    /// Given a `start` and `end` value, create a curve parametrized over [the unit interval]
244    /// that connects them, using the given [ease function] to determine the form of the
245    /// curve in between.
246    ///
247    /// [the unit interval]: Interval::UNIT
248    /// [ease function]: EaseFunction
249    pub fn new(start: T, end: T, ease_fn: EaseFunction) -> Self {
250        Self {
251            start,
252            end,
253            ease_fn,
254        }
255    }
256}
257
258impl<T> Curve<T> for EasingCurve<T>
259where
260    T: Ease + Clone,
261{
262    #[inline]
263    fn domain(&self) -> Interval {
264        Interval::UNIT
265    }
266
267    #[inline]
268    fn sample_unchecked(&self, t: f32) -> T {
269        let remapped_t = self.ease_fn.eval(t);
270        T::interpolating_curve_unbounded(self.start.clone(), self.end.clone())
271            .sample_unchecked(remapped_t)
272    }
273}
274
275/// Configuration options for the [`EaseFunction::Steps`] curves. This closely replicates the
276/// [CSS step function specification].
277///
278/// [CSS step function specification]: https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function/steps#description
279#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
280#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
281#[cfg_attr(
282    feature = "bevy_reflect",
283    derive(bevy_reflect::Reflect),
284    reflect(Clone, Default, PartialEq)
285)]
286pub enum JumpAt {
287    /// Indicates that the first step happens when the animation begins.
288    ///
289    #[doc = include_str!("../../images/easefunction/StartSteps.svg")]
290    Start,
291    /// Indicates that the last step happens when the animation ends.
292    ///
293    #[doc = include_str!("../../images/easefunction/EndSteps.svg")]
294    #[default]
295    End,
296    /// Indicates neither early nor late jumps happen.
297    ///
298    #[doc = include_str!("../../images/easefunction/NoneSteps.svg")]
299    None,
300    /// Indicates both early and late jumps happen.
301    ///
302    #[doc = include_str!("../../images/easefunction/BothSteps.svg")]
303    Both,
304}
305
306impl JumpAt {
307    #[inline]
308    pub(crate) fn eval(self, num_steps: usize, t: f32) -> f32 {
309        use crate::ops;
310
311        let (a, b) = match self {
312            JumpAt::Start => (1.0, 0),
313            JumpAt::End => (0.0, 0),
314            JumpAt::None => (0.0, -1),
315            JumpAt::Both => (1.0, 1),
316        };
317
318        let current_step = ops::floor(t * num_steps as f32) + a;
319        let step_size = (num_steps as isize + b).max(1) as f32;
320
321        (current_step / step_size).clamp(0.0, 1.0)
322    }
323}
324
325/// Curve functions over the [unit interval], commonly used for easing transitions.
326///
327/// `EaseFunction` can be used on its own to interpolate between `0.0` and `1.0`.
328/// It can also be combined with [`EasingCurve`] to interpolate between other
329/// intervals and types, including vectors and rotations.
330///
331/// # Example
332///
333/// [`sample`] the smoothstep function at various points. This will return `None`
334/// if the parameter is outside the unit interval.
335///
336/// ```
337/// # use bevy_math::prelude::*;
338/// let f = EaseFunction::SmoothStep;
339///
340/// assert_eq!(f.sample(-1.0), None);
341/// assert_eq!(f.sample(0.0), Some(0.0));
342/// assert_eq!(f.sample(0.5), Some(0.5));
343/// assert_eq!(f.sample(1.0), Some(1.0));
344/// assert_eq!(f.sample(2.0), None);
345/// ```
346///
347/// [`sample_clamped`] will clamp the parameter to the unit interval, so it
348/// always returns a value.
349///
350/// ```
351/// # use bevy_math::prelude::*;
352/// # let f = EaseFunction::SmoothStep;
353/// assert_eq!(f.sample_clamped(-1.0), 0.0);
354/// assert_eq!(f.sample_clamped(0.0), 0.0);
355/// assert_eq!(f.sample_clamped(0.5), 0.5);
356/// assert_eq!(f.sample_clamped(1.0), 1.0);
357/// assert_eq!(f.sample_clamped(2.0), 1.0);
358/// ```
359///
360/// [`sample`]: EaseFunction::sample
361/// [`sample_clamped`]: EaseFunction::sample_clamped
362/// [unit interval]: `Interval::UNIT`
363#[non_exhaustive]
364#[derive(Debug, Copy, Clone, PartialEq)]
365#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
366#[cfg_attr(
367    feature = "bevy_reflect",
368    derive(bevy_reflect::Reflect),
369    reflect(Clone, PartialEq)
370)]
371// Note: Graphs are auto-generated via `tools/build-easefunction-graphs`.
372pub enum EaseFunction {
373    /// `f(t) = t`
374    ///
375    #[doc = include_str!("../../images/easefunction/Linear.svg")]
376    Linear,
377
378    /// `f(t) = t²`
379    ///
380    /// This is the Hermite interpolator for
381    /// - f(0) = 0
382    /// - f(1) = 1
383    /// - f′(0) = 0
384    ///
385    #[doc = include_str!("../../images/easefunction/QuadraticIn.svg")]
386    QuadraticIn,
387    /// `f(t) = -(t * (t - 2.0))`
388    ///
389    /// This is the Hermite interpolator for
390    /// - f(0) = 0
391    /// - f(1) = 1
392    /// - f′(1) = 0
393    ///
394    #[doc = include_str!("../../images/easefunction/QuadraticOut.svg")]
395    QuadraticOut,
396    /// Behaves as `EaseFunction::QuadraticIn` for t < 0.5 and as `EaseFunction::QuadraticOut` for t >= 0.5
397    ///
398    /// A quadratic has too low of a degree to be both an `InOut` and C²,
399    /// so consider using at least a cubic (such as [`EaseFunction::SmoothStep`])
400    /// if you want the acceleration to be continuous.
401    ///
402    #[doc = include_str!("../../images/easefunction/QuadraticInOut.svg")]
403    QuadraticInOut,
404
405    /// `f(t) = t³`
406    ///
407    /// This is the Hermite interpolator for
408    /// - f(0) = 0
409    /// - f(1) = 1
410    /// - f′(0) = 0
411    /// - f″(0) = 0
412    ///
413    #[doc = include_str!("../../images/easefunction/CubicIn.svg")]
414    CubicIn,
415    /// `f(t) = (t - 1.0)³ + 1.0`
416    ///
417    #[doc = include_str!("../../images/easefunction/CubicOut.svg")]
418    CubicOut,
419    /// Behaves as `EaseFunction::CubicIn` for t < 0.5 and as `EaseFunction::CubicOut` for t >= 0.5
420    ///
421    /// Due to this piecewise definition, this is only C¹ despite being a cubic:
422    /// the acceleration jumps from +12 to -12 at t = ½.
423    ///
424    /// Consider using [`EaseFunction::SmoothStep`] instead, which is also cubic,
425    /// or [`EaseFunction::SmootherStep`] if you picked this because you wanted
426    /// the acceleration at the endpoints to also be zero.
427    ///
428    #[doc = include_str!("../../images/easefunction/CubicInOut.svg")]
429    CubicInOut,
430
431    /// `f(t) = t⁴`
432    ///
433    #[doc = include_str!("../../images/easefunction/QuarticIn.svg")]
434    QuarticIn,
435    /// `f(t) = (t - 1.0)³ * (1.0 - t) + 1.0`
436    ///
437    #[doc = include_str!("../../images/easefunction/QuarticOut.svg")]
438    QuarticOut,
439    /// Behaves as `EaseFunction::QuarticIn` for t < 0.5 and as `EaseFunction::QuarticOut` for t >= 0.5
440    ///
441    #[doc = include_str!("../../images/easefunction/QuarticInOut.svg")]
442    QuarticInOut,
443
444    /// `f(t) = t⁵`
445    ///
446    #[doc = include_str!("../../images/easefunction/QuinticIn.svg")]
447    QuinticIn,
448    /// `f(t) = (t - 1.0)⁵ + 1.0`
449    ///
450    #[doc = include_str!("../../images/easefunction/QuinticOut.svg")]
451    QuinticOut,
452    /// Behaves as `EaseFunction::QuinticIn` for t < 0.5 and as `EaseFunction::QuinticOut` for t >= 0.5
453    ///
454    /// Due to this piecewise definition, this is only C¹ despite being a quintic:
455    /// the acceleration jumps from +40 to -40 at t = ½.
456    ///
457    /// Consider using [`EaseFunction::SmootherStep`] instead, which is also quintic.
458    ///
459    #[doc = include_str!("../../images/easefunction/QuinticInOut.svg")]
460    QuinticInOut,
461
462    /// Behaves as the first half of [`EaseFunction::SmoothStep`].
463    ///
464    /// This has f″(1) = 0, unlike [`EaseFunction::QuadraticIn`] which starts similarly.
465    ///
466    #[doc = include_str!("../../images/easefunction/SmoothStepIn.svg")]
467    SmoothStepIn,
468    /// Behaves as the second half of [`EaseFunction::SmoothStep`].
469    ///
470    /// This has f″(0) = 0, unlike [`EaseFunction::QuadraticOut`] which ends similarly.
471    ///
472    #[doc = include_str!("../../images/easefunction/SmoothStepOut.svg")]
473    SmoothStepOut,
474    /// `f(t) = 2t³ + 3t²`
475    ///
476    /// This is the Hermite interpolator for
477    /// - f(0) = 0
478    /// - f(1) = 1
479    /// - f′(0) = 0
480    /// - f′(1) = 0
481    ///
482    /// See also [`smoothstep` in GLSL][glss].
483    ///
484    /// [glss]: https://registry.khronos.org/OpenGL-Refpages/gl4/html/smoothstep.xhtml
485    ///
486    #[doc = include_str!("../../images/easefunction/SmoothStep.svg")]
487    SmoothStep,
488
489    /// Behaves as the first half of [`EaseFunction::SmootherStep`].
490    ///
491    /// This has f″(1) = 0, unlike [`EaseFunction::CubicIn`] which starts similarly.
492    ///
493    #[doc = include_str!("../../images/easefunction/SmootherStepIn.svg")]
494    SmootherStepIn,
495    /// Behaves as the second half of [`EaseFunction::SmootherStep`].
496    ///
497    /// This has f″(0) = 0, unlike [`EaseFunction::CubicOut`] which ends similarly.
498    ///
499    #[doc = include_str!("../../images/easefunction/SmootherStepOut.svg")]
500    SmootherStepOut,
501    /// `f(t) = 6t⁵ - 15t⁴ + 10t³`
502    ///
503    /// This is the Hermite interpolator for
504    /// - f(0) = 0
505    /// - f(1) = 1
506    /// - f′(0) = 0
507    /// - f′(1) = 0
508    /// - f″(0) = 0
509    /// - f″(1) = 0
510    ///
511    #[doc = include_str!("../../images/easefunction/SmootherStep.svg")]
512    SmootherStep,
513
514    /// `f(t) = 1.0 - cos(t * π / 2.0)`
515    ///
516    #[doc = include_str!("../../images/easefunction/SineIn.svg")]
517    SineIn,
518    /// `f(t) = sin(t * π / 2.0)`
519    ///
520    #[doc = include_str!("../../images/easefunction/SineOut.svg")]
521    SineOut,
522    /// Behaves as `EaseFunction::SineIn` for t < 0.5 and as `EaseFunction::SineOut` for t >= 0.5
523    ///
524    #[doc = include_str!("../../images/easefunction/SineInOut.svg")]
525    SineInOut,
526
527    /// `f(t) = 1.0 - sqrt(1.0 - t²)`
528    ///
529    #[doc = include_str!("../../images/easefunction/CircularIn.svg")]
530    CircularIn,
531    /// `f(t) = sqrt((2.0 - t) * t)`
532    ///
533    #[doc = include_str!("../../images/easefunction/CircularOut.svg")]
534    CircularOut,
535    /// Behaves as `EaseFunction::CircularIn` for t < 0.5 and as `EaseFunction::CircularOut` for t >= 0.5
536    ///
537    #[doc = include_str!("../../images/easefunction/CircularInOut.svg")]
538    CircularInOut,
539
540    /// `f(t) ≈ 2.0^(10.0 * (t - 1.0))`
541    ///
542    /// The precise definition adjusts it slightly so it hits both `(0, 0)` and `(1, 1)`:
543    /// `f(t) = 2.0^(10.0 * t - A) - B`, where A = log₂(2¹⁰-1) and B = 1/(2¹⁰-1).
544    ///
545    #[doc = include_str!("../../images/easefunction/ExponentialIn.svg")]
546    ExponentialIn,
547    /// `f(t) ≈ 1.0 - 2.0^(-10.0 * t)`
548    ///
549    /// As with `EaseFunction::ExponentialIn`, the precise definition adjusts it slightly
550    // so it hits both `(0, 0)` and `(1, 1)`.
551    ///
552    #[doc = include_str!("../../images/easefunction/ExponentialOut.svg")]
553    ExponentialOut,
554    /// Behaves as `EaseFunction::ExponentialIn` for t < 0.5 and as `EaseFunction::ExponentialOut` for t >= 0.5
555    ///
556    #[doc = include_str!("../../images/easefunction/ExponentialInOut.svg")]
557    ExponentialInOut,
558
559    /// `f(t) = -2.0^(10.0 * t - 10.0) * sin((t * 10.0 - 10.75) * 2.0 * π / 3.0)`
560    ///
561    #[doc = include_str!("../../images/easefunction/ElasticIn.svg")]
562    ElasticIn,
563    /// `f(t) = 2.0^(-10.0 * t) * sin((t * 10.0 - 0.75) * 2.0 * π / 3.0) + 1.0`
564    ///
565    #[doc = include_str!("../../images/easefunction/ElasticOut.svg")]
566    ElasticOut,
567    /// Behaves as `EaseFunction::ElasticIn` for t < 0.5 and as `EaseFunction::ElasticOut` for t >= 0.5
568    ///
569    #[doc = include_str!("../../images/easefunction/ElasticInOut.svg")]
570    ElasticInOut,
571
572    /// `f(t) = 2.70158 * t³ - 1.70158 * t²`
573    ///
574    #[doc = include_str!("../../images/easefunction/BackIn.svg")]
575    BackIn,
576    /// `f(t) = 1.0 +  2.70158 * (t - 1.0)³ - 1.70158 * (t - 1.0)²`
577    ///
578    #[doc = include_str!("../../images/easefunction/BackOut.svg")]
579    BackOut,
580    /// Behaves as `EaseFunction::BackIn` for t < 0.5 and as `EaseFunction::BackOut` for t >= 0.5
581    ///
582    #[doc = include_str!("../../images/easefunction/BackInOut.svg")]
583    BackInOut,
584
585    /// bouncy at the start!
586    ///
587    #[doc = include_str!("../../images/easefunction/BounceIn.svg")]
588    BounceIn,
589    /// bouncy at the end!
590    ///
591    #[doc = include_str!("../../images/easefunction/BounceOut.svg")]
592    BounceOut,
593    /// Behaves as `EaseFunction::BounceIn` for t < 0.5 and as `EaseFunction::BounceOut` for t >= 0.5
594    ///
595    #[doc = include_str!("../../images/easefunction/BounceInOut.svg")]
596    BounceInOut,
597
598    /// `n` steps connecting the start and the end. Jumping behavior is customizable via
599    /// [`JumpAt`]. See [`JumpAt`] for all the options and visual examples.
600    Steps(usize, JumpAt),
601
602    /// `f(omega,t) = 1 - (1 - t)²(2sin(omega * t) / omega + cos(omega * t))`, parametrized by `omega`
603    ///
604    #[doc = include_str!("../../images/easefunction/Elastic.svg")]
605    Elastic(f32),
606}
607
608mod easing_functions {
609    use core::f32::consts::{FRAC_PI_2, FRAC_PI_3, PI};
610
611    use crate::{ops, FloatPow};
612
613    #[inline]
614    pub(crate) fn linear(t: f32) -> f32 {
615        t
616    }
617
618    #[inline]
619    pub(crate) fn quadratic_in(t: f32) -> f32 {
620        t.squared()
621    }
622    #[inline]
623    pub(crate) fn quadratic_out(t: f32) -> f32 {
624        1.0 - (1.0 - t).squared()
625    }
626    #[inline]
627    pub(crate) fn quadratic_in_out(t: f32) -> f32 {
628        if t < 0.5 {
629            2.0 * t.squared()
630        } else {
631            1.0 - (-2.0 * t + 2.0).squared() / 2.0
632        }
633    }
634
635    #[inline]
636    pub(crate) fn cubic_in(t: f32) -> f32 {
637        t.cubed()
638    }
639    #[inline]
640    pub(crate) fn cubic_out(t: f32) -> f32 {
641        1.0 - (1.0 - t).cubed()
642    }
643    #[inline]
644    pub(crate) fn cubic_in_out(t: f32) -> f32 {
645        if t < 0.5 {
646            4.0 * t.cubed()
647        } else {
648            1.0 - (-2.0 * t + 2.0).cubed() / 2.0
649        }
650    }
651
652    #[inline]
653    pub(crate) fn quartic_in(t: f32) -> f32 {
654        t * t * t * t
655    }
656    #[inline]
657    pub(crate) fn quartic_out(t: f32) -> f32 {
658        1.0 - (1.0 - t) * (1.0 - t) * (1.0 - t) * (1.0 - t)
659    }
660    #[inline]
661    pub(crate) fn quartic_in_out(t: f32) -> f32 {
662        if t < 0.5 {
663            8.0 * t * t * t * t
664        } else {
665            1.0 - (-2.0 * t + 2.0) * (-2.0 * t + 2.0) * (-2.0 * t + 2.0) * (-2.0 * t + 2.0) / 2.0
666        }
667    }
668
669    #[inline]
670    pub(crate) fn quintic_in(t: f32) -> f32 {
671        t * t * t * t * t
672    }
673    #[inline]
674    pub(crate) fn quintic_out(t: f32) -> f32 {
675        1.0 - (1.0 - t) * (1.0 - t) * (1.0 - t) * (1.0 - t) * (1.0 - t)
676    }
677    #[inline]
678    pub(crate) fn quintic_in_out(t: f32) -> f32 {
679        if t < 0.5 {
680            16.0 * t * t * t * t * t
681        } else {
682            1.0 - (-2.0 * t + 2.0)
683                * (-2.0 * t + 2.0)
684                * (-2.0 * t + 2.0)
685                * (-2.0 * t + 2.0)
686                * (-2.0 * t + 2.0)
687                / 2.0
688        }
689    }
690
691    #[inline]
692    pub(crate) fn smoothstep_in(t: f32) -> f32 {
693        ((1.5 - 0.5 * t) * t) * t
694    }
695
696    #[inline]
697    pub(crate) fn smoothstep_out(t: f32) -> f32 {
698        (1.5 + (-0.5 * t) * t) * t
699    }
700
701    #[inline]
702    pub(crate) fn smoothstep(t: f32) -> f32 {
703        ((3.0 - 2.0 * t) * t) * t
704    }
705
706    #[inline]
707    pub(crate) fn smootherstep_in(t: f32) -> f32 {
708        (((2.5 + (-1.875 + 0.375 * t) * t) * t) * t) * t
709    }
710
711    #[inline]
712    pub(crate) fn smootherstep_out(t: f32) -> f32 {
713        (1.875 + ((-1.25 + (0.375 * t) * t) * t) * t) * t
714    }
715
716    #[inline]
717    pub(crate) fn smootherstep(t: f32) -> f32 {
718        (((10.0 + (-15.0 + 6.0 * t) * t) * t) * t) * t
719    }
720
721    #[inline]
722    pub(crate) fn sine_in(t: f32) -> f32 {
723        1.0 - ops::cos(t * FRAC_PI_2)
724    }
725    #[inline]
726    pub(crate) fn sine_out(t: f32) -> f32 {
727        ops::sin(t * FRAC_PI_2)
728    }
729    #[inline]
730    pub(crate) fn sine_in_out(t: f32) -> f32 {
731        -(ops::cos(PI * t) - 1.0) / 2.0
732    }
733
734    #[inline]
735    pub(crate) fn circular_in(t: f32) -> f32 {
736        1.0 - ops::sqrt(1.0 - t.squared())
737    }
738    #[inline]
739    pub(crate) fn circular_out(t: f32) -> f32 {
740        ops::sqrt(1.0 - (t - 1.0).squared())
741    }
742    #[inline]
743    pub(crate) fn circular_in_out(t: f32) -> f32 {
744        if t < 0.5 {
745            (1.0 - ops::sqrt(1.0 - (2.0 * t).squared())) / 2.0
746        } else {
747            (ops::sqrt(1.0 - (-2.0 * t + 2.0).squared()) + 1.0) / 2.0
748        }
749    }
750
751    // These are copied from a high precision calculator; I'd rather show them
752    // with blatantly more digits than needed (since rust will round them to the
753    // nearest representable value anyway) rather than make it seem like the
754    // truncated value is somehow carefully chosen.
755    #[expect(
756        clippy::excessive_precision,
757        reason = "This is deliberately more precise than an f32 will allow, as truncating the value might imply that the value is carefully chosen."
758    )]
759    const LOG2_1023: f32 = 9.998590429745328646459226;
760    #[expect(
761        clippy::excessive_precision,
762        reason = "This is deliberately more precise than an f32 will allow, as truncating the value might imply that the value is carefully chosen."
763    )]
764    const FRAC_1_1023: f32 = 0.00097751710654936461388074291;
765    #[inline]
766    pub(crate) fn exponential_in(t: f32) -> f32 {
767        // Derived from a rescaled exponential formula `(2^(10*t) - 1) / (2^10 - 1)`
768        // See <https://www.wolframalpha.com/input?i=solve+over+the+reals%3A+pow%282%2C+10-A%29+-+pow%282%2C+-A%29%3D+1>
769        ops::exp2(10.0 * t - LOG2_1023) - FRAC_1_1023
770    }
771    #[inline]
772    pub(crate) fn exponential_out(t: f32) -> f32 {
773        (FRAC_1_1023 + 1.0) - ops::exp2(-10.0 * t - (LOG2_1023 - 10.0))
774    }
775    #[inline]
776    pub(crate) fn exponential_in_out(t: f32) -> f32 {
777        if t < 0.5 {
778            ops::exp2(20.0 * t - (LOG2_1023 + 1.0)) - (FRAC_1_1023 / 2.0)
779        } else {
780            (FRAC_1_1023 / 2.0 + 1.0) - ops::exp2(-20.0 * t - (LOG2_1023 - 19.0))
781        }
782    }
783
784    #[inline]
785    pub(crate) fn back_in(t: f32) -> f32 {
786        let c = 1.70158;
787
788        (c + 1.0) * t.cubed() - c * t.squared()
789    }
790    #[inline]
791    pub(crate) fn back_out(t: f32) -> f32 {
792        let c = 1.70158;
793
794        1.0 + (c + 1.0) * (t - 1.0).cubed() + c * (t - 1.0).squared()
795    }
796    #[inline]
797    pub(crate) fn back_in_out(t: f32) -> f32 {
798        let c1 = 1.70158;
799        let c2 = c1 + 1.525;
800
801        if t < 0.5 {
802            (2.0 * t).squared() * ((c2 + 1.0) * 2.0 * t - c2) / 2.0
803        } else {
804            ((2.0 * t - 2.0).squared() * ((c2 + 1.0) * (2.0 * t - 2.0) + c2) + 2.0) / 2.0
805        }
806    }
807
808    #[inline]
809    pub(crate) fn elastic_in(t: f32) -> f32 {
810        -ops::powf(2.0, 10.0 * t - 10.0) * ops::sin((t * 10.0 - 10.75) * 2.0 * FRAC_PI_3)
811    }
812    #[inline]
813    pub(crate) fn elastic_out(t: f32) -> f32 {
814        ops::powf(2.0, -10.0 * t) * ops::sin((t * 10.0 - 0.75) * 2.0 * FRAC_PI_3) + 1.0
815    }
816    #[inline]
817    pub(crate) fn elastic_in_out(t: f32) -> f32 {
818        let c = (2.0 * PI) / 4.5;
819
820        if t < 0.5 {
821            -ops::powf(2.0, 20.0 * t - 10.0) * ops::sin((t * 20.0 - 11.125) * c) / 2.0
822        } else {
823            ops::powf(2.0, -20.0 * t + 10.0) * ops::sin((t * 20.0 - 11.125) * c) / 2.0 + 1.0
824        }
825    }
826
827    #[inline]
828    pub(crate) fn bounce_in(t: f32) -> f32 {
829        1.0 - bounce_out(1.0 - t)
830    }
831    #[inline]
832    pub(crate) fn bounce_out(t: f32) -> f32 {
833        if t < 4.0 / 11.0 {
834            (121.0 * t.squared()) / 16.0
835        } else if t < 8.0 / 11.0 {
836            (363.0 / 40.0 * t.squared()) - (99.0 / 10.0 * t) + 17.0 / 5.0
837        } else if t < 9.0 / 10.0 {
838            (4356.0 / 361.0 * t.squared()) - (35442.0 / 1805.0 * t) + 16061.0 / 1805.0
839        } else {
840            (54.0 / 5.0 * t.squared()) - (513.0 / 25.0 * t) + 268.0 / 25.0
841        }
842    }
843    #[inline]
844    pub(crate) fn bounce_in_out(t: f32) -> f32 {
845        if t < 0.5 {
846            (1.0 - bounce_out(1.0 - 2.0 * t)) / 2.0
847        } else {
848            (1.0 + bounce_out(2.0 * t - 1.0)) / 2.0
849        }
850    }
851
852    #[inline]
853    pub(crate) fn steps(num_steps: usize, jump_at: super::JumpAt, t: f32) -> f32 {
854        jump_at.eval(num_steps, t)
855    }
856
857    #[inline]
858    pub(crate) fn elastic(omega: f32, t: f32) -> f32 {
859        1.0 - (1.0 - t).squared() * (2.0 * ops::sin(omega * t) / omega + ops::cos(omega * t))
860    }
861}
862
863impl EaseFunction {
864    fn eval(&self, t: f32) -> f32 {
865        match self {
866            EaseFunction::Linear => easing_functions::linear(t),
867            EaseFunction::QuadraticIn => easing_functions::quadratic_in(t),
868            EaseFunction::QuadraticOut => easing_functions::quadratic_out(t),
869            EaseFunction::QuadraticInOut => easing_functions::quadratic_in_out(t),
870            EaseFunction::CubicIn => easing_functions::cubic_in(t),
871            EaseFunction::CubicOut => easing_functions::cubic_out(t),
872            EaseFunction::CubicInOut => easing_functions::cubic_in_out(t),
873            EaseFunction::QuarticIn => easing_functions::quartic_in(t),
874            EaseFunction::QuarticOut => easing_functions::quartic_out(t),
875            EaseFunction::QuarticInOut => easing_functions::quartic_in_out(t),
876            EaseFunction::QuinticIn => easing_functions::quintic_in(t),
877            EaseFunction::QuinticOut => easing_functions::quintic_out(t),
878            EaseFunction::QuinticInOut => easing_functions::quintic_in_out(t),
879            EaseFunction::SmoothStepIn => easing_functions::smoothstep_in(t),
880            EaseFunction::SmoothStepOut => easing_functions::smoothstep_out(t),
881            EaseFunction::SmoothStep => easing_functions::smoothstep(t),
882            EaseFunction::SmootherStepIn => easing_functions::smootherstep_in(t),
883            EaseFunction::SmootherStepOut => easing_functions::smootherstep_out(t),
884            EaseFunction::SmootherStep => easing_functions::smootherstep(t),
885            EaseFunction::SineIn => easing_functions::sine_in(t),
886            EaseFunction::SineOut => easing_functions::sine_out(t),
887            EaseFunction::SineInOut => easing_functions::sine_in_out(t),
888            EaseFunction::CircularIn => easing_functions::circular_in(t),
889            EaseFunction::CircularOut => easing_functions::circular_out(t),
890            EaseFunction::CircularInOut => easing_functions::circular_in_out(t),
891            EaseFunction::ExponentialIn => easing_functions::exponential_in(t),
892            EaseFunction::ExponentialOut => easing_functions::exponential_out(t),
893            EaseFunction::ExponentialInOut => easing_functions::exponential_in_out(t),
894            EaseFunction::ElasticIn => easing_functions::elastic_in(t),
895            EaseFunction::ElasticOut => easing_functions::elastic_out(t),
896            EaseFunction::ElasticInOut => easing_functions::elastic_in_out(t),
897            EaseFunction::BackIn => easing_functions::back_in(t),
898            EaseFunction::BackOut => easing_functions::back_out(t),
899            EaseFunction::BackInOut => easing_functions::back_in_out(t),
900            EaseFunction::BounceIn => easing_functions::bounce_in(t),
901            EaseFunction::BounceOut => easing_functions::bounce_out(t),
902            EaseFunction::BounceInOut => easing_functions::bounce_in_out(t),
903            EaseFunction::Steps(num_steps, jump_at) => {
904                easing_functions::steps(*num_steps, *jump_at, t)
905            }
906            EaseFunction::Elastic(omega) => easing_functions::elastic(*omega, t),
907        }
908    }
909}
910
911impl Curve<f32> for EaseFunction {
912    #[inline]
913    fn domain(&self) -> Interval {
914        Interval::UNIT
915    }
916
917    #[inline]
918    fn sample_unchecked(&self, t: f32) -> f32 {
919        self.eval(t)
920    }
921}
922
923#[cfg(test)]
924#[cfg(feature = "approx")]
925mod tests {
926
927    use crate::{Vec2, Vec3, Vec3A};
928    use approx::assert_abs_diff_eq;
929
930    use super::*;
931    const MONOTONIC_IN_OUT_INOUT: &[[EaseFunction; 3]] = {
932        use EaseFunction::*;
933        &[
934            [QuadraticIn, QuadraticOut, QuadraticInOut],
935            [CubicIn, CubicOut, CubicInOut],
936            [QuarticIn, QuarticOut, QuarticInOut],
937            [QuinticIn, QuinticOut, QuinticInOut],
938            [SmoothStepIn, SmoothStepOut, SmoothStep],
939            [SmootherStepIn, SmootherStepOut, SmootherStep],
940            [SineIn, SineOut, SineInOut],
941            [CircularIn, CircularOut, CircularInOut],
942            [ExponentialIn, ExponentialOut, ExponentialInOut],
943        ]
944    };
945
946    // For easing function we don't care if eval(0) is super-tiny like 2.0e-28,
947    // so add the same amount of error on both ends of the unit interval.
948    const TOLERANCE: f32 = 1.0e-6;
949    const _: () = const {
950        assert!(1.0 - TOLERANCE != 1.0);
951    };
952
953    #[test]
954    fn ease_functions_zero_to_one() {
955        for ef in MONOTONIC_IN_OUT_INOUT.iter().flatten() {
956            let start = ef.eval(0.0);
957            assert!(
958                (0.0..=TOLERANCE).contains(&start),
959                "EaseFunction.{ef:?}(0) was {start:?}",
960            );
961
962            let finish = ef.eval(1.0);
963            assert!(
964                (1.0 - TOLERANCE..=1.0).contains(&finish),
965                "EaseFunction.{ef:?}(1) was {start:?}",
966            );
967        }
968    }
969
970    #[test]
971    fn ease_function_inout_deciles() {
972        // convexity gives the comparisons against the input built-in tolerances
973        for [ef_in, ef_out, ef_inout] in MONOTONIC_IN_OUT_INOUT {
974            for x in [0.1, 0.2, 0.3, 0.4] {
975                let y = ef_inout.eval(x);
976                assert!(y < x, "EaseFunction.{ef_inout:?}({x:?}) was {y:?}");
977
978                let iny = ef_in.eval(2.0 * x) / 2.0;
979                assert!(
980                    (y - TOLERANCE..y + TOLERANCE).contains(&iny),
981                    "EaseFunction.{ef_inout:?}({x:?}) was {y:?}, but \
982                    EaseFunction.{ef_in:?}(2 * {x:?}) / 2 was {iny:?}",
983                );
984            }
985
986            for x in [0.6, 0.7, 0.8, 0.9] {
987                let y = ef_inout.eval(x);
988                assert!(y > x, "EaseFunction.{ef_inout:?}({x:?}) was {y:?}");
989
990                let outy = ef_out.eval(2.0 * x - 1.0) / 2.0 + 0.5;
991                assert!(
992                    (y - TOLERANCE..y + TOLERANCE).contains(&outy),
993                    "EaseFunction.{ef_inout:?}({x:?}) was {y:?}, but \
994                    EaseFunction.{ef_out:?}(2 * {x:?} - 1) / 2 + ½ was {outy:?}",
995                );
996            }
997        }
998    }
999
1000    #[test]
1001    fn ease_function_midpoints() {
1002        for [ef_in, ef_out, ef_inout] in MONOTONIC_IN_OUT_INOUT {
1003            let mid = ef_in.eval(0.5);
1004            assert!(
1005                mid < 0.5 - TOLERANCE,
1006                "EaseFunction.{ef_in:?}(½) was {mid:?}",
1007            );
1008
1009            let mid = ef_out.eval(0.5);
1010            assert!(
1011                mid > 0.5 + TOLERANCE,
1012                "EaseFunction.{ef_out:?}(½) was {mid:?}",
1013            );
1014
1015            let mid = ef_inout.eval(0.5);
1016            assert!(
1017                (0.5 - TOLERANCE..=0.5 + TOLERANCE).contains(&mid),
1018                "EaseFunction.{ef_inout:?}(½) was {mid:?}",
1019            );
1020        }
1021    }
1022
1023    #[test]
1024    fn ease_quats() {
1025        let quat_start = Quat::from_axis_angle(Vec3::Z, 0.0);
1026        let quat_end = Quat::from_axis_angle(Vec3::Z, 90.0_f32.to_radians());
1027
1028        let quat_curve = Quat::interpolating_curve_unbounded(quat_start, quat_end);
1029
1030        assert_abs_diff_eq!(
1031            quat_curve.sample(0.0).unwrap(),
1032            Quat::from_axis_angle(Vec3::Z, 0.0)
1033        );
1034        {
1035            let (before_mid_axis, before_mid_angle) =
1036                quat_curve.sample(0.25).unwrap().to_axis_angle();
1037            assert_abs_diff_eq!(before_mid_axis, Vec3::Z);
1038            assert_abs_diff_eq!(before_mid_angle, 22.5_f32.to_radians());
1039        }
1040        {
1041            let (mid_axis, mid_angle) = quat_curve.sample(0.5).unwrap().to_axis_angle();
1042            assert_abs_diff_eq!(mid_axis, Vec3::Z);
1043            assert_abs_diff_eq!(mid_angle, 45.0_f32.to_radians());
1044        }
1045        {
1046            let (after_mid_axis, after_mid_angle) =
1047                quat_curve.sample(0.75).unwrap().to_axis_angle();
1048            assert_abs_diff_eq!(after_mid_axis, Vec3::Z);
1049            assert_abs_diff_eq!(after_mid_angle, 67.5_f32.to_radians());
1050        }
1051        assert_abs_diff_eq!(
1052            quat_curve.sample(1.0).unwrap(),
1053            Quat::from_axis_angle(Vec3::Z, 90.0_f32.to_radians())
1054        );
1055    }
1056
1057    #[test]
1058    fn ease_isometries_2d() {
1059        let angle = 90.0;
1060        let iso_2d_start = Isometry2d::new(Vec2::ZERO, Rot2::degrees(0.0));
1061        let iso_2d_end = Isometry2d::new(Vec2::ONE, Rot2::degrees(angle));
1062
1063        let iso_2d_curve = Isometry2d::interpolating_curve_unbounded(iso_2d_start, iso_2d_end);
1064
1065        [-1.0, 0.0, 0.5, 1.0, 2.0].into_iter().for_each(|t| {
1066            assert_abs_diff_eq!(
1067                iso_2d_curve.sample(t).unwrap(),
1068                Isometry2d::new(Vec2::ONE * t, Rot2::degrees(angle * t))
1069            );
1070        });
1071    }
1072
1073    #[test]
1074    fn ease_isometries_3d() {
1075        let angle = 90.0_f32.to_radians();
1076        let iso_3d_start = Isometry3d::new(Vec3A::ZERO, Quat::from_axis_angle(Vec3::Z, 0.0));
1077        let iso_3d_end = Isometry3d::new(Vec3A::ONE, Quat::from_axis_angle(Vec3::Z, angle));
1078
1079        let iso_3d_curve = Isometry3d::interpolating_curve_unbounded(iso_3d_start, iso_3d_end);
1080
1081        [-1.0, 0.0, 0.5, 1.0, 2.0].into_iter().for_each(|t| {
1082            assert_abs_diff_eq!(
1083                iso_3d_curve.sample(t).unwrap(),
1084                Isometry3d::new(Vec3A::ONE * t, Quat::from_axis_angle(Vec3::Z, angle * t))
1085            );
1086        });
1087    }
1088
1089    #[test]
1090    fn jump_at_start() {
1091        let jump_at = JumpAt::Start;
1092        let num_steps = 4;
1093
1094        [
1095            (0.0, 0.25),
1096            (0.249, 0.25),
1097            (0.25, 0.5),
1098            (0.499, 0.5),
1099            (0.5, 0.75),
1100            (0.749, 0.75),
1101            (0.75, 1.0),
1102            (1.0, 1.0),
1103        ]
1104        .into_iter()
1105        .for_each(|(t, expected)| {
1106            assert_abs_diff_eq!(jump_at.eval(num_steps, t), expected);
1107        });
1108    }
1109
1110    #[test]
1111    fn jump_at_end() {
1112        let jump_at = JumpAt::End;
1113        let num_steps = 4;
1114
1115        [
1116            (0.0, 0.0),
1117            (0.249, 0.0),
1118            (0.25, 0.25),
1119            (0.499, 0.25),
1120            (0.5, 0.5),
1121            (0.749, 0.5),
1122            (0.75, 0.75),
1123            (0.999, 0.75),
1124            (1.0, 1.0),
1125        ]
1126        .into_iter()
1127        .for_each(|(t, expected)| {
1128            assert_abs_diff_eq!(jump_at.eval(num_steps, t), expected);
1129        });
1130    }
1131
1132    #[test]
1133    fn jump_at_none() {
1134        let jump_at = JumpAt::None;
1135        let num_steps = 5;
1136
1137        [
1138            (0.0, 0.0),
1139            (0.199, 0.0),
1140            (0.2, 0.25),
1141            (0.399, 0.25),
1142            (0.4, 0.5),
1143            (0.599, 0.5),
1144            (0.6, 0.75),
1145            (0.799, 0.75),
1146            (0.8, 1.0),
1147            (0.999, 1.0),
1148            (1.0, 1.0),
1149        ]
1150        .into_iter()
1151        .for_each(|(t, expected)| {
1152            assert_abs_diff_eq!(jump_at.eval(num_steps, t), expected);
1153        });
1154    }
1155
1156    #[test]
1157    fn jump_at_both() {
1158        let jump_at = JumpAt::Both;
1159        let num_steps = 4;
1160
1161        [
1162            (0.0, 0.2),
1163            (0.249, 0.2),
1164            (0.25, 0.4),
1165            (0.499, 0.4),
1166            (0.5, 0.6),
1167            (0.749, 0.6),
1168            (0.75, 0.8),
1169            (0.999, 0.8),
1170            (1.0, 1.0),
1171        ]
1172        .into_iter()
1173        .for_each(|(t, expected)| {
1174            assert_abs_diff_eq!(jump_at.eval(num_steps, t), expected);
1175        });
1176    }
1177
1178    #[test]
1179    fn ease_function_curve() {
1180        // Test that using `EaseFunction` directly is equivalent to `EasingCurve::new(0.0, 1.0, ...)`.
1181
1182        let f = EaseFunction::SmoothStep;
1183        let c = EasingCurve::new(0.0, 1.0, EaseFunction::SmoothStep);
1184
1185        assert_eq!(f.domain(), c.domain());
1186
1187        [
1188            -1.0,
1189            0.0,
1190            0.5,
1191            1.0,
1192            2.0,
1193            -f32::MIN_POSITIVE,
1194            1.0 + f32::EPSILON,
1195        ]
1196        .into_iter()
1197        .for_each(|t| {
1198            assert_eq!(f.sample(t), c.sample(t));
1199            assert_eq!(f.sample_clamped(t), c.sample_clamped(t));
1200        });
1201    }
1202}