telar_motion_core/
easing.rs1#[derive(Debug, Clone, Copy, PartialEq)]
3pub enum Easing {
4 Linear,
5 EaseIn,
6 EaseOut,
7 EaseInOut,
8 CubicBezier(f32, f32, f32, f32),
10}
11
12const NEWTON_ITERS: u32 = 8;
14const NEWTON_MIN_SLOPE: f32 = 1e-6;
16const BISECTION_ITERS: u32 = 32;
17const SUBDIVISION_EPS: f32 = 1e-7;
18
19impl Easing {
20 pub fn apply(self, t: f32) -> f32 {
21 let t = t.clamp(0.0, 1.0);
22 match self {
23 Easing::Linear => t,
24 Easing::EaseIn => t * t * t,
25 Easing::EaseOut => {
26 let u = 1.0 - t;
27 1.0 - u * u * u
28 }
29 Easing::EaseInOut => {
30 if t < 0.5 {
31 4.0 * t * t * t
32 } else {
33 let u = -2.0 * t + 2.0;
34 1.0 - u * u * u / 2.0
35 }
36 }
37 Easing::CubicBezier(x1, y1, x2, y2) => {
38 let u = solve_bezier_x(t, x1, x2);
39 bezier_axis(u, y1, y2)
40 }
41 }
42 }
43}
44
45fn bezier_axis(u: f32, p1: f32, p2: f32) -> f32 {
47 let c = 3.0 * p1;
48 let b = 3.0 * (p2 - p1) - c;
49 let a = 1.0 - c - b;
50 ((a * u + b) * u + c) * u
51}
52
53fn bezier_axis_slope(u: f32, p1: f32, p2: f32) -> f32 {
54 let c = 3.0 * p1;
55 let b = 3.0 * (p2 - p1) - c;
56 let a = 1.0 - c - b;
57 (3.0 * a * u + 2.0 * b) * u + c
58}
59
60fn solve_bezier_x(x: f32, x1: f32, x2: f32) -> f32 {
62 let mut u = x;
63 for _ in 0..NEWTON_ITERS {
64 let error = bezier_axis(u, x1, x2) - x;
65 if error.abs() < SUBDIVISION_EPS {
66 return u;
67 }
68 let slope = bezier_axis_slope(u, x1, x2);
69 if slope.abs() < NEWTON_MIN_SLOPE {
70 break;
71 }
72 u -= error / slope;
73 }
74 let (mut low, mut high, mut u) = (0.0f32, 1.0f32, x.clamp(0.0, 1.0));
75 for _ in 0..BISECTION_ITERS {
76 let value = bezier_axis(u, x1, x2);
77 if (value - x).abs() < SUBDIVISION_EPS {
78 return u;
79 }
80 if value < x {
81 low = u;
82 } else {
83 high = u;
84 }
85 u = (low + high) * 0.5;
86 }
87 u
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn linear_is_identity() {
96 assert!((Easing::Linear.apply(0.3) - 0.3).abs() < 1e-6);
97 }
98
99 #[test]
100 fn apply_clamps_out_of_range_input() {
101 assert_eq!(Easing::Linear.apply(-1.0), 0.0);
102 assert_eq!(Easing::Linear.apply(2.0), 1.0);
103 }
104
105 #[test]
106 fn cubic_easings_hit_endpoints() {
107 for easing in [Easing::EaseIn, Easing::EaseOut, Easing::EaseInOut] {
108 assert!(easing.apply(0.0).abs() < 1e-6, "{easing:?} at 0");
109 assert!((easing.apply(1.0) - 1.0).abs() < 1e-6, "{easing:?} at 1");
110 }
111 }
112
113 #[test]
114 fn ease_in_out_is_symmetric_at_midpoint() {
115 assert!((Easing::EaseInOut.apply(0.5) - 0.5).abs() < 1e-6);
116 }
117
118 #[test]
119 fn cubic_bezier_diagonal_is_linear() {
120 let curve = Easing::CubicBezier(0.0, 0.0, 1.0, 1.0);
121 for &t in &[0.0, 0.25, 0.5, 0.75, 1.0] {
122 assert!((curve.apply(t) - t).abs() < 1e-3, "t={t}");
123 }
124 }
125
126 #[test]
127 fn cubic_bezier_css_ease_accelerates_early() {
128 let ease = Easing::CubicBezier(0.25, 0.1, 0.25, 1.0);
130 assert!(ease.apply(0.0).abs() < 1e-4);
131 assert!((ease.apply(1.0) - 1.0).abs() < 1e-4);
132 let mid = ease.apply(0.5);
133 assert!(mid > 0.5, "ease at 0.5 = {mid}");
134 }
135
136 #[test]
137 fn cubic_bezier_is_monotonic() {
138 let curve = Easing::CubicBezier(0.42, 0.0, 0.58, 1.0);
139 let mut prev = curve.apply(0.0);
140 for i in 1..=20 {
141 let y = curve.apply(i as f32 / 20.0);
142 assert!(y >= prev - 1e-4, "not monotonic at {i}: {y} < {prev}");
143 prev = y;
144 }
145 }
146}