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