Skip to main content

repose_core/
animation.rs

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