Skip to main content

rust_motion_animation/
easing.rs

1//! Easing functions — Rust port of Remotion's `Easing` module.
2//!
3//! All functions map an input in `[0.0, 1.0]` to an output in `[0.0, 1.0]`
4//! (though some may overshoot slightly).
5
6/// An easing function: maps progress `t ∈ [0, 1]` to an eased value.
7pub type EasingFn = fn(f64) -> f64;
8
9// ─── Basic easings ────────────────────────────────────────────────────────────
10
11/// Linear — no easing.
12#[inline]
13pub fn linear(t: f64) -> f64 {
14    t
15}
16
17/// Quad ease-in.
18#[inline]
19pub fn ease_in_quad(t: f64) -> f64 {
20    t * t
21}
22
23/// Quad ease-out.
24#[inline]
25pub fn ease_out_quad(t: f64) -> f64 {
26    t * (2.0 - t)
27}
28
29/// Quad ease-in-out.
30#[inline]
31pub fn ease_in_out_quad(t: f64) -> f64 {
32    if t < 0.5 {
33        2.0 * t * t
34    } else {
35        -1.0 + (4.0 - 2.0 * t) * t
36    }
37}
38
39/// Cubic ease-in.
40#[inline]
41pub fn ease_in_cubic(t: f64) -> f64 {
42    t * t * t
43}
44
45/// Cubic ease-out.
46#[inline]
47pub fn ease_out_cubic(t: f64) -> f64 {
48    let t1 = t - 1.0;
49    t1 * t1 * t1 + 1.0
50}
51
52/// Cubic ease-in-out.
53#[inline]
54pub fn ease_in_out_cubic(t: f64) -> f64 {
55    if t < 0.5 {
56        4.0 * t * t * t
57    } else {
58        (t - 1.0) * (2.0 * t - 2.0) * (2.0 * t - 2.0) + 1.0
59    }
60}
61
62/// Sine ease-in.
63#[inline]
64pub fn ease_in_sine(t: f64) -> f64 {
65    1.0 - (t * std::f64::consts::FRAC_PI_2).cos()
66}
67
68/// Sine ease-out.
69#[inline]
70pub fn ease_out_sine(t: f64) -> f64 {
71    (t * std::f64::consts::FRAC_PI_2).sin()
72}
73
74/// Sine ease-in-out.
75#[inline]
76pub fn ease_in_out_sine(t: f64) -> f64 {
77    -(((std::f64::consts::PI * t).cos() - 1.0) / 2.0)
78}
79
80// ─── Cubic Bézier (Remotion `Easing.bezier`) ─────────────────────────────────
81
82/// Generates a cubic Bézier easing function from two control points.
83///
84/// Equivalent to CSS `cubic-bezier(x1, y1, x2, y2)` and Remotion's
85/// `Easing.bezier(x1, y1, x2, y2)`.
86///
87/// # Arguments
88/// * `x1`, `y1` — first control point
89/// * `x2`, `y2` — second control point
90///
91/// Returns a closure that maps `t ∈ [0, 1]` → eased value.
92pub fn bezier(x1: f64, y1: f64, x2: f64, y2: f64) -> impl Fn(f64) -> f64 {
93    move |t: f64| {
94        if t <= 0.0 {
95            return 0.0;
96        }
97        if t >= 1.0 {
98            return 1.0;
99        }
100
101        // Newton's method to find the parameter `u` such that Bx(u) = t
102        let cx = 3.0 * x1;
103        let bx = 3.0 * (x2 - x1) - cx;
104        let ax = 1.0 - cx - bx;
105
106        let cy = 3.0 * y1;
107        let by = 3.0 * (y2 - y1) - cy;
108        let ay = 1.0 - cy - by;
109
110        let poly_x = |u: f64| ((ax * u + bx) * u + cx) * u;
111        let poly_x_deriv = |u: f64| (3.0 * ax * u + 2.0 * bx) * u + cx;
112        let poly_y = |u: f64| ((ay * u + by) * u + cy) * u;
113
114        // Initial guess
115        let mut u = t;
116        for _ in 0..8 {
117            let x_err = poly_x(u) - t;
118            let d = poly_x_deriv(u);
119            if d.abs() < 1e-12 {
120                break;
121            }
122            u -= x_err / d;
123        }
124        poly_y(u)
125    }
126}
127
128// ─── Remotion-style preset easings ───────────────────────────────────────────
129
130/// CSS `ease` preset — `cubic-bezier(0.25, 0.1, 0.25, 1.0)`.
131pub fn ease() -> impl Fn(f64) -> f64 {
132    bezier(0.25, 0.1, 0.25, 1.0)
133}
134
135/// CSS `ease-in` preset — `cubic-bezier(0.42, 0, 1.0, 1.0)`.
136pub fn ease_in() -> impl Fn(f64) -> f64 {
137    bezier(0.42, 0.0, 1.0, 1.0)
138}
139
140/// CSS `ease-out` preset — `cubic-bezier(0, 0, 0.58, 1.0)`.
141pub fn ease_out() -> impl Fn(f64) -> f64 {
142    bezier(0.0, 0.0, 0.58, 1.0)
143}
144
145/// CSS `ease-in-out` preset — `cubic-bezier(0.42, 0, 0.58, 1.0)`.
146pub fn ease_in_out() -> impl Fn(f64) -> f64 {
147    bezier(0.42, 0.0, 0.58, 1.0)
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn linear_identity() {
156        assert!((linear(0.5) - 0.5).abs() < 1e-10);
157    }
158
159    #[test]
160    fn bezier_endpoints() {
161        let f = bezier(0.25, 0.1, 0.25, 1.0);
162        assert!((f(0.0)).abs() < 1e-10);
163        assert!((f(1.0) - 1.0).abs() < 1e-10);
164    }
165
166    #[test]
167    fn bezier_midpoint_reasonable() {
168        let f = ease_out();
169        // ease-out should be > 0.5 at t=0.5 (fast start, slow end)
170        assert!(f(0.5) > 0.5);
171    }
172}