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