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