Skip to main content

repose_core/
animation.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3use web_time::{Duration, Instant};
4
5pub(crate) fn now() -> Instant {
6    let clock = CLOCK.with(|clock| clock.borrow().clone());
7    clock.now()
8}
9
10/// Physical spring parameters. Duration is emergent (determined by physics), not specified.
11#[derive(Clone, Copy, Debug)]
12pub struct SpringSpec {
13    /// Damping ratio ζ: 0 = undamped, <1 = underdamped (overshoot), 1 = critically damped,
14    /// >1 = overdamped.
15    pub damping_ratio: f32,
16    /// Stiffness k: higher = faster, snappier response.
17    pub stiffness: f32,
18    /// Progress threshold for settling: when `|progress - 1.0| < this`, the spring is
19    /// considered visually close enough to the target and stops. Default: 0.005 (0.5%).
20    pub settle_progress: f32,
21    /// Velocity threshold for settling (in progress-units/second). Default: 0.1.
22    pub settle_velocity: f32,
23}
24
25impl SpringSpec {
26    pub const fn new(damping_ratio: f32, stiffness: f32) -> Self {
27        Self {
28            damping_ratio,
29            stiffness,
30            settle_progress: 0.005,
31            settle_velocity: 0.1,
32        }
33    }
34    /// Gentle preset: low overshoot, moderate speed.
35    pub const fn gentle() -> Self {
36        Self::new(0.5, 200.0)
37    }
38    /// Bouncier preset: more overshoot, faster.
39    pub const fn bouncy() -> Self {
40        Self::new(0.2, 300.0)
41    }
42    /// Critically damped: no overshoot, fast settle.
43    pub const fn crit() -> Self {
44        Self::new(1.0, 200.0)
45    }
46    /// Snappy preset: high damping, high stiffness.
47    pub const fn stiff() -> Self {
48        Self::new(0.8, 600.0)
49    }
50
51    /// Set the settling threshold in progress units. Lower values = more precise settling.
52    /// For example, 0.001 means the spring stops when within 0.1% of the target.
53    pub const fn with_settle_progress(mut self, threshold: f32) -> Self {
54        self.settle_progress = threshold;
55        self
56    }
57
58    /// Set the velocity threshold for settling (progress-units/second).
59    pub const fn with_settle_velocity(mut self, threshold: f32) -> Self {
60        self.settle_velocity = threshold;
61        self
62    }
63}
64
65/// A cubic bezier curve with control points (p1x, p1y), (p2x, p2y).
66/// P0 = (0, 0) and P3 = (1, 1) are fixed.
67#[derive(Clone, Copy, Debug)]
68pub struct CubicBezier {
69    pub p1x: f32,
70    pub p1y: f32,
71    pub p2x: f32,
72    pub p2y: f32,
73}
74
75impl CubicBezier {
76    pub const fn new(p1x: f32, p1y: f32, p2x: f32, p2y: f32) -> Self {
77        Self { p1x, p1y, p2x, p2y }
78    }
79}
80
81/// Compose Material3 EmphasizedDecelerate: cubic-bezier(0.05, 0.7, 0.1, 1.0).
82pub const EASING_EMPHASIZED_DECELERATE: CubicBezier = CubicBezier::new(0.05, 0.7, 0.1, 1.0);
83/// Compose Material3 StandardDecelerate: cubic-bezier(0.2, 0.0, 0.0, 1.0).
84pub const EASING_STANDARD_DECELERATE: CubicBezier = CubicBezier::new(0.2, 0.0, 0.0, 1.0);
85
86#[derive(Clone, Copy, Debug)]
87#[non_exhaustive]
88pub enum Easing {
89    Linear,
90    EaseIn,
91    EaseOut,
92    EaseInOut,
93    /// Monotonic, critically-damped, y(t)=1-(1+ω t)e^{-ω t}, t∈[0,1].
94    SpringCrit {
95        omega: f32,
96    },
97    /// Underdamped, low-overshoot preset (ζ≈0.5, ω≈8)
98    SpringGentle,
99    /// Underdamped, bouncier preset (ζ≈0.2, ω≈12)
100    SpringBouncy,
101    /// Android FastOutSlowIn: cubic-bezier(0.4, 0.0, 0.2, 1.0).
102    /// Starts fast, decelerates through the middle, ends slow.
103    FastOutSlowIn,
104    /// Custom cubic-bezier easing with control points (p1x, p1y), (p2x, p2y).
105    Custom(CubicBezier),
106    // Godot-style transition families (TRANS_* × EASE_*), added for games
107    CubicIn,
108    CubicOut,
109    CubicInOut,
110    QuartIn,
111    QuartOut,
112    QuartInOut,
113    QuintIn,
114    QuintOut,
115    QuintInOut,
116    SineIn,
117    SineOut,
118    SineInOut,
119    ExpoIn,
120    ExpoOut,
121    ExpoInOut,
122    CircIn,
123    CircOut,
124    CircInOut,
125    /// Back overshoot preset (Godot default overshoot = 1.70158).
126    BackIn,
127    BackOut,
128    BackInOut,
129    /// Springy damped oscillation below/above the end value.
130    ElasticIn,
131    ElasticOut,
132    ElasticInOut,
133    BounceIn,
134    BounceOut,
135    BounceInOut,
136}
137
138/// Size/alpha curve over life, for games driving per-tick curves
139/// (particle size/alpha, gradient sampling) off the shared [`Easing`]
140/// family. `Default` is [`EaseKind::Linear`]; `serde` impls ride the
141/// crate `serde` feature (asset formats like `.fx.ron` enable it).
142#[derive(Clone, Copy, Debug, Default)]
143#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
144pub enum EaseKind {
145    #[default]
146    Linear,
147    QuadOut,
148    CubicOut,
149    BackOut,
150}
151
152impl EaseKind {
153    /// Ease life fraction 0..1 into curve value 0..1.
154    pub fn apply(self, t: f32) -> f32 {
155        let ease = match self {
156            EaseKind::Linear => Easing::Linear,
157            EaseKind::QuadOut => Easing::EaseOut,
158            EaseKind::CubicOut => Easing::CubicOut,
159            EaseKind::BackOut => Easing::BackOut,
160        };
161        ease.interpolate(t.clamp(0.0, 1.0))
162    }
163}
164
165impl Easing {
166    pub fn interpolate(&self, t: f32) -> f32 {
167        match self {
168            Easing::Linear => t,
169            Easing::EaseIn => t * t,
170            Easing::EaseOut => t * (2.0 - t),
171            Easing::EaseInOut => {
172                if t < 0.5 {
173                    2.0 * t * t
174                } else {
175                    -1.0 + (4.0 - 2.0 * t) * t
176                }
177            }
178            Easing::SpringCrit { omega } => {
179                let w = (*omega).max(0.0);
180                let tt = t.max(0.0);
181                // y = 1 - (1 + w t) e^{-w t}
182                1.0 - (1.0 + w * tt) * (-(w * tt)).exp()
183            }
184            Easing::SpringGentle => spring_underdamped_normalized(t, 0.5, 8.0),
185            Easing::SpringBouncy => spring_underdamped_normalized(t, 0.2, 12.0),
186            Easing::FastOutSlowIn => eval_cubic_bezier(0.4, 0.0, 0.2, 1.0, t),
187            Easing::Custom(cb) => eval_cubic_bezier(cb.p1x, cb.p1y, cb.p2x, cb.p2y, t),
188            Easing::CubicIn => t * t * t,
189            Easing::CubicOut => {
190                let u = t - 1.0;
191                u * u * u + 1.0
192            }
193            Easing::CubicInOut => {
194                if t < 0.5 {
195                    4.0 * t * t * t
196                } else {
197                    let u = 2.0 * t - 2.0;
198                    u * u * u / 2.0 + 1.0
199                }
200            }
201            Easing::QuartIn => t * t * t * t,
202            Easing::QuartOut => {
203                let u = t - 1.0;
204                1.0 - u * u * u * u
205            }
206            Easing::QuartInOut => {
207                if t < 0.5 {
208                    8.0 * t * t * t * t
209                } else {
210                    let u = -2.0 * t + 2.0;
211                    1.0 - u * u * u * u / 2.0
212                }
213            }
214            Easing::QuintIn => t * t * t * t * t,
215            Easing::QuintOut => {
216                let u = t - 1.0;
217                u * u * u * u * u + 1.0
218            }
219            Easing::QuintInOut => {
220                if t < 0.5 {
221                    16.0 * t * t * t * t * t
222                } else {
223                    let u = -2.0 * t + 2.0;
224                    1.0 - u * u * u * u * u / 2.0
225                }
226            }
227            Easing::SineIn => 1.0 - (t * std::f32::consts::FRAC_PI_2).cos(),
228            Easing::SineOut => (t * std::f32::consts::FRAC_PI_2).sin(),
229            Easing::SineInOut => 0.5 * (1.0 - (t * std::f32::consts::PI).cos()),
230            Easing::ExpoIn => {
231                if t <= 0.0 {
232                    0.0
233                } else {
234                    2.0f32.powf(10.0 * t - 10.0)
235                }
236            }
237            Easing::ExpoOut => {
238                if t >= 1.0 {
239                    1.0
240                } else {
241                    1.0 - 2.0f32.powf(-10.0 * t)
242                }
243            }
244            Easing::ExpoInOut => {
245                if t <= 0.0 {
246                    0.0
247                } else if t >= 1.0 {
248                    1.0
249                } else if t < 0.5 {
250                    2.0f32.powf(20.0 * t - 10.0) / 2.0
251                } else {
252                    (2.0 - 2.0f32.powf(-20.0 * t + 10.0)) / 2.0
253                }
254            }
255            Easing::CircIn => 1.0 - (1.0 - t * t).sqrt(),
256            Easing::CircOut => (1.0 - (t - 1.0) * (t - 1.0)).sqrt(),
257            Easing::CircInOut => {
258                if t < 0.5 {
259                    (1.0 - (1.0 - 4.0 * t * t).sqrt()) / 2.0
260                } else {
261                    ((1.0 - (-2.0 * t + 2.0) * (-2.0 * t + 2.0)).sqrt() + 1.0) / 2.0
262                }
263            }
264            Easing::BackIn => {
265                const C1: f32 = 1.70158;
266                const C3: f32 = C1 + 1.0;
267                C3 * t * t * t - C1 * t * t
268            }
269            Easing::BackOut => {
270                const C1: f32 = 1.70158;
271                const C3: f32 = C1 + 1.0;
272                let u = t - 1.0;
273                1.0 + C3 * u * u * u + C1 * u * u
274            }
275            Easing::BackInOut => {
276                const C1: f32 = 1.70158;
277                const C3: f32 = C1 + 1.0;
278                if t < 0.5 {
279                    let u = 2.0 * t;
280                    (C3 * u * u * u - C1 * u * u) / 2.0
281                } else {
282                    let u = 2.0 * t - 2.0;
283                    (C3 * u * u * u + C1 * u * u) / 2.0 + 1.0
284                }
285            }
286            Easing::ElasticIn => {
287                const C4: f32 = 2.0 * std::f32::consts::PI / 3.0;
288                if t <= 0.0 {
289                    0.0
290                } else if t >= 1.0 {
291                    1.0
292                } else {
293                    -(2.0f32.powf(10.0 * t - 10.0) * ((10.0 * t - 10.75) * C4).sin())
294                }
295            }
296            Easing::ElasticOut => {
297                const C4: f32 = 2.0 * std::f32::consts::PI / 3.0;
298                if t <= 0.0 {
299                    0.0
300                } else if t >= 1.0 {
301                    1.0
302                } else {
303                    2.0f32.powf(-10.0 * t) * ((10.0 * t - 0.75) * C4).sin() + 1.0
304                }
305            }
306            Easing::ElasticInOut => {
307                const C4: f32 = 2.0 * std::f32::consts::PI / 3.0;
308                if t <= 0.0 {
309                    0.0
310                } else if t >= 1.0 {
311                    1.0
312                } else if t < 0.5 {
313                    -(2.0f32.powf(20.0 * t - 10.0) * ((20.0 * t - 11.125) * C4).sin()) / 2.0
314                } else {
315                    2.0f32.powf(-20.0 * t + 10.0) * ((20.0 * t - 11.125) * C4).sin() / 2.0 + 1.0
316                }
317            }
318            Easing::BounceIn => 1.0 - bounce_out(1.0 - t),
319            Easing::BounceOut => bounce_out(t),
320            Easing::BounceInOut => {
321                if t < 0.5 {
322                    (1.0 - bounce_out(1.0 - 2.0 * t)) / 2.0
323                } else {
324                    (1.0 + bounce_out(2.0 * t - 1.0)) / 2.0
325                }
326            }
327        }
328    }
329}
330
331/// Classic ease-out bounce curve (Godot TRANS_BOUNCE EASE_OUT).
332fn bounce_out(t: f32) -> f32 {
333    const N1: f32 = 7.5625;
334    const D1: f32 = 2.75;
335    if t < 1.0 / D1 {
336        N1 * t * t
337    } else if t < 2.0 / D1 {
338        let t = t - 1.5 / D1;
339        N1 * t * t + 0.75
340    } else if t < 2.5 / D1 {
341        let t = t - 2.25 / D1;
342        N1 * t * t + 0.9375
343    } else {
344        let t = t - 2.625 / D1;
345        N1 * t * t + 0.984375
346    }
347}
348
349/// Evaluate a cubic bezier with control points P1=(p1x,p1y), P2=(p2x,p2y)
350/// (P0=(0,0) and P3=(1,1) are fixed). Uses Newton's method (5 iterations)
351/// to find `u` such that x(u) = t, then returns y(u).
352fn eval_cubic_bezier(p1x: f32, p1y: f32, p2x: f32, p2y: f32, t: f32) -> f32 {
353    let t = t.clamp(0.0, 1.0);
354    if t <= 0.0 {
355        return 0.0;
356    }
357    if t >= 1.0 {
358        return 1.0;
359    }
360    let mut u = t;
361    for _ in 0..6 {
362        let omu = 1.0 - u;
363        let x = 3.0 * omu * omu * u * p1x + 3.0 * omu * u * u * p2x + u * u * u;
364        let dx = 3.0 * omu * omu * p1x + 6.0 * omu * u * (p2x - p1x) + 3.0 * u * u * (1.0 - p2x);
365        if dx.abs() < 1e-10 {
366            break;
367        }
368        u -= (x - t) / dx;
369        u = u.clamp(0.0, 1.0);
370    }
371    let omu = 1.0 - u;
372    3.0 * omu * omu * u * p1y + 3.0 * omu * u * u * p2y + u * u * u
373}
374
375/// Cubic Hermite spline interpolation. Given interval width `h`, normalized
376/// position `x` in [0,1], endpoint values `y1,y2` and their tangents `t1,t2`,
377/// returns the interpolated value.
378///
379/// Factored to reduce operation count (adapted from Compose's MonoSpline).
380fn hermite_interpolate(h: f32, x: f32, y1: f32, y2: f32, t1: f32, t2: f32) -> f32 {
381    let x2 = x * x;
382    let x3 = x2 * x;
383    h * t1 * (x - 2.0 * x2 + x3) + h * t2 * (x3 - x2) + y1 - (3.0 * x2 - 2.0 * x3) * (y1 - y2)
384}
385
386/// Derivative of cubic Hermite spline at normalized position `x`.
387#[allow(dead_code)]
388fn hermite_differential(h: f32, x: f32, y1: f32, y2: f32, t1: f32, t2: f32) -> f32 {
389    let x2 = x * x;
390    h * (t1 - 2.0 * x * (2.0 * t1 + t2) + 3.0 * (t1 + t2) * x2) - 6.0 * (x - x2) * (y1 - y2)
391}
392
393/// A monotone cubic Hermite spline for C1-continuous interpolation of `f32` values.
394///
395/// Uses the Fritsch–Carlson method to compute tangents that preserve monotonicity
396/// and prevent overshoot. Based on Android Compose's `MonoSpline`.
397#[derive(Clone, Debug)]
398pub struct MonoSpline {
399    times: Vec<f32>,
400    values: Vec<f32>,
401    tangents: Vec<f32>,
402}
403
404impl MonoSpline {
405    /// Build a spline from keyframe times and values.
406    /// Times must be sorted ascending and have at least 2 entries.
407    /// Values must have the same length as times.
408    ///
409    /// Degenerate segments (duplicate/unsorted/non-finite times) are coerced
410    /// to zero slope instead of producing `inf`/`NaN` tangents: dividing by a
411    /// zero `dt` in `f32` arithmetic does not panic, it silently corrupts every
412    /// later evaluation, so the constructor sanitizes here.
413    pub fn new(times: Vec<f32>, values: Vec<f32>) -> Self {
414        assert!(times.len() >= 2, "MonoSpline requires at least 2 keyframes");
415        assert_eq!(times.len(), values.len());
416        assert!(
417            times.iter().all(|t| t.is_finite()),
418            "MonoSpline times must be finite"
419        );
420        assert!(
421            values.iter().all(|v| v.is_finite()),
422            "MonoSpline values must be finite"
423        );
424        if cfg!(debug_assertions) {
425            for w in times.windows(2) {
426                if w[1] <= w[0] {
427                    log::warn!(
428                        "MonoSpline: times not strictly ascending; degenerate segments coerce to zero slope"
429                    );
430                    break;
431                }
432            }
433        }
434        let n = times.len();
435        let mut tangents = vec![0.0; n];
436
437        // Compute slopes for each segment
438        let mut slopes = vec![0.0; n.saturating_sub(1)];
439        for i in 0..n - 1 {
440            let dt = times[i + 1] - times[i];
441            if dt > 0.0 {
442                slopes[i] = (values[i + 1] - values[i]) / dt;
443            } else {
444                log::warn!("MonoSpline: non-positive dt at segment {i}; coercing slope to 0");
445                slopes[i] = 0.0;
446            }
447        }
448
449        // Tangents at interior knots: average of adjacent slopes
450        tangents[0] = slopes[0];
451        for i in 1..n - 1 {
452            tangents[i] = (slopes[i - 1] + slopes[i]) * 0.5;
453        }
454        tangents[n - 1] = slopes[n - 2];
455
456        // Fritsch–Carlson monotonicity preservation
457        for i in 0..n - 1 {
458            if slopes[i] == 0.0 {
459                tangents[i] = 0.0;
460                tangents[i + 1] = 0.0;
461            } else {
462                let a = tangents[i] / slopes[i];
463                let b = tangents[i + 1] / slopes[i];
464                let h = (a * a + b * b).sqrt();
465                if h > 9.0 {
466                    let t = 3.0 / h;
467                    tangents[i] = t * a * slopes[i];
468                    tangents[i + 1] = t * b * slopes[i];
469                }
470            }
471        }
472
473        Self {
474            times,
475            values,
476            tangents,
477        }
478    }
479
480    /// Evaluate the spline at time `t`.
481    /// Clamps `t` to the spline's time range. Extrapolates using the endpoint tangent.
482    pub fn evaluate(&self, t: f32) -> f32 {
483        let n = self.times.len();
484        let first = self.times[0];
485        let last = self.times[n - 1];
486
487        if t <= first {
488            return self.values[0] + (t - first) * self.tangents[0];
489        }
490        if t >= last {
491            return self.values[n - 1] + (t - last) * self.tangents[n - 1];
492        }
493
494        for i in 0..n - 1 {
495            if t >= self.times[i] && t <= self.times[i + 1] {
496                let h = self.times[i + 1] - self.times[i];
497                let x = (t - self.times[i]) / h;
498                return hermite_interpolate(
499                    h,
500                    x,
501                    self.values[i],
502                    self.values[i + 1],
503                    self.tangents[i],
504                    self.tangents[i + 1],
505                );
506            }
507        }
508
509        self.values[n - 1] // fallback
510    }
511}
512
513/// Returns (progress, velocity) at time `t` with initial conditions (x0, v0).
514fn spring_analytical(zeta: f32, stiffness: f32, t: f32, x0: f32, v0: f32) -> (f32, f32) {
515    if t <= 0.0 {
516        return (x0, v0);
517    }
518
519    let omega = if stiffness > 0.0 {
520        stiffness.sqrt()
521    } else {
522        return (x0 + v0 * t, v0);
523    };
524
525    let zeta = zeta.max(0.0);
526    let exp = (-zeta * omega * t).exp();
527    let a = 1.0 - x0; // amplitude coefficient
528
529    if (zeta - 1.0).abs() < 1e-6 {
530        // Critically damped: x(t) = 1 - (A + B*t) * e^{-ωt}
531        let b = omega * a - v0;
532        let progress = 1.0 - (a + b * t) * exp;
533        let velocity = (a * omega - b + b * omega * t) * exp;
534        (progress, velocity)
535    } else if zeta < 1.0 {
536        // Underdamped: x(t) = 1 - e^{-ζωt}[A*cos(ωd*t) + C*sin(ωd*t)]
537        let wd = omega * (1.0 - zeta * zeta).sqrt();
538        let c = (zeta * omega * a - v0) / wd;
539        let cos_wd = (wd * t).cos();
540        let sin_wd = (wd * t).sin();
541        let env = a * cos_wd + c * sin_wd;
542        let progress = 1.0 - exp * env;
543        let velocity =
544            exp * ((zeta * omega * a - wd * c) * cos_wd + (zeta * omega * c + wd * a) * sin_wd);
545        (progress, velocity)
546    } else {
547        // Overdamped: x(t) = 1 - e^{-ζωt}[A*cosh(ωd'*t) + D*sinh(ωd'*t)]
548        let wd = omega * (zeta * zeta - 1.0).sqrt();
549        let d = (zeta * omega * a - v0) / wd;
550        let cosh_wd = (wd * t).cosh();
551        let sinh_wd = (wd * t).sinh();
552        let env = a * cosh_wd + d * sinh_wd;
553        let progress = 1.0 - exp * env;
554        let velocity =
555            exp * ((zeta * omega * a - wd * d) * cosh_wd + (zeta * omega * d - wd * a) * sinh_wd);
556        (progress, velocity)
557    }
558}
559
560fn spring_underdamped_normalized(t: f32, zeta: f32, omega: f32) -> f32 {
561    let tt = t.max(0.0);
562    let z = zeta.clamp(0.0, 0.999);
563    let w = omega.max(0.0);
564    let wd = w * (1.0 - z * z).sqrt();
565    let exp_term = (-z * w * tt).exp();
566    let cos_term = (wd * tt).cos();
567    let sin_term = (wd * tt).sin();
568    // Standard second-order underdamped unit-step response
569    let c = z / (1.0 - z * z).sqrt();
570    let y = 1.0 - exp_term * (cos_term + c * sin_term);
571    y.clamp(0.0, 1.0)
572}
573
574#[derive(Clone, Copy, Debug)]
575pub struct AnimationSpec {
576    pub duration: Duration,
577    pub easing: Easing,
578    pub delay: Duration,
579    /// If set, use true physical spring simulation (duration is ignored, emergent from physics).
580    pub spring: Option<SpringSpec>,
581    /// If set, wrap the animation in repeat behavior (n iterations, optional ping-pong).
582    pub repeat: Option<RepeatableSpec>,
583}
584
585impl Default for AnimationSpec {
586    fn default() -> Self {
587        Self {
588            duration: Duration::from_millis(300),
589            easing: Easing::EaseInOut,
590            delay: Duration::ZERO,
591            spring: None,
592            repeat: None,
593        }
594    }
595}
596
597impl AnimationSpec {
598    pub fn tween(duration: Duration, easing: Easing) -> Self {
599        Self {
600            duration,
601            easing,
602            delay: Duration::ZERO,
603            spring: None,
604            repeat: None,
605        }
606    }
607    /// True physical spring simulation - duration is emergent, no fixed duration needed.
608    pub fn spring(spring: SpringSpec) -> Self {
609        Self {
610            duration: Duration::ZERO,
611            easing: Easing::Linear,
612            delay: Duration::ZERO,
613            spring: Some(spring),
614            repeat: None,
615        }
616    }
617    /// Gentle underdamped preset (small overshoot). Uses true spring physics.
618    pub fn spring_gentle() -> Self {
619        Self::spring(SpringSpec::gentle())
620    }
621    /// Bouncier underdamped preset. Uses true spring physics.
622    pub fn spring_bouncy() -> Self {
623        Self::spring(SpringSpec::bouncy())
624    }
625    /// Critically damped spring with given omega (angular frequency). Uses true spring physics.
626    pub fn spring_crit(omega: f32) -> Self {
627        Self::spring(SpringSpec::new(1.0, omega * omega))
628    }
629
630    pub fn fast() -> Self {
631        Self {
632            duration: Duration::from_millis(150),
633            easing: Easing::EaseOut,
634            delay: Duration::ZERO,
635            spring: None,
636            repeat: None,
637        }
638    }
639
640    pub fn slow() -> Self {
641        Self {
642            duration: Duration::from_millis(600),
643            easing: Easing::EaseInOut,
644            delay: Duration::ZERO,
645            spring: None,
646            repeat: None,
647        }
648    }
649
650    /// Wrap this spec in a repeatable animation.
651    /// Pass `RepeatableSpec::infinite()` for infinite repeats.
652    pub fn repeated(mut self, repeat: RepeatableSpec) -> Self {
653        self.repeat = Some(repeat);
654        self
655    }
656}
657
658/// A keyframe animation specification.
659///
660/// Defines a sequence of keyframes at specific timestamps (0.0 to 1.0),
661/// with target values and optional easing between each pair.
662#[derive(Clone, Debug)]
663pub struct KeyframesSpec<T: Clone> {
664    /// Keyframes as (timestamp 0.0-1.0, value, optional easing between previous and this).
665    /// The first keyframe should be at t=0.0 and uses no easing.
666    pub keyframes: Vec<(f32, T, Option<Easing>)>,
667}
668
669impl<T: Clone + Interpolate> KeyframesSpec<T> {
670    pub fn new(keyframes: Vec<(f32, T)>) -> Self {
671        assert!(!keyframes.is_empty(), "KeyframesSpec must not be empty");
672        let with_easing = keyframes.into_iter().map(|(t, v)| (t, v, None)).collect();
673        Self {
674            keyframes: with_easing,
675        }
676    }
677
678    /// Add easing between the previous keyframe and this one.
679    pub fn with_easing(mut self, easing: Easing) -> Self {
680        if let Some(last) = self.keyframes.last_mut() {
681            last.2 = Some(easing);
682        }
683        self
684    }
685
686    pub fn evaluate(&self, t: f32) -> T {
687        let t = t.clamp(0.0, 1.0);
688        let kf = &self.keyframes;
689        if kf.is_empty() {
690            panic!("KeyframesSpec must have at least one keyframe");
691        }
692
693        // Linear interpolation (with per-segment easing)
694        for i in 0..kf.len() - 1 {
695            let (t0, _, _) = kf[i];
696            let (t1, ref v1, easing) = kf[i + 1];
697            if t >= t0 && t <= t1 {
698                let segment_t = if (t1 - t0).abs() < f32::EPSILON {
699                    1.0
700                } else {
701                    (t - t0) / (t1 - t0)
702                };
703                let eased_t = match easing {
704                    Some(e) => e.interpolate(segment_t),
705                    None => segment_t,
706                };
707                return kf[i].1.interpolate(v1, eased_t);
708            }
709        }
710        kf.last().unwrap().1.clone()
711    }
712}
713
714/// A keyframe animation specification with smooth cubic Hermite spline interpolation.
715///
716/// Provides C1 continuity (smooth derivatives at keyframe boundaries),
717/// unlike `KeyframesSpec` which uses C0 linear interpolation.
718///
719/// Uses the Fritsch–Carlson monotonicity-preserving Hermite spline
720#[derive(Clone, Debug)]
721pub struct SplineKeyframes {
722    spline: MonoSpline,
723}
724
725impl SplineKeyframes {
726    /// Build a spline keyframe from time/value pairs.
727    ///
728    /// Times should be in [0.0, 1.0] and sorted ascending.
729    /// At least 2 keyframes are required.
730    pub fn new(keyframes: Vec<(f32, f32)>) -> Self {
731        assert!(
732            keyframes.len() >= 2,
733            "SplineKeyframes requires at least 2 keyframes"
734        );
735        let times: Vec<f32> = keyframes.iter().map(|(t, _)| *t).collect();
736        let values: Vec<f32> = keyframes.iter().map(|(_, v)| *v).collect();
737        Self {
738            spline: MonoSpline::new(times, values),
739        }
740    }
741
742    /// Evaluate the spline at normalized time `t` (0.0 to 1.0).
743    pub fn evaluate(&self, t: f32) -> f32 {
744        self.spline.evaluate(t.clamp(0.0, 1.0))
745    }
746
747    /// Stable fingerprint of the keyframe times/values for change detection.
748    /// Used by `animate_spline_keyframes` to restart when keyframes change
749    /// without exposing the private spline internals.
750    pub fn fingerprint(&self) -> String {
751        format!("{:?}", self.spline)
752    }
753}
754
755/// A repeatable animation specification.
756///
757/// Wraps another animation spec and causes it to repeat.
758/// Default: infinite repeat with no reverse.
759#[derive(Clone, Copy, Debug)]
760pub struct RepeatableSpec {
761    /// Number of repetitions. `None` means infinite.
762    pub iterations: Option<u32>,
763    /// If true, alternate direction each iteration (forward, backward, forward...).
764    pub reverse: bool,
765    /// Delay between each iteration.
766    pub delay_between: Duration,
767}
768
769impl Default for RepeatableSpec {
770    fn default() -> Self {
771        Self {
772            iterations: None,
773            reverse: false,
774            delay_between: Duration::ZERO,
775        }
776    }
777}
778
779impl RepeatableSpec {
780    pub fn new(iterations: u32) -> Self {
781        Self {
782            iterations: Some(iterations),
783            reverse: false,
784            delay_between: Duration::ZERO,
785        }
786    }
787
788    pub fn infinite() -> Self {
789        Self {
790            iterations: None,
791            reverse: false,
792            delay_between: Duration::ZERO,
793        }
794    }
795
796    pub fn reverse(mut self) -> Self {
797        self.reverse = true;
798        self
799    }
800
801    pub fn delay_between(mut self, d: Duration) -> Self {
802        self.delay_between = d;
803        self
804    }
805}
806
807fn same_easing(a: Easing, b: Easing) -> bool {
808    match (a, b) {
809        (Easing::SpringCrit { omega: a }, Easing::SpringCrit { omega: b }) => {
810            a.to_bits() == b.to_bits()
811        }
812        (Easing::Custom(a), Easing::Custom(b)) => {
813            a.p1x.to_bits() == b.p1x.to_bits()
814                && a.p1y.to_bits() == b.p1y.to_bits()
815                && a.p2x.to_bits() == b.p2x.to_bits()
816                && a.p2y.to_bits() == b.p2y.to_bits()
817        }
818        (a, b) => std::mem::discriminant(&a) == std::mem::discriminant(&b),
819    }
820}
821
822fn same_spring(a: SpringSpec, b: SpringSpec) -> bool {
823    a.damping_ratio.to_bits() == b.damping_ratio.to_bits()
824        && a.stiffness.to_bits() == b.stiffness.to_bits()
825        && a.settle_progress.to_bits() == b.settle_progress.to_bits()
826        && a.settle_velocity.to_bits() == b.settle_velocity.to_bits()
827}
828
829fn same_repeat(a: Option<RepeatableSpec>, b: Option<RepeatableSpec>) -> bool {
830    match (a, b) {
831        (None, None) => true,
832        (Some(a), Some(b)) => {
833            a.iterations == b.iterations
834                && a.reverse == b.reverse
835                && a.delay_between == b.delay_between
836        }
837        _ => false,
838    }
839}
840
841fn same_animation_spec(a: &AnimationSpec, b: &AnimationSpec) -> bool {
842    a.duration == b.duration
843        && same_easing(a.easing, b.easing)
844        && a.delay == b.delay
845        && match (a.spring, b.spring) {
846            (None, None) => true,
847            (Some(a), Some(b)) => same_spring(a, b),
848            _ => false,
849        }
850        && same_repeat(a.repeat, b.repeat)
851}
852
853/// Decay animation configuration.
854///
855/// Models a damped decay (e.g., for fling-to-stop animations).
856#[derive(Clone, Copy, Debug)]
857pub struct DecayAnimationSpec {
858    /// How quickly the animation decelerates. Lower = faster stop.
859    pub friction: f32,
860    /// Minimum velocity threshold to stop.
861    pub stop_threshold: f32,
862}
863
864impl Default for DecayAnimationSpec {
865    fn default() -> Self {
866        Self {
867            friction: 0.8,
868            stop_threshold: 1.0,
869        }
870    }
871}
872
873impl DecayAnimationSpec {
874    pub fn new(friction: f32) -> Self {
875        Self {
876            friction: friction.clamp(0.01, 1.0),
877            stop_threshold: 1.0,
878        }
879    }
880}
881
882impl AnimatedValue<f32> {
883    pub fn set_target_with_velocity(&mut self, target: f32, velocity: f32) {
884        let start = self.current;
885        let distance = target - start;
886        if distance.abs() <= f32::EPSILON {
887            self.snap_to(target);
888            return;
889        }
890        self.set_target(target);
891        self.spring_v0 = if velocity.is_finite() {
892            velocity / distance
893        } else {
894            0.0
895        };
896        self.velocity = self.spring_v0;
897    }
898
899    pub fn current_velocity(&self) -> f32 {
900        if self.start_time.is_none() {
901            0.0
902        } else {
903            self.velocity * (self.target - self.start)
904        }
905    }
906
907    /// Tick the decay animation. Returns `true` if still animating.
908    pub fn update_decay(&mut self, friction: f32, stop_threshold: f32) -> bool {
909        let _start = match self.start_time {
910            Some(s) => s,
911            None => return false,
912        };
913
914        let now = now();
915        let dt = match self.last_update {
916            Some(last) => now.saturating_duration_since(last).as_secs_f32().min(0.05),
917            None => 0.0,
918        };
919        self.last_update = Some(now);
920
921        if dt <= 0.0 {
922            return true;
923        }
924
925        if self.velocity.abs() < stop_threshold {
926            self.velocity = 0.0;
927            self.start_time = None;
928            return false;
929        }
930
931        self.velocity *= friction.powf(dt * 60.0);
932        let delta = self.velocity * dt;
933        // We store the "current value" as a single f32 offset
934        // that accumulates. But AnimatedValue<f32> stores explicit
935        // start/target. For decay we just accumulate the current.
936        // Because of the AnimatedValue structure, we use progress as
937        // the accumulated value relative to start.
938        let new_progress = self.progress + delta;
939        self.progress = new_progress;
940        // current = start + (target - start) * progress but target = ???.
941        // For decay, progress IS the value (starting from 0).
942        // We repurpose: current = start + progress (progress is offset from start).
943        // Since T = f32, we can just set current directly.
944        if self.progress.abs() < 0.001 && self.velocity.abs() < stop_threshold {
945            self.progress = 0.0;
946            self.velocity = 0.0;
947            self.start_time = None;
948            self.current = self.start;
949            return false;
950        }
951
952        // Clamp progress to avoid runaway extrapolation for non-f32 interpolations (shouldn't happen as this is f32-only).
953        let clamped = self.progress.clamp(-1e6, 1e6);
954        self.current = self.start + clamped;
955        true
956    }
957}
958
959pub trait Interpolate {
960    fn interpolate(&self, other: &Self, t: f32) -> Self;
961}
962
963impl Interpolate for f32 {
964    fn interpolate(&self, other: &Self, t: f32) -> Self {
965        self + (other - self) * t
966    }
967}
968
969impl Interpolate for crate::Color {
970    fn interpolate(&self, other: &Self, t: f32) -> Self {
971        let lerp = |a: u8, b: u8| {
972            (a as f32 + (b as f32 - a as f32) * t)
973                .round()
974                .clamp(0.0, 255.0) as u8
975        };
976        crate::Color(
977            lerp(self.0, other.0),
978            lerp(self.1, other.1),
979            lerp(self.2, other.2),
980            lerp(self.3, other.3),
981        )
982    }
983}
984
985impl Interpolate for crate::Vec2 {
986    fn interpolate(&self, other: &Self, t: f32) -> Self {
987        crate::Vec2 {
988            x: self.x.interpolate(&other.x, t),
989            y: self.y.interpolate(&other.y, t),
990        }
991    }
992}
993
994impl Interpolate for crate::Size {
995    fn interpolate(&self, other: &Self, t: f32) -> Self {
996        crate::Size {
997            width: self.width.interpolate(&other.width, t),
998            height: self.height.interpolate(&other.height, t),
999        }
1000    }
1001}
1002
1003impl Interpolate for crate::Rect {
1004    fn interpolate(&self, other: &Self, t: f32) -> Self {
1005        crate::Rect {
1006            x: self.x.interpolate(&other.x, t),
1007            y: self.y.interpolate(&other.y, t),
1008            w: self.w.interpolate(&other.w, t),
1009            h: self.h.interpolate(&other.h, t),
1010        }
1011    }
1012}
1013
1014// Animation clock
1015pub trait Clock: Send + Sync + 'static {
1016    fn now(&self) -> Instant;
1017}
1018
1019pub struct SystemClock;
1020impl Clock for SystemClock {
1021    fn now(&self) -> Instant {
1022        Instant::now()
1023    }
1024}
1025
1026thread_local! {
1027    static CLOCK: RefCell<Rc<dyn Clock>> = RefCell::new(Rc::new(SystemClock));
1028}
1029
1030/// Install a per-thread animation clock.
1031pub fn set_clock(clock: Box<dyn Clock>) {
1032    let old = CLOCK.with(|slot| std::mem::replace(&mut *slot.borrow_mut(), Rc::from(clock)));
1033    drop(old);
1034}
1035/// Ensure a system clock is installed on this thread (always present since thread_local initializes it).
1036pub fn ensure_system_clock() {
1037    // Already initialized by thread_local default - no-op.
1038}
1039
1040/// A test clock you can drive deterministically.
1041#[derive(Clone)]
1042pub struct TestClock {
1043    pub t: Instant,
1044}
1045impl Clock for TestClock {
1046    fn now(&self) -> Instant {
1047        self.t
1048    }
1049}
1050
1051/// Animated value that transitions smoothly.
1052///
1053/// Supports two modes:
1054/// - **Tween** (when `spec.spring` is `None`): interpolates between `start` and `target`
1055///   over a fixed duration using an easing curve.
1056/// - **Spring** (when `spec.spring` is `Some`): analytically evaluates a physical spring ODE
1057///   (`x'' = -k·(x - target) - d·x'`) with emergent duration. When the target changes
1058///   mid-animation, the current value and velocity carry forward seamlessly.
1059pub struct AnimatedValue<T: Interpolate + Clone> {
1060    current: T,
1061    target: T,
1062    start: T,
1063    spec: AnimationSpec,
1064    keyframes: Option<KeyframesSpec<T>>,
1065    iteration: u32,
1066    start_time: Option<Instant>,
1067    // Spring simulation state (progress-based, works for any T: Interpolate)
1068    progress: f32,
1069    velocity: f32,
1070    /// Initial velocity for the current spring segment (carry-over from target changes).
1071    spring_v0: f32,
1072    last_update: Option<Instant>,
1073    delay_until: Option<Instant>,
1074    delay_applied: bool,
1075    reverse: bool,
1076}
1077
1078impl<T: Interpolate + Clone> AnimatedValue<T> {
1079    pub fn new(initial: T, spec: AnimationSpec) -> Self {
1080        Self {
1081            current: initial.clone(),
1082            target: initial.clone(),
1083            start: initial,
1084            spec,
1085            keyframes: None,
1086            iteration: 0,
1087            start_time: None,
1088            progress: 1.0,
1089            velocity: 0.0,
1090            spring_v0: 0.0,
1091            last_update: None,
1092            delay_until: None,
1093            delay_applied: false,
1094            reverse: false,
1095        }
1096    }
1097
1098    pub fn set_spec(&mut self, spec: AnimationSpec) {
1099        assert!(
1100            self.keyframes.is_none() || spec.spring.is_none(),
1101            "spring animations cannot use keyframes"
1102        );
1103        let changed = !same_animation_spec(&self.spec, &spec);
1104        self.spec = spec;
1105        if changed {
1106            self.iteration = 0;
1107            self.reverse = false;
1108            if self.start_time.is_some() || self.delay_until.is_some() {
1109                self.start = self.current.clone();
1110                self.start_time = Some(now());
1111                self.last_update = None;
1112                self.delay_until = None;
1113                self.delay_applied = false;
1114                self.progress = 0.0;
1115                self.spring_v0 = self.velocity;
1116            }
1117        }
1118    }
1119
1120    /// Set a keyframes spec for multi-stage animation.
1121    /// When set, `set_target` is ignored and the value is driven by the keyframe sequence.
1122    pub fn set_keyframes(&mut self, keyframes: KeyframesSpec<T>) {
1123        assert!(
1124            !keyframes.keyframes.is_empty(),
1125            "KeyframesSpec must not be empty"
1126        );
1127        assert!(
1128            self.spec.spring.is_none(),
1129            "spring animations cannot use keyframes"
1130        );
1131        self.keyframes = Some(keyframes);
1132        self.start_time = Some(now());
1133        self.last_update = None;
1134        self.delay_until = None;
1135        self.delay_applied = false;
1136        self.iteration = 0;
1137        self.reverse = false;
1138    }
1139
1140    pub fn set_target(&mut self, target: T) {
1141        // Don't call self.update() here -> self.spec may have been changed by the
1142        // caller before set_target (e.g. set_spec -> set_target). The driver's
1143        // tick() already advanced all animations before composition, so
1144        // self.current is already up to date.
1145        self.keyframes = None;
1146        self.start = self.current.clone();
1147        self.target = target;
1148        self.start_time = Some(now());
1149        self.last_update = None;
1150        self.delay_until = None;
1151        self.delay_applied = false;
1152        self.iteration = 0;
1153        self.reverse = false;
1154        if self.spec.spring.is_some() {
1155            // Spring mode: start progress at 0 (the current value), carry velocity forward
1156            self.progress = 0.0;
1157            self.spring_v0 = self.velocity;
1158        }
1159    }
1160
1161    /// Snap immediately to a value without animating.
1162    pub fn snap_to(&mut self, value: T) {
1163        self.current = value.clone();
1164        self.target = value.clone();
1165        self.start = value;
1166        self.keyframes = None;
1167        self.start_time = None;
1168        self.progress = 1.0;
1169        self.velocity = 0.0;
1170        self.spring_v0 = 0.0;
1171        self.last_update = None;
1172        self.delay_until = None;
1173        self.delay_applied = true;
1174        self.iteration = 0;
1175        self.reverse = false;
1176    }
1177
1178    pub fn update(&mut self) -> bool {
1179        if self.waiting_for_repeat() {
1180            return true;
1181        }
1182        let spring_spec = self.spec.spring;
1183        let mut still = if let Some(spring) = spring_spec {
1184            self.update_spring(&spring)
1185        } else if self.keyframes.is_some() {
1186            self.update_keyframes()
1187        } else {
1188            self.update_tween()
1189        };
1190
1191        if !still && let Some(repeat) = self.spec.repeat {
1192            let maxed = repeat
1193                .iterations
1194                .is_some_and(|max| self.iteration + 1 >= max);
1195            if !maxed {
1196                self.iteration += 1;
1197                self.start_time = None;
1198                self.last_update = None;
1199                self.progress = 0.0;
1200                self.velocity = 0.0;
1201                self.spring_v0 = 0.0;
1202                self.delay_applied = true;
1203                self.reverse = repeat.reverse && !self.reverse;
1204                if self.keyframes.is_none() && repeat.reverse {
1205                    std::mem::swap(&mut self.start, &mut self.target);
1206                }
1207                let start = now();
1208                if repeat.delay_between.is_zero() {
1209                    self.start_time = Some(start);
1210                } else {
1211                    self.delay_until = Some(start + repeat.delay_between);
1212                }
1213                still = true;
1214            }
1215        }
1216
1217        still
1218    }
1219
1220    fn waiting_for_repeat(&mut self) -> bool {
1221        let Some(until) = self.delay_until else {
1222            return false;
1223        };
1224        let current = now();
1225        if current < until {
1226            return true;
1227        }
1228        self.delay_until = None;
1229        self.start_time = Some(until);
1230        self.delay_applied = true;
1231        self.last_update = None;
1232        false
1233    }
1234
1235    fn update_keyframes(&mut self) -> bool {
1236        let start = match self.start_time {
1237            Some(s) => s,
1238            None => return false,
1239        };
1240        let elapsed = now().saturating_duration_since(start);
1241        let delay = if self.delay_applied {
1242            Duration::ZERO
1243        } else {
1244            self.spec.delay
1245        };
1246        if elapsed < delay {
1247            return true;
1248        }
1249        self.delay_applied = true;
1250        let animation_time = elapsed - delay;
1251        if animation_time >= self.spec.duration || self.spec.duration.is_zero() {
1252            if let Some(ref kf) = self.keyframes {
1253                self.current = kf.evaluate(if self.reverse { 0.0 } else { 1.0 });
1254            }
1255            self.start_time = None;
1256            return false;
1257        }
1258        let t = (animation_time.as_secs_f32() / self.spec.duration.as_secs_f32()).clamp(0.0, 1.0);
1259        let eased_t = self.spec.easing.interpolate(t);
1260        if let Some(ref kf) = self.keyframes {
1261            let sample_t = if self.reverse { 1.0 - eased_t } else { eased_t };
1262            self.current = kf.evaluate(sample_t);
1263        }
1264        true
1265    }
1266
1267    fn update_spring(&mut self, spring: &SpringSpec) -> bool {
1268        let start = match self.start_time {
1269            Some(s) => s,
1270            None => return false,
1271        };
1272
1273        let now = now();
1274        let elapsed = now.saturating_duration_since(start);
1275
1276        let delay = if self.delay_applied {
1277            Duration::ZERO
1278        } else {
1279            self.spec.delay
1280        };
1281        if elapsed < delay {
1282            return true;
1283        }
1284        self.delay_applied = true;
1285
1286        let t = (elapsed - delay).as_secs_f32().max(0.0);
1287        let (progress, velocity) = spring_analytical(
1288            spring.damping_ratio,
1289            spring.stiffness,
1290            t,
1291            0.0,
1292            self.spring_v0,
1293        );
1294        let progress = progress.clamp(-0.1, 2.0);
1295        if !progress.is_finite() || !velocity.is_finite() {
1296            self.progress = 1.0;
1297            self.velocity = 0.0;
1298            self.spring_v0 = 0.0;
1299            self.current = self.target.clone();
1300            self.start_time = None;
1301            self.last_update = None;
1302            return false;
1303        }
1304
1305        if (progress - 1.0).abs() < spring.settle_progress
1306            && velocity.abs() < spring.settle_velocity
1307        {
1308            self.progress = 1.0;
1309            self.velocity = 0.0;
1310            self.spring_v0 = 0.0;
1311            self.current = self.target.clone();
1312            self.start_time = None;
1313            self.last_update = None;
1314            return false;
1315        }
1316
1317        self.progress = progress;
1318        self.velocity = velocity;
1319        self.current = self.start.interpolate(&self.target, self.progress);
1320        true
1321    }
1322
1323    fn update_tween(&mut self) -> bool {
1324        if let Some(start) = self.start_time {
1325            let elapsed = now().saturating_duration_since(start);
1326            let delay = if self.delay_applied {
1327                Duration::ZERO
1328            } else {
1329                self.spec.delay
1330            };
1331
1332            if elapsed < delay {
1333                return true;
1334            }
1335            self.delay_applied = true;
1336
1337            let animation_time = elapsed - delay;
1338
1339            if animation_time >= self.spec.duration || self.spec.duration.is_zero() {
1340                self.current = self.target.clone();
1341                self.start_time = None;
1342                return false;
1343            }
1344
1345            let t =
1346                (animation_time.as_secs_f32() / self.spec.duration.as_secs_f32()).clamp(0.0, 1.0);
1347            let eased_t = self.spec.easing.interpolate(t);
1348
1349            self.current = self.start.interpolate(&self.target, eased_t);
1350            true
1351        } else {
1352            false
1353        }
1354    }
1355
1356    pub fn get(&self) -> &T {
1357        &self.current
1358    }
1359
1360    pub fn is_animating(&self) -> bool {
1361        self.start_time.is_some() || self.delay_until.is_some()
1362    }
1363
1364    pub fn has_keyframes(&self) -> bool {
1365        self.keyframes.is_some()
1366    }
1367}
1368
1369#[cfg(test)]
1370mod tests {
1371    use super::*;
1372
1373    fn assert_in_out(ease: Easing) {
1374        // All sane eases pass through (0,0) and (1,1).
1375        assert!((ease.interpolate(0.0) - 0.0).abs() < 1e-4, "{ease:?} in(0)");
1376        assert!((ease.interpolate(1.0) - 1.0).abs() < 1e-4, "{ease:?} in(1)");
1377    }
1378
1379    #[test]
1380    fn godot_eases_pass_through_endpoints() {
1381        use Easing::*;
1382        for ease in [
1383            CubicIn,
1384            CubicOut,
1385            CubicInOut,
1386            QuartIn,
1387            QuartOut,
1388            QuartInOut,
1389            QuintIn,
1390            QuintOut,
1391            QuintInOut,
1392            SineIn,
1393            SineOut,
1394            SineInOut,
1395            ExpoIn,
1396            ExpoOut,
1397            ExpoInOut,
1398            CircIn,
1399            CircOut,
1400            CircInOut,
1401            BackIn,
1402            BackOut,
1403            BackInOut,
1404            ElasticIn,
1405            ElasticOut,
1406            ElasticInOut,
1407            BounceIn,
1408            BounceOut,
1409            BounceInOut,
1410        ] {
1411            assert_in_out(ease);
1412        }
1413    }
1414
1415    #[test]
1416    fn easing_direction_is_sane() {
1417        use Easing::*;
1418        let mid = [CubicOut, QuartOut, QuintOut, SineOut, ExpoOut, CircOut];
1419        for e in mid {
1420            assert!(e.interpolate(0.5) <= 1.0, "{e:?} stays below 1 at mid");
1421            assert!(e.interpolate(0.5) > 0.5, "{e:?} is ease-out at mid");
1422        }
1423        for e in [CubicIn, QuartIn, QuintIn, SineIn, ExpoIn, CircIn] {
1424            assert!(e.interpolate(0.5) < 0.5, "{e:?} is ease-in at mid");
1425        }
1426        // Overshoot eases exceed the [0,1] band in the middle.
1427        for e in [BackOut, ElasticOut] {
1428            assert!(e.interpolate(0.5) > 1.0, "{e:?} overshoots");
1429        }
1430        for e in [BackIn, ElasticIn] {
1431            assert!(e.interpolate(0.5) < 0.0, "{e:?} undershoots");
1432        }
1433    }
1434
1435    #[test]
1436    fn ease_kind_endpoints_hold() {
1437        for ease in [
1438            EaseKind::Linear,
1439            EaseKind::QuadOut,
1440            EaseKind::CubicOut,
1441            EaseKind::BackOut,
1442        ] {
1443            assert!(ease.apply(0.0).abs() < 1e-4);
1444            assert!((ease.apply(1.0) - 1.0).abs() < 1e-4);
1445        }
1446        assert!(EaseKind::QuadOut.apply(0.5) > 0.5);
1447    }
1448
1449    #[test]
1450    fn bounce_matches_known_values() {
1451        use Easing::*;
1452        assert!((BounceOut.interpolate(0.0) - 0.0).abs() < 1e-4);
1453        assert!((BounceOut.interpolate(1.0) - 1.0).abs() < 1e-4);
1454        // Bounce out reaches its first apex (1.0) at t = 1/2.75, then settles.
1455        assert!((BounceOut.interpolate(1.0 / 2.75) - 1.0).abs() < 1e-4);
1456        assert!((BounceOut.interpolate(0.5) - 0.765625).abs() < 1e-4);
1457    }
1458
1459    #[test]
1460    fn monospline_duplicate_times_stay_finite() {
1461        // Regression: duplicate times divided by zero dt, yielding inf/NaN
1462        // tangents that silently corrupted every later evaluation.
1463        let s = MonoSpline::new(vec![0.0, 0.5, 0.5, 1.0], vec![0.0, 1.0, 1.0, 2.0]);
1464        for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
1465            assert!(s.evaluate(t).is_finite(), "t={t}");
1466        }
1467    }
1468
1469    #[test]
1470    fn scroll_offset_sanitizes_nan() {
1471        // Regression: `off.clamp(0,max)` propagates NaN into the signal.
1472        let st = crate::scroll::ScrollState::new();
1473        st.set_viewport_height(100.0);
1474        st.set_content_height(1000.0);
1475        st.set_offset(f32::NAN);
1476        assert!(st.get().is_finite());
1477        assert_eq!(st.get(), 0.0);
1478        let leftover = st.scroll_immediate(f32::NAN);
1479        assert!(leftover.is_finite());
1480        assert!(st.get().is_finite());
1481    }
1482}