Skip to main content

proof_engine/tween/
easing.rs

1//! 40+ easing functions following Robert Penner's equations.
2//!
3//! Each variant describes an acceleration curve over t ∈ [0, 1] → [0, 1].
4//! Use `Easing::apply(t)` to convert normalized time to a curved value.
5
6use std::f32::consts::{PI, TAU};
7
8/// An easing function — transforms normalized time t ∈ [0, 1] → output ∈ [0, 1].
9///
10/// For typical usage, In = slow start, Out = slow end, InOut = slow at both ends.
11#[derive(Clone, Copy, Debug, PartialEq)]
12pub enum Easing {
13    // ── Polynomial ───────────────────────────────────────────────────────────
14    Linear,
15    EaseInQuad,
16    EaseOutQuad,
17    EaseInOutQuad,
18    EaseInCubic,
19    EaseOutCubic,
20    EaseInOutCubic,
21    EaseInQuart,
22    EaseOutQuart,
23    EaseInOutQuart,
24    EaseInQuint,
25    EaseOutQuint,
26    EaseInOutQuint,
27
28    // ── Trigonometric ─────────────────────────────────────────────────────────
29    EaseInSine,
30    EaseOutSine,
31    EaseInOutSine,
32
33    // ── Exponential ───────────────────────────────────────────────────────────
34    EaseInExpo,
35    EaseOutExpo,
36    EaseInOutExpo,
37
38    // ── Circular ─────────────────────────────────────────────────────────────
39    EaseInCirc,
40    EaseOutCirc,
41    EaseInOutCirc,
42
43    // ── Back (overshoot) ──────────────────────────────────────────────────────
44    EaseInBack,
45    EaseOutBack,
46    EaseInOutBack,
47
48    // ── Elastic ───────────────────────────────────────────────────────────────
49    EaseInElastic,
50    EaseOutElastic,
51    EaseInOutElastic,
52
53    // ── Bounce ────────────────────────────────────────────────────────────────
54    EaseInBounce,
55    EaseOutBounce,
56    EaseInOutBounce,
57
58    // ── Special ───────────────────────────────────────────────────────────────
59    /// Smooth step: 3t² - 2t³ (zero derivative at endpoints).
60    SmoothStep,
61    /// Smoother step: 6t⁵ - 15t⁴ + 10t³ (Perlin's improved smoothstep).
62    SmootherStep,
63    /// Step at threshold 0.5 (instantaneous jump).
64    Step,
65    /// Instant-in, linear-out.
66    EaseOutLinear,
67    /// Hermite cubic through two tangents.
68    Hermite { p0: f32, m0: f32, p1: f32, m1: f32 },
69    /// Custom power: t^n.
70    Power(f32),
71    /// Sigmoid (logistic) curve.
72    Sigmoid { k: f32 },
73    /// Spring easing (underdamped oscillation to rest).
74    Spring { stiffness: f32, damping: f32 },
75    /// Parabolic arc (projectile).
76    Parabola,
77    /// Flash: instant full, then linear decay.
78    Flash,
79}
80
81impl Easing {
82    /// Apply the easing function to normalized time `t` ∈ [0, 1].
83    pub fn apply(&self, t: f32) -> f32 {
84        let t = t.clamp(0.0, 1.0);
85        match *self {
86            // ── Linear ───────────────────────────────────────────────────────
87            Easing::Linear => t,
88
89            // ── Quadratic ────────────────────────────────────────────────────
90            Easing::EaseInQuad      => t * t,
91            Easing::EaseOutQuad     => 1.0 - (1.0 - t) * (1.0 - t),
92            Easing::EaseInOutQuad   => {
93                if t < 0.5 { 2.0 * t * t }
94                else { 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0 }
95            }
96
97            // ── Cubic ────────────────────────────────────────────────────────
98            Easing::EaseInCubic     => t * t * t,
99            Easing::EaseOutCubic    => 1.0 - (1.0 - t).powi(3),
100            Easing::EaseInOutCubic  => {
101                if t < 0.5 { 4.0 * t * t * t }
102                else { 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0 }
103            }
104
105            // ── Quartic ──────────────────────────────────────────────────────
106            Easing::EaseInQuart     => t * t * t * t,
107            Easing::EaseOutQuart    => 1.0 - (1.0 - t).powi(4),
108            Easing::EaseInOutQuart  => {
109                if t < 0.5 { 8.0 * t * t * t * t }
110                else { 1.0 - (-2.0 * t + 2.0).powi(4) / 2.0 }
111            }
112
113            // ── Quintic ──────────────────────────────────────────────────────
114            Easing::EaseInQuint     => t * t * t * t * t,
115            Easing::EaseOutQuint    => 1.0 - (1.0 - t).powi(5),
116            Easing::EaseInOutQuint  => {
117                if t < 0.5 { 16.0 * t * t * t * t * t }
118                else { 1.0 - (-2.0 * t + 2.0).powi(5) / 2.0 }
119            }
120
121            // ── Sine ─────────────────────────────────────────────────────────
122            Easing::EaseInSine      => 1.0 - (t * PI / 2.0).cos(),
123            Easing::EaseOutSine     => (t * PI / 2.0).sin(),
124            Easing::EaseInOutSine   => -((PI * t).cos() - 1.0) / 2.0,
125
126            // ── Exponential ──────────────────────────────────────────────────
127            Easing::EaseInExpo  => {
128                if t == 0.0 { 0.0 } else { 2.0_f32.powf(10.0 * t - 10.0) }
129            }
130            Easing::EaseOutExpo => {
131                if t == 1.0 { 1.0 } else { 1.0 - 2.0_f32.powf(-10.0 * t) }
132            }
133            Easing::EaseInOutExpo => {
134                if t == 0.0 { return 0.0; }
135                if t == 1.0 { return 1.0; }
136                if t < 0.5 { 2.0_f32.powf(20.0 * t - 10.0) / 2.0 }
137                else { (2.0 - 2.0_f32.powf(-20.0 * t + 10.0)) / 2.0 }
138            }
139
140            // ── Circular ─────────────────────────────────────────────────────
141            Easing::EaseInCirc  => 1.0 - (1.0 - t * t).sqrt(),
142            Easing::EaseOutCirc => (1.0 - (t - 1.0) * (t - 1.0)).sqrt(),
143            Easing::EaseInOutCirc => {
144                if t < 0.5 { (1.0 - (1.0 - (2.0 * t).powi(2)).sqrt()) / 2.0 }
145                else { ((1.0 - (-2.0 * t + 2.0).powi(2)).sqrt() + 1.0) / 2.0 }
146            }
147
148            // ── Back (overshoot) ─────────────────────────────────────────────
149            Easing::EaseInBack => {
150                let c1 = 1.70158_f32;
151                let c3 = c1 + 1.0;
152                c3 * t * t * t - c1 * t * t
153            }
154            Easing::EaseOutBack => {
155                let c1 = 1.70158_f32;
156                let c3 = c1 + 1.0;
157                1.0 + c3 * (t - 1.0).powi(3) + c1 * (t - 1.0).powi(2)
158            }
159            Easing::EaseInOutBack => {
160                let c1 = 1.70158_f32;
161                let c2 = c1 * 1.525;
162                if t < 0.5 {
163                    ((2.0 * t).powi(2) * ((c2 + 1.0) * 2.0 * t - c2)) / 2.0
164                } else {
165                    ((2.0 * t - 2.0).powi(2) * ((c2 + 1.0) * (2.0 * t - 2.0) + c2) + 2.0) / 2.0
166                }
167            }
168
169            // ── Elastic ──────────────────────────────────────────────────────
170            Easing::EaseInElastic => {
171                if t == 0.0 { return 0.0; }
172                if t == 1.0 { return 1.0; }
173                let c4 = TAU / 3.0;
174                -2.0_f32.powf(10.0 * t - 10.0) * ((t * 10.0 - 10.75) * c4).sin()
175            }
176            Easing::EaseOutElastic => {
177                if t == 0.0 { return 0.0; }
178                if t == 1.0 { return 1.0; }
179                let c4 = TAU / 3.0;
180                2.0_f32.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
181            }
182            Easing::EaseInOutElastic => {
183                if t == 0.0 { return 0.0; }
184                if t == 1.0 { return 1.0; }
185                let c5 = TAU / 4.5;
186                if t < 0.5 {
187                    -(2.0_f32.powf(20.0 * t - 10.0) * ((20.0 * t - 11.125) * c5).sin()) / 2.0
188                } else {
189                    (2.0_f32.powf(-20.0 * t + 10.0) * ((20.0 * t - 11.125) * c5).sin()) / 2.0 + 1.0
190                }
191            }
192
193            // ── Bounce ───────────────────────────────────────────────────────
194            Easing::EaseOutBounce => bounce_out(t),
195            Easing::EaseInBounce  => 1.0 - bounce_out(1.0 - t),
196            Easing::EaseInOutBounce => {
197                if t < 0.5 { (1.0 - bounce_out(1.0 - 2.0 * t)) / 2.0 }
198                else { (1.0 + bounce_out(2.0 * t - 1.0)) / 2.0 }
199            }
200
201            // ── Special ──────────────────────────────────────────────────────
202            Easing::SmoothStep    => t * t * (3.0 - 2.0 * t),
203            Easing::SmootherStep  => t * t * t * (t * (t * 6.0 - 15.0) + 10.0),
204            Easing::Step          => if t < 0.5 { 0.0 } else { 1.0 },
205            Easing::EaseOutLinear => t,
206            Easing::Parabola      => 4.0 * t * (1.0 - t),
207            Easing::Flash         => if t < f32::EPSILON { 1.0 } else { 1.0 - t },
208
209            Easing::Power(n)      => t.powf(n),
210
211            Easing::Sigmoid { k } => {
212                let s = |x: f32| 1.0 / (1.0 + (-k * x).exp());
213                (s(t - 0.5) - s(-0.5)) / (s(0.5) - s(-0.5))
214            }
215
216            Easing::Spring { stiffness, damping } => {
217                // Underdamped spring analytical solution
218                let omega = stiffness.sqrt();
219                let zeta  = damping / (2.0 * omega).max(f32::EPSILON);
220                if zeta >= 1.0 {
221                    1.0 - (1.0 + omega * t) * (-omega * t).exp()
222                } else {
223                    let omega_d = omega * (1.0 - zeta * zeta).sqrt();
224                    let decay   = (-zeta * omega * t).exp();
225                    1.0 - decay * ((omega_d * t).cos() +
226                          (zeta / (1.0 - zeta * zeta).sqrt()) * (omega_d * t).sin())
227                }
228            }
229
230            Easing::Hermite { p0, m0, p1, m1 } => {
231                let t2 = t * t;
232                let t3 = t2 * t;
233                let h00 =  2.0 * t3 - 3.0 * t2 + 1.0;
234                let h10 =        t3 - 2.0 * t2 + t;
235                let h01 = -2.0 * t3 + 3.0 * t2;
236                let h11 =        t3 -       t2;
237                h00 * p0 + h10 * m0 + h01 * p1 + h11 * m1
238            }
239        }
240    }
241
242    /// Return the approximate derivative dE/dt at `t` using central differences.
243    pub fn derivative(&self, t: f32) -> f32 {
244        let eps = 1e-4_f32;
245        let hi = self.apply((t + eps).min(1.0));
246        let lo = self.apply((t - eps).max(0.0));
247        (hi - lo) / (2.0 * eps)
248    }
249
250    /// Name for debug display.
251    pub fn name(&self) -> &'static str {
252        match self {
253            Easing::Linear          => "Linear",
254            Easing::EaseInQuad      => "EaseInQuad",
255            Easing::EaseOutQuad     => "EaseOutQuad",
256            Easing::EaseInOutQuad   => "EaseInOutQuad",
257            Easing::EaseInCubic     => "EaseInCubic",
258            Easing::EaseOutCubic    => "EaseOutCubic",
259            Easing::EaseInOutCubic  => "EaseInOutCubic",
260            Easing::EaseInQuart     => "EaseInQuart",
261            Easing::EaseOutQuart    => "EaseOutQuart",
262            Easing::EaseInOutQuart  => "EaseInOutQuart",
263            Easing::EaseInQuint     => "EaseInQuint",
264            Easing::EaseOutQuint    => "EaseOutQuint",
265            Easing::EaseInOutQuint  => "EaseInOutQuint",
266            Easing::EaseInSine      => "EaseInSine",
267            Easing::EaseOutSine     => "EaseOutSine",
268            Easing::EaseInOutSine   => "EaseInOutSine",
269            Easing::EaseInExpo      => "EaseInExpo",
270            Easing::EaseOutExpo     => "EaseOutExpo",
271            Easing::EaseInOutExpo   => "EaseInOutExpo",
272            Easing::EaseInCirc      => "EaseInCirc",
273            Easing::EaseOutCirc     => "EaseOutCirc",
274            Easing::EaseInOutCirc   => "EaseInOutCirc",
275            Easing::EaseInBack      => "EaseInBack",
276            Easing::EaseOutBack     => "EaseOutBack",
277            Easing::EaseInOutBack   => "EaseInOutBack",
278            Easing::EaseInElastic   => "EaseInElastic",
279            Easing::EaseOutElastic  => "EaseOutElastic",
280            Easing::EaseInOutElastic => "EaseInOutElastic",
281            Easing::EaseInBounce    => "EaseInBounce",
282            Easing::EaseOutBounce   => "EaseOutBounce",
283            Easing::EaseInOutBounce => "EaseInOutBounce",
284            Easing::SmoothStep      => "SmoothStep",
285            Easing::SmootherStep    => "SmootherStep",
286            Easing::Step            => "Step",
287            Easing::EaseOutLinear   => "EaseOutLinear",
288            Easing::Parabola        => "Parabola",
289            Easing::Flash           => "Flash",
290            Easing::Power(_)        => "Power",
291            Easing::Sigmoid { .. }  => "Sigmoid",
292            Easing::Spring { .. }   => "Spring",
293            Easing::Hermite { .. }  => "Hermite",
294        }
295    }
296
297    /// All named easings (excludes parameterized variants).
298    pub fn all_named() -> &'static [Easing] {
299        &[
300            Easing::Linear,
301            Easing::EaseInQuad, Easing::EaseOutQuad, Easing::EaseInOutQuad,
302            Easing::EaseInCubic, Easing::EaseOutCubic, Easing::EaseInOutCubic,
303            Easing::EaseInQuart, Easing::EaseOutQuart, Easing::EaseInOutQuart,
304            Easing::EaseInQuint, Easing::EaseOutQuint, Easing::EaseInOutQuint,
305            Easing::EaseInSine, Easing::EaseOutSine, Easing::EaseInOutSine,
306            Easing::EaseInExpo, Easing::EaseOutExpo, Easing::EaseInOutExpo,
307            Easing::EaseInCirc, Easing::EaseOutCirc, Easing::EaseInOutCirc,
308            Easing::EaseInBack, Easing::EaseOutBack, Easing::EaseInOutBack,
309            Easing::EaseInElastic, Easing::EaseOutElastic, Easing::EaseInOutElastic,
310            Easing::EaseInBounce, Easing::EaseOutBounce, Easing::EaseInOutBounce,
311            Easing::SmoothStep, Easing::SmootherStep, Easing::Step,
312            Easing::Parabola, Easing::Flash,
313        ]
314    }
315}
316
317/// Shared bounce-out helper (used by In/InOut variants too).
318fn bounce_out(t: f32) -> f32 {
319    const N1: f32 = 7.5625;
320    const D1: f32 = 2.75;
321    if t < 1.0 / D1 {
322        N1 * t * t
323    } else if t < 2.0 / D1 {
324        let t2 = t - 1.5 / D1;
325        N1 * t2 * t2 + 0.75
326    } else if t < 2.5 / D1 {
327        let t2 = t - 2.25 / D1;
328        N1 * t2 * t2 + 0.9375
329    } else {
330        let t2 = t - 2.625 / D1;
331        N1 * t2 * t2 + 0.984375
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    #[test]
340    fn linear_endpoints() {
341        assert!((Easing::Linear.apply(0.0)).abs() < 1e-6);
342        assert!((Easing::Linear.apply(1.0) - 1.0).abs() < 1e-6);
343    }
344
345    #[test]
346    fn all_easings_start_at_zero_end_at_one() {
347        let parameterized = [
348            Easing::Power(2.5),
349            Easing::Sigmoid { k: 5.0 },
350            Easing::Spring { stiffness: 100.0, damping: 10.0 },
351            Easing::SmoothStep,
352            Easing::SmootherStep,
353        ];
354        for e in Easing::all_named().iter().chain(parameterized.iter()) {
355            let start = e.apply(0.0);
356            let end   = e.apply(1.0);
357            assert!((start).abs() < 1e-4, "{} start={}", e.name(), start);
358            assert!((end - 1.0).abs() < 1e-4, "{} end={}", e.name(), end);
359        }
360    }
361
362    #[test]
363    fn bounce_is_monotonic_at_end() {
364        let prev = Easing::EaseOutBounce.apply(0.99);
365        let curr = Easing::EaseOutBounce.apply(1.00);
366        assert!(curr >= prev - 1e-4);
367    }
368
369    #[test]
370    fn spring_overshoots() {
371        let e = Easing::Spring { stiffness: 200.0, damping: 5.0 };
372        let max = (0..200).map(|i| (e.apply(i as f32 / 100.0) * 1000.0) as i32).max().unwrap();
373        assert!(max > 1000, "spring should overshoot past 1.0 (max={})", max);
374    }
375
376    #[test]
377    fn hermite_through_endpoints() {
378        let e = Easing::Hermite { p0: 0.0, m0: 1.0, p1: 1.0, m1: 1.0 };
379        assert!((e.apply(0.0) - 0.0).abs() < 1e-5);
380        assert!((e.apply(1.0) - 1.0).abs() < 1e-5);
381    }
382
383    #[test]
384    fn parabola_peaks_at_half() {
385        let e = Easing::Parabola;
386        assert!((e.apply(0.5) - 1.0).abs() < 1e-5);
387        assert!(e.apply(0.0).abs() < 1e-5);
388        assert!(e.apply(1.0).abs() < 1e-5);
389    }
390}