goud_engine/core/math/
easing.rs1pub type EasingFn = fn(f32) -> f32;
8
9#[inline]
11pub fn linear(t: f32) -> f32 {
12 t
13}
14
15#[inline]
17pub fn ease_in(t: f32) -> f32 {
18 t * t
19}
20
21#[inline]
23pub fn ease_out(t: f32) -> f32 {
24 t * (2.0 - t)
25}
26
27#[inline]
29pub fn ease_in_out(t: f32) -> f32 {
30 if t < 0.5 {
31 2.0 * t * t
32 } else {
33 -1.0 + (4.0 - 2.0 * t) * t
34 }
35}
36
37#[inline]
41pub fn ease_in_back(t: f32) -> f32 {
42 const S: f32 = 1.70158;
43 t * t * ((S + 1.0) * t - S)
44}
45
46#[inline]
50pub fn ease_out_bounce(t: f32) -> f32 {
51 const N1: f32 = 7.5625;
52 const D1: f32 = 2.75;
53
54 if t < 1.0 / D1 {
55 N1 * t * t
56 } else if t < 2.0 / D1 {
57 let t = t - 1.5 / D1;
58 N1 * t * t + 0.75
59 } else if t < 2.5 / D1 {
60 let t = t - 2.25 / D1;
61 N1 * t * t + 0.9375
62 } else {
63 let t = t - 2.625 / D1;
64 N1 * t * t + 0.984375
65 }
66}
67
68#[repr(C)]
73#[derive(Clone, Copy, Debug, PartialEq)]
74pub struct BezierEasing {
75 pub x1: f32,
77 pub y1: f32,
79 pub x2: f32,
81 pub y2: f32,
83}
84
85impl BezierEasing {
86 #[inline]
88 pub const fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
89 Self { x1, y1, x2, y2 }
90 }
91
92 pub fn evaluate(&self, t: f32) -> f32 {
97 if t <= 0.0 {
98 return 0.0;
99 }
100 if t >= 1.0 {
101 return 1.0;
102 }
103
104 let mut u = t; for _ in 0..8 {
108 let x = self.sample_x(u) - t;
109 if x.abs() < 1e-6 {
110 return self.sample_y(u);
111 }
112 let dx = self.sample_dx(u);
113 if dx.abs() < 1e-6 {
114 break;
115 }
116 u -= x / dx;
117 }
118
119 let mut lo = 0.0_f32;
121 let mut hi = 1.0_f32;
122 u = t;
123 for _ in 0..20 {
124 let x = self.sample_x(u);
125 if (x - t).abs() < 1e-6 {
126 return self.sample_y(u);
127 }
128 if x < t {
129 lo = u;
130 } else {
131 hi = u;
132 }
133 u = (lo + hi) * 0.5;
134 }
135
136 self.sample_y(u)
137 }
138
139 #[inline]
141 fn sample_x(&self, u: f32) -> f32 {
142 let u1 = 1.0 - u;
144 3.0 * u1 * u1 * u * self.x1 + 3.0 * u1 * u * u * self.x2 + u * u * u
145 }
146
147 #[inline]
149 fn sample_y(&self, u: f32) -> f32 {
150 let u1 = 1.0 - u;
151 3.0 * u1 * u1 * u * self.y1 + 3.0 * u1 * u * u * self.y2 + u * u * u
152 }
153
154 #[inline]
156 fn sample_dx(&self, u: f32) -> f32 {
157 let u1 = 1.0 - u;
158 3.0 * u1 * u1 * self.x1 + 6.0 * u1 * u * (self.x2 - self.x1) + 3.0 * u * u * (1.0 - self.x2)
159 }
160}
161
162#[derive(Clone, Copy, Debug, PartialEq)]
164pub enum Easing {
165 Linear,
167 EaseIn,
169 EaseOut,
171 EaseInOut,
173 EaseInBack,
175 EaseOutBounce,
177 CubicBezier(BezierEasing),
179}
180
181impl Easing {
182 #[inline]
184 pub fn apply(&self, t: f32) -> f32 {
185 match self {
186 Easing::Linear => linear(t),
187 Easing::EaseIn => ease_in(t),
188 Easing::EaseOut => ease_out(t),
189 Easing::EaseInOut => ease_in_out(t),
190 Easing::EaseInBack => ease_in_back(t),
191 Easing::EaseOutBounce => ease_out_bounce(t),
192 Easing::CubicBezier(bezier) => bezier.evaluate(t),
193 }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 const EPSILON: f32 = 1e-5;
202
203 fn assert_boundaries(f: EasingFn, name: &str) {
204 assert!(
205 (f(0.0)).abs() < EPSILON,
206 "{name}: f(0) should be 0, got {}",
207 f(0.0)
208 );
209 assert!(
210 (f(1.0) - 1.0).abs() < EPSILON,
211 "{name}: f(1) should be 1, got {}",
212 f(1.0)
213 );
214 }
215
216 #[test]
217 fn test_linear_boundaries() {
218 assert_boundaries(linear, "linear");
219 }
220
221 #[test]
222 fn test_linear_midpoint() {
223 assert!((linear(0.5) - 0.5).abs() < EPSILON);
224 }
225
226 #[test]
227 fn test_ease_in_boundaries() {
228 assert_boundaries(ease_in, "ease_in");
229 }
230
231 #[test]
232 fn test_ease_in_is_slow_start() {
233 assert!((ease_in(0.5) - 0.25).abs() < EPSILON);
235 }
236
237 #[test]
238 fn test_ease_out_boundaries() {
239 assert_boundaries(ease_out, "ease_out");
240 }
241
242 #[test]
243 fn test_ease_out_is_slow_end() {
244 assert!((ease_out(0.5) - 0.75).abs() < EPSILON);
246 }
247
248 #[test]
249 fn test_ease_in_out_boundaries() {
250 assert_boundaries(ease_in_out, "ease_in_out");
251 }
252
253 #[test]
254 fn test_ease_in_out_midpoint() {
255 assert!((ease_in_out(0.5) - 0.5).abs() < EPSILON);
256 }
257
258 #[test]
259 fn test_ease_in_back_boundaries() {
260 assert_boundaries(ease_in_back, "ease_in_back");
261 }
262
263 #[test]
264 fn test_ease_in_back_overshoots_negative() {
265 assert!(ease_in_back(0.25) < 0.0);
267 }
268
269 #[test]
270 fn test_ease_out_bounce_boundaries() {
271 assert_boundaries(ease_out_bounce, "ease_out_bounce");
272 }
273
274 #[test]
275 fn test_ease_out_bounce_midpoint_above_half() {
276 assert!(ease_out_bounce(0.5) > 0.5);
278 }
279
280 #[test]
281 fn test_bezier_linear() {
282 let bezier = BezierEasing::new(0.0, 0.0, 1.0, 1.0);
284 assert!((bezier.evaluate(0.0)).abs() < EPSILON);
285 assert!((bezier.evaluate(1.0) - 1.0).abs() < EPSILON);
286 assert!((bezier.evaluate(0.5) - 0.5).abs() < 0.01);
287 }
288
289 #[test]
290 fn test_bezier_css_ease() {
291 let bezier = BezierEasing::new(0.25, 0.1, 0.25, 1.0);
293 assert!((bezier.evaluate(0.0)).abs() < EPSILON);
294 assert!((bezier.evaluate(1.0) - 1.0).abs() < EPSILON);
295 let mid = bezier.evaluate(0.5);
297 assert!(mid > 0.5, "CSS ease at 0.5 should be > 0.5, got {mid}");
298 }
299
300 #[test]
301 fn test_easing_enum_delegates() {
302 let easings = [
303 (Easing::Linear, linear as EasingFn),
304 (Easing::EaseIn, ease_in as EasingFn),
305 (Easing::EaseOut, ease_out as EasingFn),
306 (Easing::EaseInOut, ease_in_out as EasingFn),
307 (Easing::EaseInBack, ease_in_back as EasingFn),
308 (Easing::EaseOutBounce, ease_out_bounce as EasingFn),
309 ];
310 for (easing, func) in &easings {
311 for &t in &[0.0, 0.25, 0.5, 0.75, 1.0] {
312 assert!(
313 (easing.apply(t) - func(t)).abs() < EPSILON,
314 "Easing enum mismatch at t={t}"
315 );
316 }
317 }
318 }
319
320 #[test]
321 fn test_easing_enum_cubic_bezier() {
322 let bezier = BezierEasing::new(0.42, 0.0, 0.58, 1.0);
323 let easing = Easing::CubicBezier(bezier);
324 assert!((easing.apply(0.0)).abs() < EPSILON);
325 assert!((easing.apply(1.0) - 1.0).abs() < EPSILON);
326 }
327}