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