Skip to main content

repose_core/
animation.rs

1use parking_lot::RwLock;
2use std::sync::OnceLock;
3use web_time::{Duration, Instant};
4
5pub(crate) fn now() -> Instant {
6    let lock = CLOCK.get_or_init(|| RwLock::new(Box::new(SystemClock) as Box<dyn Clock>));
7    lock.read().now()
8}
9
10/// Physical spring parameters. Duration is emergent (determined by physics), not specified.
11#[derive(Clone, Copy, Debug)]
12pub struct SpringSpec {
13    /// Damping ratio ζ: 0 = undamped, <1 = underdamped (overshoot), 1 = critically damped,
14    /// >1 = overdamped.
15    pub damping_ratio: f32,
16    /// Stiffness k: higher = faster, snappier response.
17    pub stiffness: f32,
18}
19
20impl SpringSpec {
21    pub const fn new(damping_ratio: f32, stiffness: f32) -> Self {
22        Self {
23            damping_ratio,
24            stiffness,
25        }
26    }
27    /// Gentle preset: low overshoot, moderate speed.
28    pub const fn gentle() -> Self {
29        Self::new(0.5, 200.0)
30    }
31    /// Bouncier preset: more overshoot, faster.
32    pub const fn bouncy() -> Self {
33        Self::new(0.2, 300.0)
34    }
35    /// Critically damped: no overshoot, fast settle.
36    pub const fn crit() -> Self {
37        Self::new(1.0, 200.0)
38    }
39    /// Snappy preset: high damping, high stiffness.
40    pub const fn stiff() -> Self {
41        Self::new(0.8, 600.0)
42    }
43}
44
45#[derive(Clone, Copy, Debug)]
46pub enum Easing {
47    Linear,
48    EaseIn,
49    EaseOut,
50    EaseInOut,
51    /// Monotonic, critically-damped, y(t)=1-(1+ω t)e^{-ω t}, t∈[0,1].
52    SpringCrit {
53        omega: f32,
54    },
55    /// Underdamped, low-overshoot preset (ζ≈0.5, ω≈8)
56    SpringGentle,
57    /// Underdamped, bouncier preset (ζ≈0.2, ω≈12)
58    SpringBouncy,
59    /// Android FastOutSlowIn: cubic-bezier(0.4, 0.0, 0.2, 1.0).
60    /// Starts fast, decelerates through the middle, ends slow.
61    FastOutSlowIn,
62}
63
64impl Easing {
65    pub fn interpolate(&self, t: f32) -> f32 {
66        match self {
67            Easing::Linear => t,
68            Easing::EaseIn => t * t,
69            Easing::EaseOut => t * (2.0 - t),
70            Easing::EaseInOut => {
71                if t < 0.5 {
72                    2.0 * t * t
73                } else {
74                    -1.0 + (4.0 - 2.0 * t) * t
75                }
76            }
77            Easing::SpringCrit { omega } => {
78                let w = (*omega).max(0.0);
79                let tt = t.max(0.0);
80                // y = 1 - (1 + w t) e^{-w t}
81                1.0 - (1.0 + w * tt) * (-(w * tt)).exp()
82            }
83            Easing::SpringGentle => spring_underdamped_normalized(t, 0.5, 8.0),
84            Easing::SpringBouncy => spring_underdamped_normalized(t, 0.2, 12.0),
85            Easing::FastOutSlowIn => eval_cubic_bezier(0.4, 0.0, 0.2, 1.0, t),
86        }
87    }
88}
89
90/// Evaluate a cubic bezier with control points P1=(p1x,p1y), P2=(p2x,p2y)
91/// (P0=(0,0) and P3=(1,1) are fixed). Uses Newton's method (5 iterations)
92/// to find `u` such that x(u) = t, then returns y(u).
93fn eval_cubic_bezier(p1x: f32, p1y: f32, p2x: f32, p2y: f32, t: f32) -> f32 {
94    let t = t.clamp(0.0, 1.0);
95    if t <= 0.0 {
96        return 0.0;
97    }
98    if t >= 1.0 {
99        return 1.0;
100    }
101    let mut u = t;
102    for _ in 0..6 {
103        let omu = 1.0 - u;
104        let x = 3.0 * omu * omu * u * p1x + 3.0 * omu * u * u * p2x + u * u * u;
105        let dx = 3.0 * omu * omu * p1x + 6.0 * omu * u * (p2x - p1x) + 3.0 * u * u * (1.0 - p2x);
106        if dx.abs() < 1e-10 {
107            break;
108        }
109        u -= (x - t) / dx;
110        u = u.clamp(0.0, 1.0);
111    }
112    let omu = 1.0 - u;
113    3.0 * omu * omu * u * p1y + 3.0 * omu * u * u * p2y + u * u * u
114}
115
116fn spring_underdamped_normalized(t: f32, zeta: f32, omega: f32) -> f32 {
117    let tt = t.max(0.0);
118    let z = zeta.clamp(0.0, 0.999);
119    let w = omega.max(0.0);
120    let wd = w * (1.0 - z * z).sqrt();
121    let exp_term = (-z * w * tt).exp();
122    let cos_term = (wd * tt).cos();
123    let sin_term = (wd * tt).sin();
124    // Standard second-order underdamped unit-step response
125    let c = z / (1.0 - z * z).sqrt();
126    let y = 1.0 - exp_term * (cos_term + c * sin_term);
127    y.clamp(0.0, 1.0)
128}
129
130#[derive(Clone, Copy, Debug)]
131pub struct AnimationSpec {
132    pub duration: Duration,
133    pub easing: Easing,
134    pub delay: Duration,
135    /// If set, use true physical spring simulation (duration is ignored, emergent from physics).
136    pub spring: Option<SpringSpec>,
137    /// If set, wrap the animation in repeat behavior (n iterations, optional ping-pong).
138    pub repeat: Option<RepeatableSpec>,
139}
140
141impl Default for AnimationSpec {
142    fn default() -> Self {
143        Self {
144            duration: Duration::from_millis(300),
145            easing: Easing::EaseInOut,
146            delay: Duration::ZERO,
147            spring: None,
148            repeat: None,
149        }
150    }
151}
152
153impl AnimationSpec {
154    pub fn tween(duration: Duration, easing: Easing) -> Self {
155        Self {
156            duration,
157            easing,
158            delay: Duration::ZERO,
159            spring: None,
160            repeat: None,
161        }
162    }
163    /// True physical spring simulation - duration is emergent, no fixed duration needed.
164    pub fn spring(spring: SpringSpec) -> Self {
165        Self {
166            duration: Duration::ZERO,
167            easing: Easing::Linear,
168            delay: Duration::ZERO,
169            spring: Some(spring),
170            repeat: None,
171        }
172    }
173    /// Gentle underdamped preset (small overshoot). Uses true spring physics.
174    pub fn spring_gentle() -> Self {
175        Self::spring(SpringSpec::gentle())
176    }
177    /// Bouncier underdamped preset. Uses true spring physics.
178    pub fn spring_bouncy() -> Self {
179        Self::spring(SpringSpec::bouncy())
180    }
181    /// Critically damped spring with given omega (angular frequency). Uses true spring physics.
182    pub fn spring_crit(omega: f32) -> Self {
183        Self::spring(SpringSpec::new(1.0, omega * omega))
184    }
185
186    pub fn fast() -> Self {
187        Self {
188            duration: Duration::from_millis(150),
189            easing: Easing::EaseOut,
190            delay: Duration::ZERO,
191            spring: None,
192            repeat: None,
193        }
194    }
195
196    pub fn slow() -> Self {
197        Self {
198            duration: Duration::from_millis(600),
199            easing: Easing::EaseInOut,
200            delay: Duration::ZERO,
201            spring: None,
202            repeat: None,
203        }
204    }
205
206    /// Wrap this spec in a repeatable animation.
207    /// Pass `RepeatableSpec::infinite()` for infinite repeats.
208    pub fn repeated(mut self, repeat: RepeatableSpec) -> Self {
209        self.repeat = Some(repeat);
210        self
211    }
212}
213
214/// A keyframe animation specification.
215///
216/// Defines a sequence of keyframes at specific timestamps (0.0 to 1.0),
217/// with target values and optional easing between each pair.
218#[derive(Clone, Debug)]
219pub struct KeyframesSpec<T: Clone> {
220    /// Keyframes as (timestamp 0.0-1.0, value, optional easing between previous and this).
221    /// The first keyframe should be at t=0.0 and uses no easing.
222    pub keyframes: Vec<(f32, T, Option<Easing>)>,
223}
224
225impl<T: Clone + Interpolate> KeyframesSpec<T> {
226    pub fn new(keyframes: Vec<(f32, T)>) -> Self {
227        let with_easing = keyframes.into_iter().map(|(t, v)| (t, v, None)).collect();
228        Self {
229            keyframes: with_easing,
230        }
231    }
232
233    /// Add easing between the previous keyframe and this one.
234    pub fn with_easing(mut self, easing: Easing) -> Self {
235        if let Some(last) = self.keyframes.last_mut() {
236            last.2 = Some(easing);
237        }
238        self
239    }
240
241    pub fn evaluate(&self, t: f32) -> T {
242        let t = t.clamp(0.0, 1.0);
243        let kf = &self.keyframes;
244        if kf.is_empty() {
245            panic!("KeyframesSpec must have at least one keyframe");
246        }
247        // Find the segment containing t
248        for i in 0..kf.len() - 1 {
249            let (t0, _, _) = kf[i];
250            let (t1, ref v1, easing) = kf[i + 1];
251            if t >= t0 && t <= t1 {
252                let segment_t = if (t1 - t0).abs() < f32::EPSILON {
253                    1.0
254                } else {
255                    (t - t0) / (t1 - t0)
256                };
257                let eased_t = match easing {
258                    Some(e) => e.interpolate(segment_t),
259                    None => segment_t,
260                };
261                return kf[i].1.interpolate(v1, eased_t);
262            }
263        }
264        kf.last().unwrap().1.clone()
265    }
266}
267
268/// A repeatable animation specification.
269///
270/// Wraps another animation spec and causes it to repeat.
271/// Default: infinite repeat with no reverse.
272#[derive(Clone, Copy, Debug)]
273pub struct RepeatableSpec {
274    /// Number of repetitions. `None` means infinite.
275    pub iterations: Option<u32>,
276    /// If true, alternate direction each iteration (forward, backward, forward...).
277    pub reverse: bool,
278    /// Delay between each iteration.
279    pub delay_between: Duration,
280}
281
282impl Default for RepeatableSpec {
283    fn default() -> Self {
284        Self {
285            iterations: None,
286            reverse: false,
287            delay_between: Duration::ZERO,
288        }
289    }
290}
291
292impl RepeatableSpec {
293    pub fn new(iterations: u32) -> Self {
294        Self {
295            iterations: Some(iterations),
296            reverse: false,
297            delay_between: Duration::ZERO,
298        }
299    }
300
301    pub fn infinite() -> Self {
302        Self {
303            iterations: None,
304            reverse: false,
305            delay_between: Duration::ZERO,
306        }
307    }
308
309    pub fn reverse(mut self) -> Self {
310        self.reverse = true;
311        self
312    }
313
314    pub fn delay_between(mut self, d: Duration) -> Self {
315        self.delay_between = d;
316        self
317    }
318}
319
320/// Decay animation configuration.
321///
322/// Models a damped decay (e.g., for fling-to-stop animations).
323#[derive(Clone, Copy, Debug)]
324pub struct DecayAnimationSpec {
325    /// How quickly the animation decelerates. Lower = faster stop.
326    pub friction: f32,
327    /// Minimum velocity threshold to stop.
328    pub stop_threshold: f32,
329}
330
331impl Default for DecayAnimationSpec {
332    fn default() -> Self {
333        Self {
334            friction: 0.8,
335            stop_threshold: 1.0,
336        }
337    }
338}
339
340impl DecayAnimationSpec {
341    pub fn new(friction: f32) -> Self {
342        Self {
343            friction: friction.clamp(0.01, 1.0),
344            stop_threshold: 1.0,
345        }
346    }
347}
348
349impl AnimatedValue<f32> {
350    /// Tick the decay animation. Returns `true` if still animating.
351    pub fn update_decay(&mut self, friction: f32, stop_threshold: f32) -> bool {
352        let _start = match self.start_time {
353            Some(s) => s,
354            None => return false,
355        };
356
357        let now = now();
358        let dt = match self.last_update {
359            Some(last) => now.saturating_duration_since(last).as_secs_f32().min(0.05),
360            None => 0.0,
361        };
362        self.last_update = Some(now);
363
364        if dt <= 0.0 {
365            return true;
366        }
367
368        if self.velocity.abs() < stop_threshold {
369            self.velocity = 0.0;
370            self.start_time = None;
371            return false;
372        }
373
374        self.velocity *= friction.powf(dt * 60.0);
375        let delta = self.velocity * dt;
376        // We store the "current value" as a single f32 offset
377        // that accumulates. But AnimatedValue<f32> stores explicit
378        // start/target. For decay we just accumulate the current.
379        // Because of the AnimatedValue structure, we use progress as
380        // the accumulated value relative to start.
381        let new_progress = self.progress + delta;
382        self.progress = new_progress;
383        // current = start + (target - start) * progress but target = ???.
384        // For decay, progress IS the value (starting from 0).
385        // We repurpose: current = start + progress (progress is offset from start).
386        // Since T = f32, we can just set current directly.
387        if self.progress.abs() < 0.001 && self.velocity.abs() < stop_threshold {
388            self.progress = 0.0;
389            self.velocity = 0.0;
390            self.start_time = None;
391            return false;
392        }
393
394        self.current = self.start.interpolate(&self.target, self.progress);
395        true
396    }
397}
398
399pub trait Interpolate {
400    fn interpolate(&self, other: &Self, t: f32) -> Self;
401}
402
403impl Interpolate for f32 {
404    fn interpolate(&self, other: &Self, t: f32) -> Self {
405        self + (other - self) * t
406    }
407}
408
409impl Interpolate for crate::Color {
410    fn interpolate(&self, other: &Self, t: f32) -> Self {
411        let lerp = |a: u8, b: u8| {
412            (a as f32 + (b as f32 - a as f32) * t)
413                .round()
414                .clamp(0.0, 255.0) as u8
415        };
416        crate::Color(
417            lerp(self.0, other.0),
418            lerp(self.1, other.1),
419            lerp(self.2, other.2),
420            lerp(self.3, other.3),
421        )
422    }
423}
424
425impl Interpolate for crate::Vec2 {
426    fn interpolate(&self, other: &Self, t: f32) -> Self {
427        crate::Vec2 {
428            x: self.x.interpolate(&other.x, t),
429            y: self.y.interpolate(&other.y, t),
430        }
431    }
432}
433
434impl Interpolate for crate::Size {
435    fn interpolate(&self, other: &Self, t: f32) -> Self {
436        crate::Size {
437            width: self.width.interpolate(&other.width, t),
438            height: self.height.interpolate(&other.height, t),
439        }
440    }
441}
442
443impl Interpolate for crate::Rect {
444    fn interpolate(&self, other: &Self, t: f32) -> Self {
445        crate::Rect {
446            x: self.x.interpolate(&other.x, t),
447            y: self.y.interpolate(&other.y, t),
448            w: self.w.interpolate(&other.w, t),
449            h: self.h.interpolate(&other.h, t),
450        }
451    }
452}
453
454// Animation clock
455pub trait Clock: Send + Sync + 'static {
456    fn now(&self) -> Instant;
457}
458
459pub struct SystemClock;
460impl Clock for SystemClock {
461    fn now(&self) -> Instant {
462        Instant::now()
463    }
464}
465
466static CLOCK: OnceLock<RwLock<Box<dyn Clock>>> = OnceLock::new();
467
468/// Install a global animation clock. Platform sets this to SystemClock; tests can set TestClock.
469pub fn set_clock(clock: Box<dyn Clock>) {
470    let lock = CLOCK.get_or_init(|| RwLock::new(Box::new(SystemClock) as Box<dyn Clock>));
471    *lock.write() = clock;
472}
473/// Install default system clock if none present (idempotent).
474pub fn ensure_system_clock() {
475    let _ = CLOCK.get_or_init(|| RwLock::new(Box::new(SystemClock) as Box<dyn Clock>));
476}
477
478/// A test clock you can drive deterministically.
479#[derive(Clone)]
480pub struct TestClock {
481    pub t: Instant,
482}
483impl Clock for TestClock {
484    fn now(&self) -> Instant {
485        self.t
486    }
487}
488
489/// Animated value that transitions smoothly.
490///
491/// Supports two modes:
492/// - **Tween** (when `spec.spring` is `None`): interpolates between `start` and `target`
493///   over a fixed duration using an easing curve.
494/// - **Spring** (when `spec.spring` is `Some`): numerically integrates a physical spring ODE
495///   (`x'' = -k·(x - target) - d·x'`) with emergent duration. When the target changes
496///   mid-animation, the current value and velocity carry forward seamlessly.
497pub struct AnimatedValue<T: Interpolate + Clone> {
498    current: T,
499    target: T,
500    start: T,
501    spec: AnimationSpec,
502    keyframes: Option<KeyframesSpec<T>>,
503    iteration: u32,
504    start_time: Option<Instant>,
505    // Spring simulation state (progress-based, works for any T: Interpolate)
506    progress: f32,
507    velocity: f32,
508    last_update: Option<Instant>,
509}
510
511impl<T: Interpolate + Clone> AnimatedValue<T> {
512    pub fn new(initial: T, spec: AnimationSpec) -> Self {
513        Self {
514            current: initial.clone(),
515            target: initial.clone(),
516            start: initial,
517            spec,
518            keyframes: None,
519            iteration: 0,
520            start_time: None,
521            progress: 1.0,
522            velocity: 0.0,
523            last_update: None,
524        }
525    }
526
527    pub fn set_spec(&mut self, spec: AnimationSpec) {
528        self.spec = spec;
529    }
530
531    /// Set a keyframes spec for multi-stage animation.
532    /// When set, `set_target` is ignored and the value is driven by the keyframe sequence.
533    pub fn set_keyframes(&mut self, keyframes: KeyframesSpec<T>) {
534        self.keyframes = Some(keyframes);
535        self.start_time = Some(now());
536        self.last_update = None;
537        self.iteration = 0;
538    }
539
540    pub fn set_target(&mut self, target: T) {
541        if self.start_time.is_some() {
542            self.update();
543        }
544        self.keyframes = None;
545        self.start = self.current.clone();
546        self.target = target;
547        self.start_time = Some(now());
548        self.last_update = None;
549        self.iteration = 0;
550        if self.spec.spring.is_some() {
551            // Spring mode: start progress at 0 (the current value), carry velocity forward
552            self.progress = 0.0;
553        }
554    }
555
556    /// Snap immediately to a value without animating.
557    pub fn snap_to(&mut self, value: T) {
558        self.current = value.clone();
559        self.target = value.clone();
560        self.start = value;
561        self.keyframes = None;
562        self.start_time = None;
563        self.progress = 1.0;
564        self.velocity = 0.0;
565        self.last_update = None;
566    }
567
568    pub fn update(&mut self) -> bool {
569        let spring_spec = self.spec.spring;
570        let still = if let Some(spring) = spring_spec {
571            self.update_spring(&spring)
572        } else if self.keyframes.is_some() {
573            self.update_keyframes()
574        } else {
575            self.update_tween()
576        };
577
578        if !still {
579            // Check if we should repeat
580            if let Some(repeat) = &self.spec.repeat {
581                let maxed = repeat
582                    .iterations
583                    .is_some_and(|max| self.iteration + 1 >= max);
584                if !maxed {
585                    self.iteration += 1;
586                    if repeat.reverse {
587                        std::mem::swap(&mut self.start, &mut self.target);
588                    }
589                    self.progress = 0.0;
590                    self.velocity = 0.0;
591                    self.start_time = Some(now());
592                    self.last_update = None;
593                    return true;
594                }
595            }
596        }
597
598        still
599    }
600
601    fn update_keyframes(&mut self) -> bool {
602        let start = match self.start_time {
603            Some(s) => s,
604            None => return false,
605        };
606        let elapsed = now().saturating_duration_since(start);
607        if elapsed < self.spec.delay {
608            return true;
609        }
610        let animation_time = elapsed - self.spec.delay;
611        if animation_time >= self.spec.duration {
612            if let Some(ref kf) = self.keyframes {
613                self.current = kf.evaluate(1.0);
614            }
615            self.start_time = None;
616            return false;
617        }
618        let t = (animation_time.as_secs_f32() / self.spec.duration.as_secs_f32()).clamp(0.0, 1.0);
619        let eased_t = self.spec.easing.interpolate(t).clamp(0.0, 1.0);
620        if let Some(ref kf) = self.keyframes {
621            self.current = kf.evaluate(eased_t);
622        }
623        true
624    }
625
626    fn update_spring(&mut self, spring: &SpringSpec) -> bool {
627        let start = match self.start_time {
628            Some(s) => s,
629            None => return false,
630        };
631
632        let now = now();
633        let dt = match self.last_update {
634            Some(last) => now.saturating_duration_since(last).as_secs_f32().min(0.05),
635            None => 0.0,
636        };
637        self.last_update = Some(now);
638
639        // Still in delay phase
640        let elapsed = now.saturating_duration_since(start);
641        if elapsed < self.spec.delay {
642            return true;
643        }
644
645        if dt <= 0.0 {
646            return true;
647        }
648
649        // Spring ODE (progress-based, target progress = 1.0)
650        let k = spring.stiffness;
651        let d = 2.0 * spring.damping_ratio * k.sqrt();
652        let displacement = self.progress - 1.0;
653
654        if displacement.abs() < 0.005 && self.velocity.abs() < 0.1 {
655            // Settled
656            self.progress = 1.0;
657            self.velocity = 0.0;
658            self.current = self.target.clone();
659            self.start_time = None;
660            self.last_update = None;
661            return false;
662        }
663
664        // Semi-implicit Euler (symplectic integrator, more stable than explicit)
665        let acceleration = -k * displacement - d * self.velocity;
666        self.velocity += acceleration * dt;
667        self.progress += self.velocity * dt;
668
669        // Clamp progress to prevent extreme overshoot
670        self.progress = self.progress.clamp(-0.1, 2.0);
671
672        self.current = self.start.interpolate(&self.target, self.progress);
673        true
674    }
675
676    fn update_tween(&mut self) -> bool {
677        if let Some(start) = self.start_time {
678            let elapsed = now().saturating_duration_since(start);
679
680            if elapsed < self.spec.delay {
681                return true;
682            }
683
684            let animation_time = elapsed - self.spec.delay;
685
686            if animation_time >= self.spec.duration {
687                self.current = self.target.clone();
688                self.start_time = None;
689                return false;
690            }
691
692            let t =
693                (animation_time.as_secs_f32() / self.spec.duration.as_secs_f32()).clamp(0.0, 1.0);
694            let eased_t = self.spec.easing.interpolate(t);
695            let eased_t = eased_t.clamp(0.0, 1.0);
696
697            self.current = self.start.interpolate(&self.target, eased_t);
698            true
699        } else {
700            false
701        }
702    }
703
704    pub fn get(&self) -> &T {
705        &self.current
706    }
707
708    pub fn is_animating(&self) -> bool {
709        self.start_time.is_some()
710    }
711
712    pub fn has_keyframes(&self) -> bool {
713        self.keyframes.is_some()
714    }
715}