fret_ui_headless/
easing.rs1#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct CubicBezier {
14 pub x1: f32,
15 pub y1: f32,
16 pub x2: f32,
17 pub y2: f32,
18}
19
20impl CubicBezier {
21 pub const fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
22 Self { x1, y1, x2, y2 }
23 }
24
25 pub fn sample(self, x: f32) -> f32 {
27 let x = x.clamp(0.0, 1.0);
28 if x <= 0.0 {
29 return 0.0;
30 }
31 if x >= 1.0 {
32 return 1.0;
33 }
34
35 let t = self.solve_t_for_x(x);
37 self.curve_y(t).clamp(0.0, 1.0)
38 }
39
40 pub fn sample_unclamped(self, x: f32) -> f32 {
45 let x = x.clamp(0.0, 1.0);
46 if x <= 0.0 {
47 return 0.0;
48 }
49 if x >= 1.0 {
50 return 1.0;
51 }
52
53 let t = self.solve_t_for_x(x);
54 self.curve_y(t)
55 }
56
57 fn curve_x(self, t: f32) -> f32 {
58 let t = t.clamp(0.0, 1.0);
59 let u = 1.0 - t;
60 3.0 * u * u * t * self.x1 + 3.0 * u * t * t * self.x2 + t * t * t
61 }
62
63 fn curve_y(self, t: f32) -> f32 {
64 let t = t.clamp(0.0, 1.0);
65 let u = 1.0 - t;
66 3.0 * u * u * t * self.y1 + 3.0 * u * t * t * self.y2 + t * t * t
67 }
68
69 fn curve_dx_dt(self, t: f32) -> f32 {
70 let t = t.clamp(0.0, 1.0);
71 let u = 1.0 - t;
72 3.0 * u * u * self.x1 + 6.0 * u * t * (self.x2 - self.x1) + 3.0 * t * t * (1.0 - self.x2)
74 }
75
76 fn solve_t_for_x(self, x: f32) -> f32 {
77 let mut t = x;
79 for _ in 0..8 {
80 let x_t = self.curve_x(t);
81 let dx = x_t - x;
82 if dx.abs() < 1e-6 {
83 return t.clamp(0.0, 1.0);
84 }
85 let d = self.curve_dx_dt(t);
86 if d.abs() < 1e-6 {
87 break;
88 }
89 t -= dx / d;
90 if !(0.0..=1.0).contains(&t) {
91 break;
92 }
93 }
94
95 let mut lo = 0.0f32;
97 let mut hi = 1.0f32;
98 for _ in 0..24 {
99 let mid = 0.5 * (lo + hi);
100 let x_mid = self.curve_x(mid);
101 if (x_mid - x).abs() < 1e-6 {
102 return mid;
103 }
104 if x_mid < x {
105 lo = mid;
106 } else {
107 hi = mid;
108 }
109 }
110 0.5 * (lo + hi)
111 }
112}
113
114pub const EASE: CubicBezier = CubicBezier::new(0.25, 0.1, 0.25, 1.0);
115pub const EASE_IN: CubicBezier = CubicBezier::new(0.42, 0.0, 1.0, 1.0);
116pub const EASE_OUT: CubicBezier = CubicBezier::new(0.0, 0.0, 0.58, 1.0);
117pub const EASE_IN_OUT: CubicBezier = CubicBezier::new(0.42, 0.0, 0.58, 1.0);
118
119pub const SHADCN_EASE: CubicBezier = CubicBezier::new(0.22, 1.0, 0.36, 1.0);
121
122pub fn linear(t: f32) -> f32 {
123 t.clamp(0.0, 1.0)
124}
125
126pub fn smoothstep(t: f32) -> f32 {
127 let t = t.clamp(0.0, 1.0);
128 t * t * (3.0 - 2.0 * t)
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn cubic_bezier_hits_endpoints() {
137 let c = CubicBezier::new(0.25, 0.1, 0.25, 1.0);
138 assert_eq!(c.sample(0.0), 0.0);
139 assert_eq!(c.sample(1.0), 1.0);
140 }
141
142 #[test]
143 fn cubic_bezier_linear_is_identity() {
144 let c = CubicBezier::new(0.0, 0.0, 1.0, 1.0);
145 for i in 0..=10 {
146 let x = i as f32 / 10.0;
147 let y = c.sample(x);
148 assert!((y - x).abs() < 1e-4, "x={x} y={y}");
149 }
150 }
151
152 #[test]
153 fn cubic_bezier_is_monotonic_for_common_presets() {
154 let curves = [EASE, EASE_IN, EASE_OUT, EASE_IN_OUT, SHADCN_EASE];
155 for c in curves {
156 let mut prev = 0.0f32;
157 for i in 0..=100 {
158 let x = i as f32 / 100.0;
159 let y = c.sample(x);
160 assert!(y >= prev - 1e-4, "curve={c:?} x={x} prev={prev} y={y}");
161 prev = y;
162 }
163 }
164 }
165
166 #[test]
167 fn cubic_bezier_sample_unclamped_allows_overshoot() {
168 let c = CubicBezier::new(0.175, 0.885, 0.32, 1.275);
169 let mut seen_overshoot = false;
170 for i in 0..=100 {
171 let x = i as f32 / 100.0;
172 let y = c.sample_unclamped(x);
173 if y > 1.0 {
174 seen_overshoot = true;
175 break;
176 }
177 }
178 assert!(seen_overshoot);
179 }
180}