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            self.current = self.start;
814            return false;
815        }
816
817        // Clamp progress to avoid runaway extrapolation for non-f32 interpolations (shouldn't happen as this is f32-only).
818        let clamped = self.progress.clamp(-1e6, 1e6);
819        self.current = self.start + clamped;
820        true
821    }
822}
823
824pub trait Interpolate {
825    fn interpolate(&self, other: &Self, t: f32) -> Self;
826}
827
828impl Interpolate for f32 {
829    fn interpolate(&self, other: &Self, t: f32) -> Self {
830        self + (other - self) * t
831    }
832}
833
834impl Interpolate for crate::Color {
835    fn interpolate(&self, other: &Self, t: f32) -> Self {
836        let lerp = |a: u8, b: u8| {
837            (a as f32 + (b as f32 - a as f32) * t)
838                .round()
839                .clamp(0.0, 255.0) as u8
840        };
841        crate::Color(
842            lerp(self.0, other.0),
843            lerp(self.1, other.1),
844            lerp(self.2, other.2),
845            lerp(self.3, other.3),
846        )
847    }
848}
849
850impl Interpolate for crate::Vec2 {
851    fn interpolate(&self, other: &Self, t: f32) -> Self {
852        crate::Vec2 {
853            x: self.x.interpolate(&other.x, t),
854            y: self.y.interpolate(&other.y, t),
855        }
856    }
857}
858
859impl Interpolate for crate::Size {
860    fn interpolate(&self, other: &Self, t: f32) -> Self {
861        crate::Size {
862            width: self.width.interpolate(&other.width, t),
863            height: self.height.interpolate(&other.height, t),
864        }
865    }
866}
867
868impl Interpolate for crate::Rect {
869    fn interpolate(&self, other: &Self, t: f32) -> Self {
870        crate::Rect {
871            x: self.x.interpolate(&other.x, t),
872            y: self.y.interpolate(&other.y, t),
873            w: self.w.interpolate(&other.w, t),
874            h: self.h.interpolate(&other.h, t),
875        }
876    }
877}
878
879// Animation clock
880pub trait Clock: Send + Sync + 'static {
881    fn now(&self) -> Instant;
882}
883
884pub struct SystemClock;
885impl Clock for SystemClock {
886    fn now(&self) -> Instant {
887        Instant::now()
888    }
889}
890
891thread_local! {
892    static CLOCK: RefCell<Box<dyn Clock>> = RefCell::new(Box::new(SystemClock) as Box<dyn Clock>);
893}
894
895/// Install a per-thread animation clock.
896pub fn set_clock(clock: Box<dyn Clock>) {
897    CLOCK.with(|c| *c.borrow_mut() = clock);
898}
899/// Ensure a system clock is installed on this thread (always present since thread_local initializes it).
900pub fn ensure_system_clock() {
901    // Already initialized by thread_local default - no-op.
902}
903
904/// A test clock you can drive deterministically.
905#[derive(Clone)]
906pub struct TestClock {
907    pub t: Instant,
908}
909impl Clock for TestClock {
910    fn now(&self) -> Instant {
911        self.t
912    }
913}
914
915/// Animated value that transitions smoothly.
916///
917/// Supports two modes:
918/// - **Tween** (when `spec.spring` is `None`): interpolates between `start` and `target`
919///   over a fixed duration using an easing curve.
920/// - **Spring** (when `spec.spring` is `Some`): numerically integrates a physical spring ODE
921///   (`x'' = -k·(x - target) - d·x'`) with emergent duration. When the target changes
922///   mid-animation, the current value and velocity carry forward seamlessly.
923pub struct AnimatedValue<T: Interpolate + Clone> {
924    current: T,
925    target: T,
926    start: T,
927    spec: AnimationSpec,
928    keyframes: Option<KeyframesSpec<T>>,
929    iteration: u32,
930    start_time: Option<Instant>,
931    // Spring simulation state (progress-based, works for any T: Interpolate)
932    progress: f32,
933    velocity: f32,
934    /// Initial velocity for the current spring segment (carry-over from target changes).
935    spring_v0: f32,
936    last_update: Option<Instant>,
937}
938
939impl<T: Interpolate + Clone> AnimatedValue<T> {
940    pub fn new(initial: T, spec: AnimationSpec) -> Self {
941        Self {
942            current: initial.clone(),
943            target: initial.clone(),
944            start: initial,
945            spec,
946            keyframes: None,
947            iteration: 0,
948            start_time: None,
949            progress: 1.0,
950            velocity: 0.0,
951            spring_v0: 0.0,
952            last_update: None,
953        }
954    }
955
956    pub fn set_spec(&mut self, spec: AnimationSpec) {
957        self.spec = spec;
958    }
959
960    /// Set a keyframes spec for multi-stage animation.
961    /// When set, `set_target` is ignored and the value is driven by the keyframe sequence.
962    pub fn set_keyframes(&mut self, keyframes: KeyframesSpec<T>) {
963        self.keyframes = Some(keyframes);
964        self.start_time = Some(now());
965        self.last_update = None;
966        self.iteration = 0;
967    }
968
969    pub fn set_target(&mut self, target: T) {
970        // Don't call self.update() here -> self.spec may have been changed by the
971        // caller before set_target (e.g. set_spec -> set_target). The driver's
972        // tick() already advanced all animations before composition, so
973        // self.current is already up to date.
974        self.keyframes = None;
975        self.start = self.current.clone();
976        self.target = target;
977        self.start_time = Some(now());
978        self.last_update = None;
979        self.iteration = 0;
980        if self.spec.spring.is_some() {
981            // Spring mode: start progress at 0 (the current value), carry velocity forward
982            self.progress = 0.0;
983            self.spring_v0 = self.velocity;
984        }
985    }
986
987    /// Snap immediately to a value without animating.
988    pub fn snap_to(&mut self, value: T) {
989        self.current = value.clone();
990        self.target = value.clone();
991        self.start = value;
992        self.keyframes = None;
993        self.start_time = None;
994        self.progress = 1.0;
995        self.velocity = 0.0;
996        self.spring_v0 = 0.0;
997        self.last_update = None;
998    }
999
1000    pub fn update(&mut self) -> bool {
1001        let spring_spec = self.spec.spring;
1002        let mut still = if let Some(spring) = spring_spec {
1003            self.update_spring(&spring)
1004        } else if self.keyframes.is_some() {
1005            self.update_keyframes()
1006        } else {
1007            self.update_tween()
1008        };
1009
1010        if !still {
1011            if let Some(repeat) = &self.spec.repeat {
1012                let maxed = repeat
1013                    .iterations
1014                    .is_some_and(|max| self.iteration + 1 >= max);
1015                if !maxed {
1016                    self.iteration += 1;
1017                    if repeat.reverse {
1018                        std::mem::swap(&mut self.start, &mut self.target);
1019                    }
1020                    self.progress = 0.0;
1021                    self.velocity = 0.0;
1022                    self.start_time = Some(now());
1023                    self.last_update = None;
1024                    still = true;
1025                }
1026            }
1027        }
1028
1029        still
1030    }
1031
1032    fn update_keyframes(&mut self) -> bool {
1033        let start = match self.start_time {
1034            Some(s) => s,
1035            None => return false,
1036        };
1037        let elapsed = now().saturating_duration_since(start);
1038        if elapsed < self.spec.delay {
1039            return true;
1040        }
1041        let animation_time = elapsed - self.spec.delay;
1042        if animation_time >= self.spec.duration {
1043            if let Some(ref kf) = self.keyframes {
1044                self.current = kf.evaluate(1.0);
1045            }
1046            self.start_time = None;
1047            return false;
1048        }
1049        let t = (animation_time.as_secs_f32() / self.spec.duration.as_secs_f32()).clamp(0.0, 1.0);
1050        let eased_t = self.spec.easing.interpolate(t).clamp(0.0, 1.0);
1051        if let Some(ref kf) = self.keyframes {
1052            self.current = kf.evaluate(eased_t);
1053        }
1054        true
1055    }
1056
1057    fn update_spring(&mut self, spring: &SpringSpec) -> bool {
1058        let start = match self.start_time {
1059            Some(s) => s,
1060            None => return false,
1061        };
1062
1063        let now = now();
1064        let elapsed = now.saturating_duration_since(start);
1065
1066        // Still in delay phase
1067        if elapsed < self.spec.delay {
1068            return true;
1069        }
1070
1071        let t = elapsed.as_secs_f32().max(0.0);
1072        let (progress, velocity) = spring_analytical(
1073            spring.damping_ratio,
1074            spring.stiffness,
1075            t,
1076            0.0,
1077            self.spring_v0,
1078        );
1079        let progress = progress.clamp(-0.1, 2.0);
1080
1081        if (progress - 1.0).abs() < spring.settle_progress
1082            && velocity.abs() < spring.settle_velocity
1083        {
1084            self.progress = 1.0;
1085            self.velocity = 0.0;
1086            self.spring_v0 = 0.0;
1087            self.current = self.target.clone();
1088            self.start_time = None;
1089            self.last_update = None;
1090            return false;
1091        }
1092
1093        self.progress = progress;
1094        self.velocity = velocity;
1095        self.current = self.start.interpolate(&self.target, self.progress);
1096        true
1097    }
1098
1099    fn update_tween(&mut self) -> bool {
1100        if let Some(start) = self.start_time {
1101            let elapsed = now().saturating_duration_since(start);
1102
1103            if elapsed < self.spec.delay {
1104                return true;
1105            }
1106
1107            let animation_time = elapsed - self.spec.delay;
1108
1109            if animation_time >= self.spec.duration {
1110                self.current = self.target.clone();
1111                self.start_time = None;
1112                return false;
1113            }
1114
1115            let t =
1116                (animation_time.as_secs_f32() / self.spec.duration.as_secs_f32()).clamp(0.0, 1.0);
1117            let eased_t = self.spec.easing.interpolate(t);
1118            let eased_t = eased_t.clamp(0.0, 1.0);
1119
1120            self.current = self.start.interpolate(&self.target, eased_t);
1121            true
1122        } else {
1123            false
1124        }
1125    }
1126
1127    pub fn get(&self) -> &T {
1128        &self.current
1129    }
1130
1131    pub fn is_animating(&self) -> bool {
1132        self.start_time.is_some()
1133    }
1134
1135    pub fn has_keyframes(&self) -> bool {
1136        self.keyframes.is_some()
1137    }
1138}
1139
1140#[cfg(test)]
1141mod tests {
1142    use super::*;
1143
1144    fn assert_in_out(ease: Easing) {
1145        // All sane eases pass through (0,0) and (1,1).
1146        assert!((ease.interpolate(0.0) - 0.0).abs() < 1e-4, "{ease:?} in(0)");
1147        assert!((ease.interpolate(1.0) - 1.0).abs() < 1e-4, "{ease:?} in(1)");
1148    }
1149
1150    #[test]
1151    fn godot_eases_pass_through_endpoints() {
1152        use Easing::*;
1153        for ease in [
1154            CubicIn,
1155            CubicOut,
1156            CubicInOut,
1157            QuartIn,
1158            QuartOut,
1159            QuartInOut,
1160            QuintIn,
1161            QuintOut,
1162            QuintInOut,
1163            SineIn,
1164            SineOut,
1165            SineInOut,
1166            ExpoIn,
1167            ExpoOut,
1168            ExpoInOut,
1169            CircIn,
1170            CircOut,
1171            CircInOut,
1172            BackIn,
1173            BackOut,
1174            BackInOut,
1175            ElasticIn,
1176            ElasticOut,
1177            ElasticInOut,
1178            BounceIn,
1179            BounceOut,
1180            BounceInOut,
1181        ] {
1182            assert_in_out(ease);
1183        }
1184    }
1185
1186    #[test]
1187    fn easing_direction_is_sane() {
1188        use Easing::*;
1189        let mid = [CubicOut, QuartOut, QuintOut, SineOut, ExpoOut, CircOut];
1190        for e in mid {
1191            assert!(e.interpolate(0.5) <= 1.0, "{e:?} stays below 1 at mid");
1192            assert!(e.interpolate(0.5) > 0.5, "{e:?} is ease-out at mid");
1193        }
1194        for e in [CubicIn, QuartIn, QuintIn, SineIn, ExpoIn, CircIn] {
1195            assert!(e.interpolate(0.5) < 0.5, "{e:?} is ease-in at mid");
1196        }
1197        // Overshoot eases exceed the [0,1] band in the middle.
1198        for e in [BackOut, ElasticOut] {
1199            assert!(e.interpolate(0.5) > 1.0, "{e:?} overshoots");
1200        }
1201        for e in [BackIn, ElasticIn] {
1202            assert!(e.interpolate(0.5) < 0.0, "{e:?} undershoots");
1203        }
1204    }
1205
1206    #[test]
1207    fn bounce_matches_known_values() {
1208        use Easing::*;
1209        assert!((BounceOut.interpolate(0.0) - 0.0).abs() < 1e-4);
1210        assert!((BounceOut.interpolate(1.0) - 1.0).abs() < 1e-4);
1211        // Bounce out reaches its first apex (1.0) at t = 1/2.75, then settles.
1212        assert!((BounceOut.interpolate(1.0 / 2.75) - 1.0).abs() < 1e-4);
1213        assert!((BounceOut.interpolate(0.5) - 0.765625).abs() < 1e-4);
1214    }
1215}