Skip to main content

motion_core/animation/
easing.rs

1//! Double-precision normalized easing curves and their deterministic math.
2
3use std::{fmt, sync::Arc};
4
5#[cfg(test)]
6mod tests;
7
8/// A normalized double-precision easing curve, independent of clip duration.
9///
10/// Finite input is clamped to the unit interval with exact zero/one endpoints.
11/// Output may overshoot. Parameterized curves are built with the validated
12/// constructors such as [`Self::back_in`] and [`Self::cubic_bezier`], so sampling
13/// never rechecks parameters; [`Self::try_apply`] reports nonfinite progress and output.
14///
15/// ```
16/// use motion_core::prelude::*;
17/// use motion_core::{
18///     DisplayList, RasterLimits, ResourceStore, ValidatedDisplayList, MAX_PATH_VERBS,
19///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
20/// };
21/// let movement = Tween::new(0.0_f64, 100.0, 2.seconds()).easing(Easing::CubicOut);
22/// assert_eq!(movement.sample(1.seconds().after_origin()), 87.5);
23/// ```
24#[derive(Clone)]
25pub enum Easing {
26    /// Constant speed.
27    Linear,
28    /// Sinusoidal acceleration.
29    SineIn,
30    /// Sinusoidal deceleration.
31    SineOut,
32    /// Symmetric sinusoidal movement.
33    SineInOut,
34    /// Quadratic acceleration.
35    QuadraticIn,
36    /// Quadratic deceleration.
37    QuadraticOut,
38    /// Symmetric quadratic movement.
39    QuadraticInOut,
40    /// Cubic acceleration.
41    CubicIn,
42    /// Cubic deceleration.
43    CubicOut,
44    /// Symmetric cubic movement.
45    CubicInOut,
46    /// Quartic acceleration.
47    QuarticIn,
48    /// Quartic deceleration.
49    QuarticOut,
50    /// Symmetric quartic movement.
51    QuarticInOut,
52    /// Base-two exponential acceleration, pinned at zero.
53    ExponentialIn,
54    /// Base-two exponential deceleration, pinned at one.
55    ExponentialOut,
56    /// Symmetric exponential movement.
57    ExponentialInOut,
58    /// Initial backward movement before acceleration; built by [`Easing::back_in`].
59    #[non_exhaustive]
60    BackIn {
61        /// Finite, nonnegative backward coefficient.
62        overshoot: f64,
63    },
64    /// Overshoots the target before settling; built by [`Easing::back_out`].
65    #[non_exhaustive]
66    BackOut {
67        /// Finite, nonnegative overshoot coefficient.
68        overshoot: f64,
69    },
70    /// Mirrored BackIn, using the coefficient directly without an extra multiplier;
71    /// built by [`Easing::back_in_out`].
72    #[non_exhaustive]
73    BackInOut {
74        /// Finite, nonnegative coefficient for each half.
75        overshoot: f64,
76    },
77    /// Exponentially damped oscillation, pinned at both endpoints; built by [`Easing::elastic_out`].
78    #[non_exhaustive]
79    ElasticOut {
80        /// Finite amplitude, at least one.
81        amplitude: f64,
82        /// Finite, strictly positive period in normalized time.
83        period: f64,
84    },
85    /// A piecewise quadratic outward bounce.
86    BounceOut,
87    /// Right-continuous equal steps; built by [`Easing::steps`], which requires a nonzero count.
88    #[non_exhaustive]
89    Steps(u32),
90    /// Unit-endpoint cubic Bezier, solving X before evaluating Y; built by [`Easing::cubic_bezier`].
91    #[non_exhaustive]
92    CubicBezier {
93        /// First X control, finite and in 0..=1.
94        x1: f64,
95        /// First Y control, finite and allowed to overshoot.
96        y1: f64,
97        /// Second X control, finite and in 0..=1.
98        x2: f64,
99        /// Second Y control, finite and allowed to overshoot.
100        y2: f64,
101    },
102    /// A deterministic, thread-safe mapping with finite output.
103    /// Input is clamped to 0..=1; callback endpoint values are preserved.
104    Custom(Arc<dyn Fn(f64) -> f64 + Send + Sync>),
105}
106
107impl Easing {
108    /// Builds [`Self::BackIn`], rejecting a nonfinite or negative overshoot.
109    pub fn back_in(overshoot: f64) -> Result<Self, crate::ValidationError> {
110        check_overshoot(overshoot).map(|overshoot| Self::BackIn { overshoot })
111    }
112
113    /// Builds [`Self::BackOut`], rejecting a nonfinite or negative overshoot.
114    pub fn back_out(overshoot: f64) -> Result<Self, crate::ValidationError> {
115        check_overshoot(overshoot).map(|overshoot| Self::BackOut { overshoot })
116    }
117
118    /// Builds [`Self::BackInOut`], rejecting a nonfinite or negative overshoot.
119    pub fn back_in_out(overshoot: f64) -> Result<Self, crate::ValidationError> {
120        check_overshoot(overshoot).map(|overshoot| Self::BackInOut { overshoot })
121    }
122
123    /// Builds [`Self::ElasticOut`], rejecting an amplitude below one or a nonpositive period.
124    pub fn elastic_out(amplitude: f64, period: f64) -> Result<Self, crate::ValidationError> {
125        if !amplitude.is_finite() || amplitude < 1.0 || !period.is_finite() || period <= 0.0 {
126            return Err(easing_error(
127                "elastic",
128                "elastic amplitude must be finite and at least one; period must be finite and positive",
129            ));
130        }
131        Ok(Self::ElasticOut { amplitude, period })
132    }
133
134    /// Builds [`Self::Steps`], rejecting a zero count.
135    pub fn steps(count: u32) -> Result<Self, crate::ValidationError> {
136        if count == 0 {
137            return Err(easing_error("steps", "easing steps must be nonzero"));
138        }
139        Ok(Self::Steps(count))
140    }
141
142    /// Builds [`Self::CubicBezier`], rejecting nonfinite controls and X controls outside `0..=1`.
143    pub fn cubic_bezier(
144        x1: f64,
145        y1: f64,
146        x2: f64,
147        y2: f64,
148    ) -> Result<Self, crate::ValidationError> {
149        if ![x1, y1, x2, y2].iter().all(|value| value.is_finite())
150            || !(0.0..=1.0).contains(&x1)
151            || !(0.0..=1.0).contains(&x2)
152        {
153            return Err(easing_error(
154                "controls",
155                "Bezier controls must be finite with X in 0..=1",
156            ));
157        }
158        Ok(Self::CubicBezier { x1, y1, x2, y2 })
159    }
160
161    /// Evaluates normalized progress with double-precision math.
162    /// Built-in transcendental operations use libm, not platform math routines.
163    ///
164    /// Infinite progress clamps to the endpoints and NaN produces NaN; a custom
165    /// callback's output is returned as given. Use [`Self::try_apply`] to
166    /// reject nonfinite values.
167    pub fn apply(&self, progress: f64) -> f64 {
168        let progress = progress.clamp(0.0, 1.0);
169        if progress == 0.0 && !matches!(self, Self::Custom(_)) {
170            return 0.0;
171        }
172        if progress == 1.0 && !matches!(self, Self::Custom(_)) {
173            return 1.0;
174        }
175        match self {
176            Self::Linear => progress,
177            Self::SineIn => {
178                let sine = libm::sin(progress * std::f64::consts::FRAC_PI_4);
179                2.0 * sine * sine
180            }
181            Self::SineOut => libm::sin(progress * std::f64::consts::FRAC_PI_2),
182            Self::SineInOut => {
183                if progress == 0.5 {
184                    return 0.5;
185                }
186                let sine = libm::sin(progress * std::f64::consts::FRAC_PI_2);
187                sine * sine
188            }
189            Self::QuadraticIn => progress * progress,
190            Self::QuadraticOut => progress * (2.0 - progress),
191            Self::QuadraticInOut => mirrored(progress, |value| value * value),
192            Self::CubicIn => progress * progress * progress,
193            Self::CubicOut => progress * (3.0 + progress * (progress - 3.0)),
194            Self::CubicInOut => mirrored(progress, |value| value * value * value),
195            Self::QuarticIn => progress * progress * progress * progress,
196            Self::QuarticOut => progress * (4.0 + progress * (-6.0 + progress * (4.0 - progress))),
197            Self::QuarticInOut => mirrored(progress, |value| value * value * value * value),
198            Self::ExponentialIn => libm::exp2(10.0 * (progress - 1.0)),
199            Self::ExponentialOut => -libm::expm1(-10.0 * std::f64::consts::LN_2 * progress),
200            Self::ExponentialInOut => mirrored(progress, |value| libm::exp2(10.0 * (value - 1.0))),
201            Self::BackIn { overshoot } => back_in(progress, *overshoot),
202            Self::BackOut { overshoot } => 1.0 - back_in(1.0 - progress, *overshoot),
203            Self::BackInOut { overshoot } => mirrored(progress, |value| back_in(value, *overshoot)),
204            Self::ElasticOut { amplitude, period } => {
205                let phase = libm::asin(1.0 / amplitude) / std::f64::consts::TAU;
206                let cycles = libm::fmod(progress, *period) / period;
207                1.0 + amplitude
208                    * libm::exp2(-10.0 * progress)
209                    * libm::sin(std::f64::consts::TAU * (cycles - phase))
210            }
211            Self::BounceOut => bounce_out(progress),
212            Self::Steps(count) => libm::floor(progress * f64::from(*count)) / f64::from(*count),
213            Self::CubicBezier { x1, y1, x2, y2 } => cubic_bezier(progress, *x1, *y1, *x2, *y2),
214            Self::Custom(function) => function(progress),
215        }
216    }
217
218    /// Evaluates progress, reporting nonfinite progress or output.
219    /// Panics raised inside a user callback still propagate unchanged.
220    pub fn try_apply(&self, progress: f64) -> Result<f64, crate::ValidationError> {
221        if !progress.is_finite() {
222            return Err(easing_error("progress", "easing progress must be finite"));
223        }
224        let value = self.apply(progress);
225        if !value.is_finite() {
226            return Err(easing_error("output", "easing output must be finite"));
227        }
228        Ok(value)
229    }
230
231    /// Converts an inward curve to its outward counterpart: `1 - f(1 - t)`.
232    pub fn reversed(self) -> Self {
233        Self::Custom(Arc::new(move |progress| 1.0 - self.apply(1.0 - progress)))
234    }
235
236    /// Uses the curve for acceleration and its reversal for deceleration.
237    pub fn mirrored(self) -> Self {
238        Self::Custom(Arc::new(move |progress| {
239            mirrored(progress, |value| self.apply(value))
240        }))
241    }
242}
243
244impl fmt::Debug for Easing {
245    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
246        match self {
247            Self::Linear => formatter.write_str("Linear"),
248            Self::SineIn => formatter.write_str("SineIn"),
249            Self::SineOut => formatter.write_str("SineOut"),
250            Self::SineInOut => formatter.write_str("SineInOut"),
251            Self::QuadraticIn => formatter.write_str("QuadraticIn"),
252            Self::QuadraticOut => formatter.write_str("QuadraticOut"),
253            Self::QuadraticInOut => formatter.write_str("QuadraticInOut"),
254            Self::CubicIn => formatter.write_str("CubicIn"),
255            Self::CubicOut => formatter.write_str("CubicOut"),
256            Self::CubicInOut => formatter.write_str("CubicInOut"),
257            Self::QuarticIn => formatter.write_str("QuarticIn"),
258            Self::QuarticOut => formatter.write_str("QuarticOut"),
259            Self::QuarticInOut => formatter.write_str("QuarticInOut"),
260            Self::ExponentialIn => formatter.write_str("ExponentialIn"),
261            Self::ExponentialOut => formatter.write_str("ExponentialOut"),
262            Self::ExponentialInOut => formatter.write_str("ExponentialInOut"),
263            Self::BackIn { overshoot } => formatter
264                .debug_struct("BackIn")
265                .field("overshoot", overshoot)
266                .finish(),
267            Self::BackOut { overshoot } => formatter
268                .debug_struct("BackOut")
269                .field("overshoot", overshoot)
270                .finish(),
271            Self::BackInOut { overshoot } => formatter
272                .debug_struct("BackInOut")
273                .field("overshoot", overshoot)
274                .finish(),
275            Self::ElasticOut { amplitude, period } => formatter
276                .debug_struct("ElasticOut")
277                .field("amplitude", amplitude)
278                .field("period", period)
279                .finish(),
280            Self::BounceOut => formatter.write_str("BounceOut"),
281            Self::Steps(count) => formatter.debug_tuple("Steps").field(count).finish(),
282            Self::CubicBezier { x1, y1, x2, y2 } => formatter
283                .debug_struct("CubicBezier")
284                .field("x1", x1)
285                .field("y1", y1)
286                .field("x2", x2)
287                .field("y2", y2)
288                .finish(),
289            Self::Custom(_) => formatter.write_str("Custom(..)"),
290        }
291    }
292}
293
294fn check_overshoot(overshoot: f64) -> Result<f64, crate::ValidationError> {
295    if overshoot.is_finite() && overshoot >= 0.0 {
296        Ok(overshoot)
297    } else {
298        Err(easing_error(
299            "overshoot",
300            "back overshoot must be finite and nonnegative",
301        ))
302    }
303}
304
305fn easing_error(field: &str, message: &str) -> crate::ValidationError {
306    crate::ValidationError::invalid("easing", field, message)
307}
308
309fn mirrored(progress: f64, inward: impl Fn(f64) -> f64) -> f64 {
310    if progress < 0.5 {
311        inward(2.0 * progress) * 0.5
312    } else {
313        1.0 - inward(2.0 * (1.0 - progress)) * 0.5
314    }
315}
316
317fn back_in(progress: f64, overshoot: f64) -> f64 {
318    progress * progress * (progress + overshoot * (progress - 1.0))
319}
320
321fn bounce_out(progress: f64) -> f64 {
322    let (shifted, offset) = if progress < 1.0 / 2.75 {
323        (progress, 0.0)
324    } else if progress < 2.0 / 2.75 {
325        (progress - 1.5 / 2.75, 0.75)
326    } else if progress < 2.5 / 2.75 {
327        (progress - 2.25 / 2.75, 0.9375)
328    } else {
329        (progress - 2.625 / 2.75, 0.984375)
330    };
331    7.5625 * shifted * shifted + offset
332}
333
334fn coordinate(parameter: f64, first: f64, second: f64) -> f64 {
335    let inverse = 1.0 - parameter;
336    let first_weight = 3.0 * inverse * inverse * parameter;
337    let second_weight = 3.0 * inverse * parameter * parameter;
338    first_weight * first + second_weight * second + parameter * parameter * parameter
339}
340
341fn cubic_bezier(progress: f64, x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
342    coordinate(bezier_parameter(progress, x1, x2), y1, y2)
343}
344
345fn bezier_parameter(progress: f64, x1: f64, x2: f64) -> f64 {
346    if progress > 0.5 {
347        return 1.0 - bezier_parameter(1.0 - progress, 1.0 - x2, 1.0 - x1);
348    }
349    let mut lower = 0.0;
350    let mut upper = 1.0;
351    for _ in 0..1_100 {
352        let parameter = lower + (upper - lower) * 0.5;
353        let horizontal = coordinate(parameter, x1, x2);
354        if horizontal == progress || parameter == lower || parameter == upper {
355            return parameter;
356        }
357        if horizontal < progress {
358            lower = parameter;
359        } else {
360            upper = parameter;
361        }
362    }
363    lower + (upper - lower) * 0.5
364}