Skip to main content

repose_core/
animation.rs

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