Skip to main content

repose_core/
animation.rs

1use parking_lot::RwLock;
2use std::sync::OnceLock;
3use web_time::{Duration, Instant};
4
5pub(crate) fn now() -> Instant {
6    let lock = CLOCK.get_or_init(|| RwLock::new(Box::new(SystemClock) as Box<dyn Clock>));
7    lock.read().now()
8}
9
10/// Physical spring parameters. Duration is emergent (determined by physics), not specified.
11#[derive(Clone, Copy, Debug)]
12pub struct SpringSpec {
13    /// Damping ratio ζ: 0 = undamped, <1 = underdamped (overshoot), 1 = critically damped,
14    /// >1 = overdamped.
15    pub damping_ratio: f32,
16    /// Stiffness k: higher = faster, snappier response.
17    pub stiffness: f32,
18    /// Progress threshold for settling: when `|progress - 1.0| < this`, the spring is
19    /// considered visually close enough to the target and stops. Default: 0.005 (0.5%).
20    pub settle_progress: f32,
21    /// Velocity threshold for settling (in progress-units/second). Default: 0.1.
22    pub settle_velocity: f32,
23}
24
25impl SpringSpec {
26    pub const fn new(damping_ratio: f32, stiffness: f32) -> Self {
27        Self {
28            damping_ratio,
29            stiffness,
30            settle_progress: 0.005,
31            settle_velocity: 0.1,
32        }
33    }
34    /// Gentle preset: low overshoot, moderate speed.
35    pub const fn gentle() -> Self {
36        Self::new(0.5, 200.0)
37    }
38    /// Bouncier preset: more overshoot, faster.
39    pub const fn bouncy() -> Self {
40        Self::new(0.2, 300.0)
41    }
42    /// Critically damped: no overshoot, fast settle.
43    pub const fn crit() -> Self {
44        Self::new(1.0, 200.0)
45    }
46    /// Snappy preset: high damping, high stiffness.
47    pub const fn stiff() -> Self {
48        Self::new(0.8, 600.0)
49    }
50
51    /// Set the settling threshold in progress units. Lower values = more precise settling.
52    /// For example, 0.001 means the spring stops when within 0.1% of the target.
53    pub const fn with_settle_progress(mut self, threshold: f32) -> Self {
54        self.settle_progress = threshold;
55        self
56    }
57
58    /// Set the velocity threshold for settling (progress-units/second).
59    pub const fn with_settle_velocity(mut self, threshold: f32) -> Self {
60        self.settle_velocity = threshold;
61        self
62    }
63}
64
65/// A cubic bezier curve with control points (p1x, p1y), (p2x, p2y).
66/// P0 = (0, 0) and P3 = (1, 1) are fixed.
67#[derive(Clone, Copy, Debug)]
68pub struct CubicBezier {
69    pub p1x: f32,
70    pub p1y: f32,
71    pub p2x: f32,
72    pub p2y: f32,
73}
74
75impl CubicBezier {
76    pub const fn new(p1x: f32, p1y: f32, p2x: f32, p2y: f32) -> Self {
77        Self { p1x, p1y, p2x, p2y }
78    }
79}
80
81/// Compose Material3 EmphasizedDecelerate: cubic-bezier(0.05, 0.7, 0.1, 1.0).
82pub const EASING_EMPHASIZED_DECELERATE: CubicBezier = CubicBezier::new(0.05, 0.7, 0.1, 1.0);
83/// Compose Material3 StandardDecelerate: cubic-bezier(0.2, 0.0, 0.0, 1.0).
84pub const EASING_STANDARD_DECELERATE: CubicBezier = CubicBezier::new(0.2, 0.0, 0.0, 1.0);
85
86#[derive(Clone, Copy, Debug)]
87#[non_exhaustive]
88pub enum Easing {
89    Linear,
90    EaseIn,
91    EaseOut,
92    EaseInOut,
93    /// Monotonic, critically-damped, y(t)=1-(1+ω t)e^{-ω t}, t∈[0,1].
94    SpringCrit {
95        omega: f32,
96    },
97    /// Underdamped, low-overshoot preset (ζ≈0.5, ω≈8)
98    SpringGentle,
99    /// Underdamped, bouncier preset (ζ≈0.2, ω≈12)
100    SpringBouncy,
101    /// Android FastOutSlowIn: cubic-bezier(0.4, 0.0, 0.2, 1.0).
102    /// Starts fast, decelerates through the middle, ends slow.
103    FastOutSlowIn,
104    /// Custom cubic-bezier easing with control points (p1x, p1y), (p2x, p2y).
105    Custom(CubicBezier),
106}
107
108impl Easing {
109    pub fn interpolate(&self, t: f32) -> f32 {
110        match self {
111            Easing::Linear => t,
112            Easing::EaseIn => t * t,
113            Easing::EaseOut => t * (2.0 - t),
114            Easing::EaseInOut => {
115                if t < 0.5 {
116                    2.0 * t * t
117                } else {
118                    -1.0 + (4.0 - 2.0 * t) * t
119                }
120            }
121            Easing::SpringCrit { omega } => {
122                let w = (*omega).max(0.0);
123                let tt = t.max(0.0);
124                // y = 1 - (1 + w t) e^{-w t}
125                1.0 - (1.0 + w * tt) * (-(w * tt)).exp()
126            }
127            Easing::SpringGentle => spring_underdamped_normalized(t, 0.5, 8.0),
128            Easing::SpringBouncy => spring_underdamped_normalized(t, 0.2, 12.0),
129            Easing::FastOutSlowIn => eval_cubic_bezier(0.4, 0.0, 0.2, 1.0, t),
130            Easing::Custom(cb) => eval_cubic_bezier(cb.p1x, cb.p1y, cb.p2x, cb.p2y, t),
131        }
132    }
133}
134
135/// Evaluate a cubic bezier with control points P1=(p1x,p1y), P2=(p2x,p2y)
136/// (P0=(0,0) and P3=(1,1) are fixed). Uses Newton's method (5 iterations)
137/// to find `u` such that x(u) = t, then returns y(u).
138fn eval_cubic_bezier(p1x: f32, p1y: f32, p2x: f32, p2y: f32, t: f32) -> f32 {
139    let t = t.clamp(0.0, 1.0);
140    if t <= 0.0 {
141        return 0.0;
142    }
143    if t >= 1.0 {
144        return 1.0;
145    }
146    let mut u = t;
147    for _ in 0..6 {
148        let omu = 1.0 - u;
149        let x = 3.0 * omu * omu * u * p1x + 3.0 * omu * u * u * p2x + u * u * u;
150        let dx = 3.0 * omu * omu * p1x + 6.0 * omu * u * (p2x - p1x) + 3.0 * u * u * (1.0 - p2x);
151        if dx.abs() < 1e-10 {
152            break;
153        }
154        u -= (x - t) / dx;
155        u = u.clamp(0.0, 1.0);
156    }
157    let omu = 1.0 - u;
158    3.0 * omu * omu * u * p1y + 3.0 * omu * u * u * p2y + u * u * u
159}
160
161/// Cubic Hermite spline interpolation. Given interval width `h`, normalized
162/// position `x` in [0,1], endpoint values `y1,y2` and their tangents `t1,t2`,
163/// returns the interpolated value.
164///
165/// Factored to reduce operation count (adapted from Compose's MonoSpline).
166fn hermite_interpolate(h: f32, x: f32, y1: f32, y2: f32, t1: f32, t2: f32) -> f32 {
167    let x2 = x * x;
168    let x3 = x2 * x;
169    h * t1 * (x - 2.0 * x2 + x3) + h * t2 * (x3 - x2) + y1 - (3.0 * x2 - 2.0 * x3) * (y1 - y2)
170}
171
172/// Derivative of cubic Hermite spline at normalized position `x`.
173#[allow(dead_code)]
174fn hermite_differential(h: f32, x: f32, y1: f32, y2: f32, t1: f32, t2: f32) -> f32 {
175    let x2 = x * x;
176    h * (t1 - 2.0 * x * (2.0 * t1 + t2) + 3.0 * (t1 + t2) * x2) - 6.0 * (x - x2) * (y1 - y2)
177}
178
179/// A monotone cubic Hermite spline for C1-continuous interpolation of `f32` values.
180///
181/// Uses the Fritsch–Carlson method to compute tangents that preserve monotonicity
182/// and prevent overshoot. Based on Android Compose's `MonoSpline`.
183#[derive(Clone, Debug)]
184pub struct MonoSpline {
185    times: Vec<f32>,
186    values: Vec<f32>,
187    tangents: Vec<f32>,
188}
189
190impl MonoSpline {
191    /// Build a spline from keyframe times and values.
192    /// Times must be sorted ascending and have at least 2 entries.
193    /// Values must have the same length as times.
194    pub fn new(times: Vec<f32>, values: Vec<f32>) -> Self {
195        assert!(times.len() >= 2, "MonoSpline requires at least 2 keyframes");
196        assert_eq!(times.len(), values.len());
197        let n = times.len();
198        let mut tangents = vec![0.0; n];
199
200        // Compute slopes for each segment
201        let mut slopes = vec![0.0; n.saturating_sub(1)];
202        for i in 0..n - 1 {
203            let dt = times[i + 1] - times[i];
204            slopes[i] = (values[i + 1] - values[i]) / dt;
205        }
206
207        // Tangents at interior knots: average of adjacent slopes
208        tangents[0] = slopes[0];
209        for i in 1..n - 1 {
210            tangents[i] = (slopes[i - 1] + slopes[i]) * 0.5;
211        }
212        tangents[n - 1] = slopes[n - 2];
213
214        // Fritsch–Carlson monotonicity preservation
215        for i in 0..n - 1 {
216            if slopes[i] == 0.0 {
217                tangents[i] = 0.0;
218                tangents[i + 1] = 0.0;
219            } else {
220                let a = tangents[i] / slopes[i];
221                let b = tangents[i + 1] / slopes[i];
222                let h = (a * a + b * b).sqrt();
223                if h > 9.0 {
224                    let t = 3.0 / h;
225                    tangents[i] = t * a * slopes[i];
226                    tangents[i + 1] = t * b * slopes[i];
227                }
228            }
229        }
230
231        Self {
232            times,
233            values,
234            tangents,
235        }
236    }
237
238    /// Evaluate the spline at time `t`.
239    /// Clamps `t` to the spline's time range. Extrapolates using the endpoint tangent.
240    pub fn evaluate(&self, t: f32) -> f32 {
241        let n = self.times.len();
242        let first = self.times[0];
243        let last = self.times[n - 1];
244
245        if t <= first {
246            return self.values[0] + (t - first) * self.tangents[0];
247        }
248        if t >= last {
249            return self.values[n - 1] + (t - last) * self.tangents[n - 1];
250        }
251
252        for i in 0..n - 1 {
253            if t >= self.times[i] && t <= self.times[i + 1] {
254                let h = self.times[i + 1] - self.times[i];
255                let x = (t - self.times[i]) / h;
256                return hermite_interpolate(
257                    h,
258                    x,
259                    self.values[i],
260                    self.values[i + 1],
261                    self.tangents[i],
262                    self.tangents[i + 1],
263                );
264            }
265        }
266
267        self.values[n - 1] // fallback
268    }
269}
270
271/// Returns (progress, velocity) at time `t` with initial conditions (x0, v0).
272fn spring_analytical(zeta: f32, stiffness: f32, t: f32, x0: f32, v0: f32) -> (f32, f32) {
273    if t <= 0.0 {
274        return (x0, v0);
275    }
276
277    let omega = if stiffness > 0.0 {
278        stiffness.sqrt()
279    } else {
280        return (x0 + v0 * t, v0);
281    };
282
283    let zeta = zeta.max(0.0);
284    let exp = (-zeta * omega * t).exp();
285    let a = 1.0 - x0; // amplitude coefficient
286
287    if (zeta - 1.0).abs() < 1e-6 {
288        // Critically damped: x(t) = 1 - (A + B*t) * e^{-ωt}
289        let b = v0 + omega * a;
290        let progress = 1.0 - (a + b * t) * exp;
291        let velocity = (a * omega - b + b * omega * t) * exp;
292        (progress, velocity)
293    } else if zeta < 1.0 {
294        // Underdamped: x(t) = 1 - e^{-ζωt}[A*cos(ωd*t) + C*sin(ωd*t)]
295        let wd = omega * (1.0 - zeta * zeta).sqrt();
296        let c = (v0 + zeta * omega * a) / wd;
297        let cos_wd = (wd * t).cos();
298        let sin_wd = (wd * t).sin();
299        let env = a * cos_wd + c * sin_wd;
300        let progress = 1.0 - exp * env;
301        let velocity =
302            exp * ((zeta * omega * a - wd * c) * cos_wd + (zeta * omega * c + wd * a) * sin_wd);
303        (progress, velocity)
304    } else {
305        // Overdamped: x(t) = 1 - e^{-ζωt}[A*cosh(ωd'*t) + D*sinh(ωd'*t)]
306        let wd = omega * (zeta * zeta - 1.0).sqrt();
307        let d = (v0 + zeta * omega * a) / wd;
308        let cosh_wd = (wd * t).cosh();
309        let sinh_wd = (wd * t).sinh();
310        let env = a * cosh_wd + d * sinh_wd;
311        let progress = 1.0 - exp * env;
312        let velocity =
313            exp * ((zeta * omega * a - wd * d) * cosh_wd + (zeta * omega * d - wd * a) * sinh_wd);
314        (progress, velocity)
315    }
316}
317
318fn spring_underdamped_normalized(t: f32, zeta: f32, omega: f32) -> f32 {
319    let tt = t.max(0.0);
320    let z = zeta.clamp(0.0, 0.999);
321    let w = omega.max(0.0);
322    let wd = w * (1.0 - z * z).sqrt();
323    let exp_term = (-z * w * tt).exp();
324    let cos_term = (wd * tt).cos();
325    let sin_term = (wd * tt).sin();
326    // Standard second-order underdamped unit-step response
327    let c = z / (1.0 - z * z).sqrt();
328    let y = 1.0 - exp_term * (cos_term + c * sin_term);
329    y.clamp(0.0, 1.0)
330}
331
332#[derive(Clone, Copy, Debug)]
333pub struct AnimationSpec {
334    pub duration: Duration,
335    pub easing: Easing,
336    pub delay: Duration,
337    /// If set, use true physical spring simulation (duration is ignored, emergent from physics).
338    pub spring: Option<SpringSpec>,
339    /// If set, wrap the animation in repeat behavior (n iterations, optional ping-pong).
340    pub repeat: Option<RepeatableSpec>,
341}
342
343impl Default for AnimationSpec {
344    fn default() -> Self {
345        Self {
346            duration: Duration::from_millis(300),
347            easing: Easing::EaseInOut,
348            delay: Duration::ZERO,
349            spring: None,
350            repeat: None,
351        }
352    }
353}
354
355impl AnimationSpec {
356    pub fn tween(duration: Duration, easing: Easing) -> Self {
357        Self {
358            duration,
359            easing,
360            delay: Duration::ZERO,
361            spring: None,
362            repeat: None,
363        }
364    }
365    /// True physical spring simulation - duration is emergent, no fixed duration needed.
366    pub fn spring(spring: SpringSpec) -> Self {
367        Self {
368            duration: Duration::ZERO,
369            easing: Easing::Linear,
370            delay: Duration::ZERO,
371            spring: Some(spring),
372            repeat: None,
373        }
374    }
375    /// Gentle underdamped preset (small overshoot). Uses true spring physics.
376    pub fn spring_gentle() -> Self {
377        Self::spring(SpringSpec::gentle())
378    }
379    /// Bouncier underdamped preset. Uses true spring physics.
380    pub fn spring_bouncy() -> Self {
381        Self::spring(SpringSpec::bouncy())
382    }
383    /// Critically damped spring with given omega (angular frequency). Uses true spring physics.
384    pub fn spring_crit(omega: f32) -> Self {
385        Self::spring(SpringSpec::new(1.0, omega * omega))
386    }
387
388    pub fn fast() -> Self {
389        Self {
390            duration: Duration::from_millis(150),
391            easing: Easing::EaseOut,
392            delay: Duration::ZERO,
393            spring: None,
394            repeat: None,
395        }
396    }
397
398    pub fn slow() -> Self {
399        Self {
400            duration: Duration::from_millis(600),
401            easing: Easing::EaseInOut,
402            delay: Duration::ZERO,
403            spring: None,
404            repeat: None,
405        }
406    }
407
408    /// Wrap this spec in a repeatable animation.
409    /// Pass `RepeatableSpec::infinite()` for infinite repeats.
410    pub fn repeated(mut self, repeat: RepeatableSpec) -> Self {
411        self.repeat = Some(repeat);
412        self
413    }
414}
415
416/// A keyframe animation specification.
417///
418/// Defines a sequence of keyframes at specific timestamps (0.0 to 1.0),
419/// with target values and optional easing between each pair.
420#[derive(Clone, Debug)]
421pub struct KeyframesSpec<T: Clone> {
422    /// Keyframes as (timestamp 0.0-1.0, value, optional easing between previous and this).
423    /// The first keyframe should be at t=0.0 and uses no easing.
424    pub keyframes: Vec<(f32, T, Option<Easing>)>,
425}
426
427impl<T: Clone + Interpolate> KeyframesSpec<T> {
428    pub fn new(keyframes: Vec<(f32, T)>) -> Self {
429        let with_easing = keyframes.into_iter().map(|(t, v)| (t, v, None)).collect();
430        Self {
431            keyframes: with_easing,
432        }
433    }
434
435    /// Add easing between the previous keyframe and this one.
436    pub fn with_easing(mut self, easing: Easing) -> Self {
437        if let Some(last) = self.keyframes.last_mut() {
438            last.2 = Some(easing);
439        }
440        self
441    }
442
443    pub fn evaluate(&self, t: f32) -> T {
444        let t = t.clamp(0.0, 1.0);
445        let kf = &self.keyframes;
446        if kf.is_empty() {
447            panic!("KeyframesSpec must have at least one keyframe");
448        }
449
450        // Linear interpolation (with per-segment easing)
451        for i in 0..kf.len() - 1 {
452            let (t0, _, _) = kf[i];
453            let (t1, ref v1, easing) = kf[i + 1];
454            if t >= t0 && t <= t1 {
455                let segment_t = if (t1 - t0).abs() < f32::EPSILON {
456                    1.0
457                } else {
458                    (t - t0) / (t1 - t0)
459                };
460                let eased_t = match easing {
461                    Some(e) => e.interpolate(segment_t),
462                    None => segment_t,
463                };
464                return kf[i].1.interpolate(v1, eased_t);
465            }
466        }
467        kf.last().unwrap().1.clone()
468    }
469}
470
471/// A keyframe animation specification with smooth cubic Hermite spline interpolation.
472///
473/// Provides C1 continuity (smooth derivatives at keyframe boundaries),
474/// unlike `KeyframesSpec` which uses C0 linear interpolation.
475///
476/// Uses the Fritsch–Carlson monotonicity-preserving Hermite spline
477#[derive(Clone, Debug)]
478pub struct SplineKeyframes {
479    spline: MonoSpline,
480}
481
482impl SplineKeyframes {
483    /// Build a spline keyframe from time/value pairs.
484    ///
485    /// Times should be in [0.0, 1.0] and sorted ascending.
486    /// At least 2 keyframes are required.
487    pub fn new(keyframes: Vec<(f32, f32)>) -> Self {
488        assert!(
489            keyframes.len() >= 2,
490            "SplineKeyframes requires at least 2 keyframes"
491        );
492        let times: Vec<f32> = keyframes.iter().map(|(t, _)| *t).collect();
493        let values: Vec<f32> = keyframes.iter().map(|(_, v)| *v).collect();
494        Self {
495            spline: MonoSpline::new(times, values),
496        }
497    }
498
499    /// Evaluate the spline at normalized time `t` (0.0 to 1.0).
500    pub fn evaluate(&self, t: f32) -> f32 {
501        self.spline.evaluate(t.clamp(0.0, 1.0))
502    }
503}
504
505/// A repeatable animation specification.
506///
507/// Wraps another animation spec and causes it to repeat.
508/// Default: infinite repeat with no reverse.
509#[derive(Clone, Copy, Debug)]
510pub struct RepeatableSpec {
511    /// Number of repetitions. `None` means infinite.
512    pub iterations: Option<u32>,
513    /// If true, alternate direction each iteration (forward, backward, forward...).
514    pub reverse: bool,
515    /// Delay between each iteration.
516    pub delay_between: Duration,
517}
518
519impl Default for RepeatableSpec {
520    fn default() -> Self {
521        Self {
522            iterations: None,
523            reverse: false,
524            delay_between: Duration::ZERO,
525        }
526    }
527}
528
529impl RepeatableSpec {
530    pub fn new(iterations: u32) -> Self {
531        Self {
532            iterations: Some(iterations),
533            reverse: false,
534            delay_between: Duration::ZERO,
535        }
536    }
537
538    pub fn infinite() -> Self {
539        Self {
540            iterations: None,
541            reverse: false,
542            delay_between: Duration::ZERO,
543        }
544    }
545
546    pub fn reverse(mut self) -> Self {
547        self.reverse = true;
548        self
549    }
550
551    pub fn delay_between(mut self, d: Duration) -> Self {
552        self.delay_between = d;
553        self
554    }
555}
556
557/// Decay animation configuration.
558///
559/// Models a damped decay (e.g., for fling-to-stop animations).
560#[derive(Clone, Copy, Debug)]
561pub struct DecayAnimationSpec {
562    /// How quickly the animation decelerates. Lower = faster stop.
563    pub friction: f32,
564    /// Minimum velocity threshold to stop.
565    pub stop_threshold: f32,
566}
567
568impl Default for DecayAnimationSpec {
569    fn default() -> Self {
570        Self {
571            friction: 0.8,
572            stop_threshold: 1.0,
573        }
574    }
575}
576
577impl DecayAnimationSpec {
578    pub fn new(friction: f32) -> Self {
579        Self {
580            friction: friction.clamp(0.01, 1.0),
581            stop_threshold: 1.0,
582        }
583    }
584}
585
586impl AnimatedValue<f32> {
587    /// Tick the decay animation. Returns `true` if still animating.
588    pub fn update_decay(&mut self, friction: f32, stop_threshold: f32) -> bool {
589        let _start = match self.start_time {
590            Some(s) => s,
591            None => return false,
592        };
593
594        let now = now();
595        let dt = match self.last_update {
596            Some(last) => now.saturating_duration_since(last).as_secs_f32().min(0.05),
597            None => 0.0,
598        };
599        self.last_update = Some(now);
600
601        if dt <= 0.0 {
602            return true;
603        }
604
605        if self.velocity.abs() < stop_threshold {
606            self.velocity = 0.0;
607            self.start_time = None;
608            return false;
609        }
610
611        self.velocity *= friction.powf(dt * 60.0);
612        let delta = self.velocity * dt;
613        // We store the "current value" as a single f32 offset
614        // that accumulates. But AnimatedValue<f32> stores explicit
615        // start/target. For decay we just accumulate the current.
616        // Because of the AnimatedValue structure, we use progress as
617        // the accumulated value relative to start.
618        let new_progress = self.progress + delta;
619        self.progress = new_progress;
620        // current = start + (target - start) * progress but target = ???.
621        // For decay, progress IS the value (starting from 0).
622        // We repurpose: current = start + progress (progress is offset from start).
623        // Since T = f32, we can just set current directly.
624        if self.progress.abs() < 0.001 && self.velocity.abs() < stop_threshold {
625            self.progress = 0.0;
626            self.velocity = 0.0;
627            self.start_time = None;
628            return false;
629        }
630
631        self.current = self.start.interpolate(&self.target, self.progress);
632        true
633    }
634}
635
636pub trait Interpolate {
637    fn interpolate(&self, other: &Self, t: f32) -> Self;
638}
639
640impl Interpolate for f32 {
641    fn interpolate(&self, other: &Self, t: f32) -> Self {
642        self + (other - self) * t
643    }
644}
645
646impl Interpolate for crate::Color {
647    fn interpolate(&self, other: &Self, t: f32) -> Self {
648        let lerp = |a: u8, b: u8| {
649            (a as f32 + (b as f32 - a as f32) * t)
650                .round()
651                .clamp(0.0, 255.0) as u8
652        };
653        crate::Color(
654            lerp(self.0, other.0),
655            lerp(self.1, other.1),
656            lerp(self.2, other.2),
657            lerp(self.3, other.3),
658        )
659    }
660}
661
662impl Interpolate for crate::Vec2 {
663    fn interpolate(&self, other: &Self, t: f32) -> Self {
664        crate::Vec2 {
665            x: self.x.interpolate(&other.x, t),
666            y: self.y.interpolate(&other.y, t),
667        }
668    }
669}
670
671impl Interpolate for crate::Size {
672    fn interpolate(&self, other: &Self, t: f32) -> Self {
673        crate::Size {
674            width: self.width.interpolate(&other.width, t),
675            height: self.height.interpolate(&other.height, t),
676        }
677    }
678}
679
680impl Interpolate for crate::Rect {
681    fn interpolate(&self, other: &Self, t: f32) -> Self {
682        crate::Rect {
683            x: self.x.interpolate(&other.x, t),
684            y: self.y.interpolate(&other.y, t),
685            w: self.w.interpolate(&other.w, t),
686            h: self.h.interpolate(&other.h, t),
687        }
688    }
689}
690
691// Animation clock
692pub trait Clock: Send + Sync + 'static {
693    fn now(&self) -> Instant;
694}
695
696pub struct SystemClock;
697impl Clock for SystemClock {
698    fn now(&self) -> Instant {
699        Instant::now()
700    }
701}
702
703static CLOCK: OnceLock<RwLock<Box<dyn Clock>>> = OnceLock::new();
704
705/// Install a global animation clock. Platform sets this to SystemClock; tests can set TestClock.
706pub fn set_clock(clock: Box<dyn Clock>) {
707    let lock = CLOCK.get_or_init(|| RwLock::new(Box::new(SystemClock) as Box<dyn Clock>));
708    *lock.write() = clock;
709}
710/// Install default system clock if none present (idempotent).
711pub fn ensure_system_clock() {
712    let _ = CLOCK.get_or_init(|| RwLock::new(Box::new(SystemClock) as Box<dyn Clock>));
713}
714
715/// A test clock you can drive deterministically.
716#[derive(Clone)]
717pub struct TestClock {
718    pub t: Instant,
719}
720impl Clock for TestClock {
721    fn now(&self) -> Instant {
722        self.t
723    }
724}
725
726/// Animated value that transitions smoothly.
727///
728/// Supports two modes:
729/// - **Tween** (when `spec.spring` is `None`): interpolates between `start` and `target`
730///   over a fixed duration using an easing curve.
731/// - **Spring** (when `spec.spring` is `Some`): numerically integrates a physical spring ODE
732///   (`x'' = -k·(x - target) - d·x'`) with emergent duration. When the target changes
733///   mid-animation, the current value and velocity carry forward seamlessly.
734pub struct AnimatedValue<T: Interpolate + Clone> {
735    current: T,
736    target: T,
737    start: T,
738    spec: AnimationSpec,
739    keyframes: Option<KeyframesSpec<T>>,
740    iteration: u32,
741    start_time: Option<Instant>,
742    // Spring simulation state (progress-based, works for any T: Interpolate)
743    progress: f32,
744    velocity: f32,
745    /// Initial velocity for the current spring segment (carry-over from target changes).
746    spring_v0: f32,
747    last_update: Option<Instant>,
748}
749
750impl<T: Interpolate + Clone> AnimatedValue<T> {
751    pub fn new(initial: T, spec: AnimationSpec) -> Self {
752        Self {
753            current: initial.clone(),
754            target: initial.clone(),
755            start: initial,
756            spec,
757            keyframes: None,
758            iteration: 0,
759            start_time: None,
760            progress: 1.0,
761            velocity: 0.0,
762            spring_v0: 0.0,
763            last_update: None,
764        }
765    }
766
767    pub fn set_spec(&mut self, spec: AnimationSpec) {
768        self.spec = spec;
769    }
770
771    /// Set a keyframes spec for multi-stage animation.
772    /// When set, `set_target` is ignored and the value is driven by the keyframe sequence.
773    pub fn set_keyframes(&mut self, keyframes: KeyframesSpec<T>) {
774        self.keyframes = Some(keyframes);
775        self.start_time = Some(now());
776        self.last_update = None;
777        self.iteration = 0;
778    }
779
780    pub fn set_target(&mut self, target: T) {
781        // Don't call self.update() here — self.spec may have been changed by the
782        // caller before set_target (e.g. set_spec → set_target). The driver's
783        // tick() already advanced all animations before composition, so
784        // self.current is already up to date.
785        self.keyframes = None;
786        self.start = self.current.clone();
787        self.target = target;
788        self.start_time = Some(now());
789        self.last_update = None;
790        self.iteration = 0;
791        if self.spec.spring.is_some() {
792            // Spring mode: start progress at 0 (the current value), carry velocity forward
793            self.progress = 0.0;
794            self.spring_v0 = self.velocity;
795        }
796    }
797
798    /// Snap immediately to a value without animating.
799    pub fn snap_to(&mut self, value: T) {
800        self.current = value.clone();
801        self.target = value.clone();
802        self.start = value;
803        self.keyframes = None;
804        self.start_time = None;
805        self.progress = 1.0;
806        self.velocity = 0.0;
807        self.spring_v0 = 0.0;
808        self.last_update = None;
809    }
810
811    pub fn update(&mut self) -> bool {
812        let spring_spec = self.spec.spring;
813        let mut still = if let Some(spring) = spring_spec {
814            self.update_spring(&spring)
815        } else if self.keyframes.is_some() {
816            self.update_keyframes()
817        } else {
818            self.update_tween()
819        };
820
821        if !still {
822            // Check if we should repeat
823            if let Some(repeat) = &self.spec.repeat {
824                let maxed = repeat
825                    .iterations
826                    .is_some_and(|max| self.iteration + 1 >= max);
827                if !maxed {
828                    self.iteration += 1;
829                    if repeat.reverse {
830                        std::mem::swap(&mut self.start, &mut self.target);
831                    }
832                    self.progress = 0.0;
833                    self.velocity = 0.0;
834                    self.start_time = Some(now());
835                    self.last_update = None;
836                    still = true;
837                }
838            }
839        }
840
841        still
842    }
843
844    fn update_keyframes(&mut self) -> bool {
845        let start = match self.start_time {
846            Some(s) => s,
847            None => return false,
848        };
849        let elapsed = now().saturating_duration_since(start);
850        if elapsed < self.spec.delay {
851            return true;
852        }
853        let animation_time = elapsed - self.spec.delay;
854        if animation_time >= self.spec.duration {
855            if let Some(ref kf) = self.keyframes {
856                self.current = kf.evaluate(1.0);
857            }
858            self.start_time = None;
859            return false;
860        }
861        let t = (animation_time.as_secs_f32() / self.spec.duration.as_secs_f32()).clamp(0.0, 1.0);
862        let eased_t = self.spec.easing.interpolate(t).clamp(0.0, 1.0);
863        if let Some(ref kf) = self.keyframes {
864            self.current = kf.evaluate(eased_t);
865        }
866        true
867    }
868
869    fn update_spring(&mut self, spring: &SpringSpec) -> bool {
870        let start = match self.start_time {
871            Some(s) => s,
872            None => return false,
873        };
874
875        let now = now();
876        let elapsed = now.saturating_duration_since(start);
877
878        // Still in delay phase
879        if elapsed < self.spec.delay {
880            return true;
881        }
882
883        let t = elapsed.as_secs_f32().max(0.0);
884        let (progress, velocity) = spring_analytical(
885            spring.damping_ratio,
886            spring.stiffness,
887            t,
888            0.0,
889            self.spring_v0,
890        );
891        let progress = progress.clamp(-0.1, 2.0);
892
893        // Check if settled
894        if (progress - 1.0).abs() < spring.settle_progress
895            && velocity.abs() < spring.settle_velocity
896        {
897            self.progress = 1.0;
898            self.velocity = 0.0;
899            self.spring_v0 = 0.0;
900            self.current = self.target.clone();
901            self.start_time = None;
902            self.last_update = None;
903            return false;
904        }
905
906        self.progress = progress;
907        self.velocity = velocity;
908        self.current = self.start.interpolate(&self.target, self.progress);
909        true
910    }
911
912    fn update_tween(&mut self) -> bool {
913        if let Some(start) = self.start_time {
914            let elapsed = now().saturating_duration_since(start);
915
916            if elapsed < self.spec.delay {
917                return true;
918            }
919
920            let animation_time = elapsed - self.spec.delay;
921
922            if animation_time >= self.spec.duration {
923                self.current = self.target.clone();
924                self.start_time = None;
925                return false;
926            }
927
928            let t =
929                (animation_time.as_secs_f32() / self.spec.duration.as_secs_f32()).clamp(0.0, 1.0);
930            let eased_t = self.spec.easing.interpolate(t);
931            let eased_t = eased_t.clamp(0.0, 1.0);
932
933            self.current = self.start.interpolate(&self.target, eased_t);
934            true
935        } else {
936            false
937        }
938    }
939
940    pub fn get(&self) -> &T {
941        &self.current
942    }
943
944    pub fn is_animating(&self) -> bool {
945        self.start_time.is_some()
946    }
947
948    pub fn has_keyframes(&self) -> bool {
949        self.keyframes.is_some()
950    }
951}