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