waterui_core/animation_system/
easing.rs1use core::time::Duration;
17
18#[derive(Debug, Clone, Copy, PartialEq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub enum EasingCurve {
29 CubicBezier(f32, f32, f32, f32),
36 Spring {
41 stiffness: f32,
43 damping: f32,
45 },
46}
47
48impl EasingCurve {
49 pub const LINEAR: Self = Self::CubicBezier(0.0, 0.0, 1.0, 1.0);
52
53 pub const EASE_IN: Self = Self::CubicBezier(0.42, 0.0, 1.0, 1.0);
56
57 pub const EASE_OUT: Self = Self::CubicBezier(0.0, 0.0, 0.58, 1.0);
60
61 pub const EASE_IN_OUT: Self = Self::CubicBezier(0.42, 0.0, 0.58, 1.0);
64
65 pub const EASE: Self = Self::CubicBezier(0.25, 0.1, 0.25, 1.0);
68
69 #[must_use]
71 pub const fn bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> Self {
72 Self::CubicBezier(x1, y1, x2, y2)
73 }
74
75 #[must_use]
77 pub const fn spring(stiffness: f32, damping: f32) -> Self {
78 Self::Spring { stiffness, damping }
79 }
80
81 #[must_use]
86 pub fn ease(&self, t: f32) -> f32 {
87 match self {
88 Self::CubicBezier(x1, y1, x2, y2) => cubic_bezier_ease(t, *x1, *y1, *x2, *y2),
89 Self::Spring { stiffness, damping } => spring_ease(t, *stiffness, *damping),
90 }
91 }
92
93 #[must_use]
95 pub const fn is_spring(&self) -> bool {
96 matches!(self, Self::Spring { .. })
97 }
98}
99
100impl Default for EasingCurve {
101 fn default() -> Self {
102 Self::EASE_IN_OUT
103 }
104}
105
106fn cubic_bezier_ease(t: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
111 const EPSILON: f32 = 0.0001;
112
113 if t <= 0.0 {
115 return 0.0;
116 }
117 if t >= 1.0 {
118 return 1.0;
119 }
120
121 if (x1 - y1).abs() < 0.0001 && (x2 - y2).abs() < 0.0001 {
123 return t;
124 }
125
126 let mut guess = t;
128 let mut converged = false;
129 for _ in 0..8 {
130 let x = bezier_sample(guess, x1, x2) - t;
131 if x.abs() < EPSILON {
132 converged = true;
133 break;
134 }
135 let dx = bezier_derivative(guess, x1, x2);
136 if dx.abs() < 0.000_001 {
137 break;
138 }
139 let next = guess - x / dx;
140 if !(0.0..=1.0).contains(&next) {
141 break;
142 }
143 guess = next;
144 }
145
146 if !converged {
149 let mut low = 0.0;
150 let mut high = 1.0;
151 guess = t.clamp(0.0, 1.0);
152 for _ in 0..16 {
153 let sample = bezier_sample(guess, x1, x2);
154 let delta = sample - t;
155 if delta.abs() < EPSILON {
156 break;
157 }
158 if delta > 0.0 {
159 high = guess;
160 } else {
161 low = guess;
162 }
163 guess = f32::midpoint(low, high);
164 }
165 }
166
167 guess = guess.clamp(0.0, 1.0);
169
170 bezier_sample(guess, y1, y2)
172}
173
174#[inline]
177fn bezier_sample(t: f32, p1: f32, p2: f32) -> f32 {
178 let t2 = t * t;
180 let t3 = t2 * t;
181 let mt = 1.0 - t;
182 let mt2 = mt * mt;
183 (3.0 * mt2 * t).mul_add(p1, (3.0 * mt * t2).mul_add(p2, t3))
184}
185
186#[inline]
188fn bezier_derivative(t: f32, p1: f32, p2: f32) -> f32 {
189 let t2 = t * t;
191 let mt = 1.0 - t;
192 let mt2 = mt * mt;
193 (3.0 * mt2).mul_add(p1, (6.0 * mt * t).mul_add(p2 - p1, 3.0 * t2 * (1.0 - p2)))
194}
195
196fn spring_ease(t: f32, stiffness: f32, damping: f32) -> f32 {
201 if t <= 0.0 {
202 return 0.0;
203 }
204 if t >= 1.0 {
205 return 1.0;
206 }
207 if !stiffness.is_finite() || !damping.is_finite() || stiffness <= 0.0 || damping < 0.0 {
208 return t;
211 }
212
213 let omega = stiffness.sqrt();
219 let zeta = damping / (2.0 * omega);
220
221 if zeta >= 1.0 {
222 let decay = (-omega * zeta * t).exp();
224 decay.mul_add(-(omega * zeta).mul_add(t, 1.0), 1.0)
225 } else {
226 let omega_d = omega * zeta.mul_add(-zeta, 1.0).sqrt();
228 let decay = (-zeta * omega * t).exp();
229 let cos_part = (omega_d * t).cos();
230 let sin_part = (zeta * omega / omega_d) * (omega_d * t).sin();
231 decay.mul_add(-(cos_part + sin_part), 1.0)
232 }
233}
234
235pub trait Interpolatable: Clone {
240 #[must_use]
242 fn lerp(&self, other: &Self, t: f32) -> Self;
243}
244
245impl Interpolatable for f32 {
247 fn lerp(&self, other: &Self, t: f32) -> Self {
248 self + (other - self) * t
249 }
250}
251
252impl Interpolatable for f64 {
254 fn lerp(&self, other: &Self, t: f32) -> Self {
255 self + (other - self) * Self::from(t)
256 }
257}
258
259impl<A: Interpolatable, B: Interpolatable> Interpolatable for (A, B) {
261 fn lerp(&self, other: &Self, t: f32) -> Self {
262 (self.0.lerp(&other.0, t), self.1.lerp(&other.1, t))
263 }
264}
265
266impl<A: Interpolatable, B: Interpolatable, C: Interpolatable> Interpolatable for (A, B, C) {
267 fn lerp(&self, other: &Self, t: f32) -> Self {
268 (
269 self.0.lerp(&other.0, t),
270 self.1.lerp(&other.1, t),
271 self.2.lerp(&other.2, t),
272 )
273 }
274}
275
276impl<A: Interpolatable, B: Interpolatable, C: Interpolatable, D: Interpolatable> Interpolatable
277 for (A, B, C, D)
278{
279 fn lerp(&self, other: &Self, t: f32) -> Self {
280 (
281 self.0.lerp(&other.0, t),
282 self.1.lerp(&other.1, t),
283 self.2.lerp(&other.2, t),
284 self.3.lerp(&other.3, t),
285 )
286 }
287}
288
289impl<T: Interpolatable + Copy, const N: usize> Interpolatable for [T; N] {
291 fn lerp(&self, other: &Self, t: f32) -> Self {
292 let mut result = *self;
293 for i in 0..N {
294 result[i] = self[i].lerp(&other[i], t);
295 }
296 result
297 }
298}
299
300#[derive(Debug, Clone, PartialEq)]
305#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
306pub struct AnimationSegment {
307 pub duration: Duration,
309 pub curve: EasingCurve,
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn test_linear_easing() {
319 let curve = EasingCurve::LINEAR;
320 assert!((curve.ease(0.0) - 0.0).abs() < 0.001);
321 assert!((curve.ease(0.5) - 0.5).abs() < 0.001);
322 assert!((curve.ease(1.0) - 1.0).abs() < 0.001);
323 }
324
325 #[test]
326 fn test_ease_in() {
327 let curve = EasingCurve::EASE_IN;
328 assert!((curve.ease(0.0) - 0.0).abs() < 0.001);
329 assert!((curve.ease(1.0) - 1.0).abs() < 0.001);
330 assert!(curve.ease(0.5) < 0.5);
332 }
333
334 #[test]
335 fn test_ease_out() {
336 let curve = EasingCurve::EASE_OUT;
337 assert!((curve.ease(0.0) - 0.0).abs() < 0.001);
338 assert!((curve.ease(1.0) - 1.0).abs() < 0.001);
339 assert!(curve.ease(0.5) > 0.5);
341 }
342
343 #[test]
344 fn test_ease_in_out() {
345 let curve = EasingCurve::EASE_IN_OUT;
346 assert!((curve.ease(0.0) - 0.0).abs() < 0.001);
347 assert!((curve.ease(1.0) - 1.0).abs() < 0.001);
348 assert!((curve.ease(0.5) - 0.5).abs() < 0.1);
350 }
351
352 #[test]
353 fn test_spring_settles_to_one() {
354 let curve = EasingCurve::spring(100.0, 10.0);
355 assert!((curve.ease(0.0) - 0.0).abs() < 0.001);
356 assert!((curve.ease(1.0) - 1.0).abs() < 0.001);
357 }
358
359 #[test]
360 fn test_bezier_solver_handles_extreme_control_points() {
361 let curve = EasingCurve::bezier(0.0, 1.0, 1.0, 0.0);
362 for step in 0_u16..=100 {
363 let t = f32::from(step) / 100.0;
364 let eased = curve.ease(t);
365 assert!(eased.is_finite(), "eased must be finite at t={t}");
366 assert!(
367 (0.0..=1.0).contains(&eased),
368 "eased out of range at t={t}: {eased}"
369 );
370 }
371 }
372
373 #[test]
374 fn test_f32_lerp() {
375 let a = 0.0_f32;
376 let b = 10.0_f32;
377 assert!((a.lerp(&b, 0.0) - 0.0).abs() < 0.001);
378 assert!((a.lerp(&b, 0.5) - 5.0).abs() < 0.001);
379 assert!((a.lerp(&b, 1.0) - 10.0).abs() < 0.001);
380 }
381
382 #[test]
383 fn test_tuple_lerp() {
384 let a = (0.0_f32, 0.0_f32);
385 let b = (10.0_f32, 20.0_f32);
386 let mid = a.lerp(&b, 0.5);
387 assert!((mid.0 - 5.0).abs() < 0.001);
388 assert!((mid.1 - 10.0).abs() < 0.001);
389 }
390}