1#[derive(Clone, Copy, Debug, PartialEq)]
20pub enum Easing {
21 Linear,
23 EaseIn,
25 EaseOut,
27 EaseInOut,
29 CubicBezier {
32 x1: f32,
34 y1: f32,
36 x2: f32,
38 y2: f32,
40 },
41}
42
43impl Easing {
44 pub fn eval(&self, t: f32) -> f32 {
46 let t = t.clamp(0.0, 1.0);
47 match *self {
48 Easing::Linear => t,
49 Easing::EaseIn => cubic_bezier_eval(0.42, 0.0, 1.0, 1.0, t),
50 Easing::EaseOut => cubic_bezier_eval(0.0, 0.0, 0.58, 1.0, t),
51 Easing::EaseInOut => cubic_bezier_eval(0.42, 0.0, 0.58, 1.0, t),
52 Easing::CubicBezier { x1, y1, x2, y2 } => cubic_bezier_eval(x1, y1, x2, y2, t),
53 }
54 }
55}
56
57#[inline]
62fn bezier_axis(c1: f32, c2: f32, u: f32) -> f32 {
63 let one_minus = 1.0 - u;
64 3.0 * one_minus * one_minus * u * c1 + 3.0 * one_minus * u * u * c2 + u * u * u
65}
66
67#[inline]
69fn bezier_axis_deriv(c1: f32, c2: f32, u: f32) -> f32 {
70 let one_minus = 1.0 - u;
71 3.0 * one_minus * one_minus * c1 + 6.0 * one_minus * u * (c2 - c1) + 3.0 * u * u * (1.0 - c2)
72}
73
74fn cubic_bezier_eval(x1: f32, y1: f32, x2: f32, y2: f32, x: f32) -> f32 {
80 if x <= 0.0 {
82 return 0.0;
83 }
84 if x >= 1.0 {
85 return 1.0;
86 }
87 let u = solve_bezier_u_for_x(x1, x2, x);
88 bezier_axis(y1, y2, u)
89}
90
91fn solve_bezier_u_for_x(x1: f32, x2: f32, x: f32) -> f32 {
93 const NEWTON_ITERS: usize = 8;
94 const EPS: f32 = 1e-6;
95
96 let mut u = x;
98 for _ in 0..NEWTON_ITERS {
99 let fx = bezier_axis(x1, x2, u) - x;
100 if fx.abs() < EPS {
101 return u.clamp(0.0, 1.0);
102 }
103 let d = bezier_axis_deriv(x1, x2, u);
104 if d.abs() < 1e-6 {
105 break;
107 }
108 u -= fx / d;
109 u = u.clamp(0.0, 1.0);
111 }
112
113 let mut lo = 0.0_f32;
116 let mut hi = 1.0_f32;
117 let mut mid = u.clamp(lo, hi);
118 for _ in 0..32 {
119 mid = 0.5 * (lo + hi);
120 let fx = bezier_axis(x1, x2, mid);
121 if (fx - x).abs() < EPS {
122 return mid;
123 }
124 if fx < x {
125 lo = mid;
126 } else {
127 hi = mid;
128 }
129 }
130 mid
131}
132
133#[derive(Clone, Copy, Debug, PartialEq)]
139pub struct Spring {
140 pub mass: f32,
142 pub stiffness: f32,
144 pub damping: f32,
146}
147
148impl Default for Spring {
149 fn default() -> Self {
151 Self {
152 mass: 1.0,
153 stiffness: 170.0,
154 damping: 26.0,
155 }
156 }
157}
158
159impl Spring {
160 pub fn new(mass: f32, stiffness: f32, damping: f32) -> Self {
162 Self {
163 mass,
164 stiffness,
165 damping,
166 }
167 }
168
169 pub fn from_frequency(omega0: f32, zeta: f32) -> Self {
172 let mass = 1.0;
173 let stiffness = omega0 * omega0 * mass;
174 let damping = 2.0 * zeta * omega0 * mass;
175 Self {
176 mass,
177 stiffness,
178 damping,
179 }
180 }
181
182 pub fn natural_frequency(&self) -> f32 {
184 (self.stiffness / self.mass.max(f32::EPSILON)).sqrt()
185 }
186
187 pub fn damping_ratio(&self) -> f32 {
189 let denom = 2.0 * (self.stiffness * self.mass).max(f32::EPSILON).sqrt();
190 self.damping / denom
191 }
192
193 pub fn position(&self, from: f32, to: f32, v0: f32, t: f32) -> f32 {
200 if t <= 0.0 {
201 return from;
202 }
203 let x0 = from - to; let omega0 = self.natural_frequency();
205 if omega0 <= f32::EPSILON {
206 return to + x0; }
208 let zeta = self.damping_ratio();
209
210 let offset = if (zeta - 1.0).abs() < 1e-4 {
211 let c2 = v0 + omega0 * x0;
213 (x0 + c2 * t) * (-omega0 * t).exp()
214 } else if zeta < 1.0 {
215 let omega_d = omega0 * (1.0 - zeta * zeta).sqrt();
217 let decay = (-zeta * omega0 * t).exp();
218 let a = x0;
219 let b = (v0 + zeta * omega0 * x0) / omega_d;
220 decay * (a * (omega_d * t).cos() + b * (omega_d * t).sin())
221 } else {
222 let disc = (zeta * zeta - 1.0).sqrt();
224 let r1 = -omega0 * (zeta - disc);
225 let r2 = -omega0 * (zeta + disc);
226 let c1 = (v0 - r2 * x0) / (r1 - r2);
227 let c2 = x0 - c1;
228 c1 * (r1 * t).exp() + c2 * (r2 * t).exp()
229 };
230 to + offset
231 }
232
233 pub fn is_settled(&self, from: f32, to: f32, v0: f32, t: f32, tolerance: f32) -> bool {
236 (self.position(from, to, v0, t) - to).abs() <= tolerance
237 }
238}
239
240#[derive(Clone, Copy, Debug, PartialEq)]
242pub struct Transition {
243 pub duration: f32,
245 pub delay: f32,
247 pub easing: Easing,
249}
250
251impl Transition {
252 pub fn new(duration: f32, easing: Easing) -> Self {
254 Self {
255 duration,
256 delay: 0.0,
257 easing,
258 }
259 }
260
261 pub fn with_delay(mut self, delay: f32) -> Self {
263 self.delay = delay;
264 self
265 }
266
267 pub fn progress(&self, elapsed: f32) -> f32 {
271 let active = elapsed - self.delay;
272 if active <= 0.0 {
273 return 0.0;
274 }
275 if self.duration <= 0.0 || active >= self.duration {
276 return 1.0;
277 }
278 self.easing.eval(active / self.duration)
279 }
280
281 pub fn sample(&self, start: f32, end: f32, elapsed: f32) -> f32 {
283 let p = self.progress(elapsed);
284 start + (end - start) * p
285 }
286
287 pub fn is_finished(&self, elapsed: f32) -> bool {
289 elapsed >= self.delay + self.duration
290 }
291}
292
293#[derive(Clone, Copy, Debug)]
295struct ActiveTransition {
296 key: u64,
297 start: f32,
298 end: f32,
299 transition: Transition,
300 elapsed: f32,
302}
303
304#[derive(Debug, Default)]
311pub struct Animator {
312 active: Vec<ActiveTransition>,
313}
314
315impl Animator {
316 pub fn new() -> Self {
318 Self::default()
319 }
320
321 pub fn active_count(&self) -> usize {
323 self.active.len()
324 }
325
326 pub fn is_animating(&self) -> bool {
328 !self.active.is_empty()
329 }
330
331 pub fn start(&mut self, key: u64, start: f32, end: f32, transition: Transition) {
335 let entry = ActiveTransition {
336 key,
337 start,
338 end,
339 transition,
340 elapsed: 0.0,
341 };
342 if let Some(slot) = self.active.iter_mut().find(|a| a.key == key) {
343 *slot = entry;
344 } else {
345 self.active.push(entry);
346 }
347 }
348
349 pub fn value(&self, key: u64) -> Option<f32> {
352 self.active
353 .iter()
354 .find(|a| a.key == key)
355 .map(|a| a.transition.sample(a.start, a.end, a.elapsed))
356 }
357
358 pub fn advance(&mut self, dt: f32) -> usize {
361 for a in &mut self.active {
362 a.elapsed += dt;
363 }
364 self.active.retain(|a| !a.transition.is_finished(a.elapsed));
365 self.active.len()
366 }
367
368 pub fn cancel(&mut self, key: u64) -> bool {
370 let before = self.active.len();
371 self.active.retain(|a| a.key != key);
372 self.active.len() != before
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 fn close(a: f32, b: f32, eps: f32) -> bool {
381 (a - b).abs() <= eps
382 }
383
384 #[test]
385 fn linear_easing_endpoints_and_midpoint() {
386 assert_eq!(Easing::Linear.eval(0.0), 0.0);
387 assert_eq!(Easing::Linear.eval(1.0), 1.0);
388 assert!(close(Easing::Linear.eval(0.5), 0.5, 1e-6));
389 }
390
391 #[test]
392 fn ease_in_out_is_symmetric_about_half() {
393 let e = Easing::EaseInOut;
394 assert!(close(e.eval(0.0), 0.0, 1e-6));
396 assert!(close(e.eval(1.0), 1.0, 1e-6));
397 assert!(close(e.eval(0.5), 0.5, 1e-3), "got {}", e.eval(0.5));
399 for t in [0.1f32, 0.25, 0.4] {
401 assert!(close(e.eval(t) + e.eval(1.0 - t), 1.0, 2e-3), "t={t}");
402 }
403 }
404
405 #[test]
406 fn ease_in_starts_slow() {
407 let e = Easing::EaseIn;
409 assert!(
410 e.eval(0.25) < 0.25,
411 "ease-in should be below the diagonal early"
412 );
413 assert!(close(e.eval(1.0), 1.0, 1e-6));
414 }
415
416 #[test]
417 fn cubic_bezier_recovers_linear() {
418 let lin = Easing::CubicBezier {
420 x1: 0.25,
421 y1: 0.25,
422 x2: 0.75,
423 y2: 0.75,
424 };
425 for t in [0.0f32, 0.2, 0.5, 0.8, 1.0] {
426 assert!(close(lin.eval(t), t, 2e-3), "t={t} got {}", lin.eval(t));
427 }
428 }
429
430 #[test]
431 fn cubic_bezier_degenerate_does_not_nan() {
432 let e = Easing::CubicBezier {
434 x1: 0.0,
435 y1: 1.0,
436 x2: 1.0,
437 y2: 0.0,
438 };
439 for i in 0..=10 {
440 let t = i as f32 / 10.0;
441 let v = e.eval(t);
442 assert!(v.is_finite(), "value at t={t} must be finite, got {v}");
443 assert!((0.0..=1.0).contains(&v) || close(v, 0.0, 1e-3) || close(v, 1.0, 1e-3));
444 }
445 }
446
447 #[test]
448 fn spring_critically_damped_converges_without_overshoot() {
449 let s = Spring::from_frequency(20.0, 1.0); assert!(close(s.damping_ratio(), 1.0, 1e-3));
451 let mut prev = s.position(0.0, 1.0, 0.0, 0.0);
453 assert!(close(prev, 0.0, 1e-4));
454 for i in 1..=60 {
456 let t = i as f32 / 60.0;
457 let p = s.position(0.0, 1.0, 0.0, t);
458 assert!(p <= 1.0 + 1e-3, "overshoot at t={t}: {p}");
459 assert!(p >= prev - 1e-4, "should be monotone increasing at t={t}");
460 prev = p;
461 }
462 assert!(s.is_settled(0.0, 1.0, 0.0, 1.5, 1e-2));
463 }
464
465 #[test]
466 fn spring_underdamped_overshoots_then_settles() {
467 let s = Spring::from_frequency(30.0, 0.3); assert!(s.damping_ratio() < 1.0);
469 let mut max = f32::MIN;
470 for i in 0..=200 {
471 let t = i as f32 / 100.0;
472 max = max.max(s.position(0.0, 1.0, 0.0, t));
473 }
474 assert!(
475 max > 1.0,
476 "underdamped spring should overshoot the target, max={max}"
477 );
478 assert!(s.is_settled(0.0, 1.0, 0.0, 5.0, 2e-2));
480 }
481
482 #[test]
483 fn spring_overdamped_no_overshoot() {
484 let s = Spring::from_frequency(10.0, 2.0); assert!(s.damping_ratio() > 1.0);
486 for i in 0..=100 {
487 let t = i as f32 / 50.0;
488 let p = s.position(0.0, 1.0, 0.0, t);
489 assert!(
490 p <= 1.0 + 1e-3,
491 "overdamped must not overshoot, t={t} p={p}"
492 );
493 }
494 }
495
496 #[test]
497 fn transition_progress_respects_delay_and_duration() {
498 let tr = Transition::new(2.0, Easing::Linear).with_delay(1.0);
499 assert_eq!(tr.progress(0.5), 0.0); assert!(close(tr.progress(2.0), 0.5, 1e-6)); assert_eq!(tr.progress(3.0), 1.0); assert!(tr.is_finished(3.0));
503 assert!(!tr.is_finished(2.5));
504 assert!(close(tr.sample(10.0, 20.0, 2.0), 15.0, 1e-4));
505 }
506
507 #[test]
508 fn animator_tracks_and_drops_finished() {
509 let mut anim = Animator::new();
510 anim.start(1, 0.0, 100.0, Transition::new(1.0, Easing::Linear));
511 assert!(anim.is_animating());
512 assert!(close(anim.value(1).expect("active"), 0.0, 1e-4));
513 anim.advance(0.5);
514 assert!(close(anim.value(1).expect("active"), 50.0, 1e-3));
515 anim.start(1, 0.0, 100.0, Transition::new(1.0, Easing::Linear));
517 assert!(close(anim.value(1).expect("active"), 0.0, 1e-4));
518 anim.advance(1.5);
520 assert_eq!(anim.active_count(), 0);
521 assert!(anim.value(1).is_none());
522 }
523
524 #[test]
525 fn animator_cancel() {
526 let mut anim = Animator::new();
527 anim.start(7, 0.0, 1.0, Transition::new(1.0, Easing::Linear));
528 assert!(anim.cancel(7));
529 assert!(!anim.cancel(7));
530 }
531}