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}
138
139impl Default for AnimationSpec {
140    fn default() -> Self {
141        Self {
142            duration: Duration::from_millis(300),
143            easing: Easing::EaseInOut,
144            delay: Duration::ZERO,
145            spring: None,
146        }
147    }
148}
149
150impl AnimationSpec {
151    pub fn tween(duration: Duration, easing: Easing) -> Self {
152        Self {
153            duration,
154            easing,
155            delay: Duration::ZERO,
156            spring: None,
157        }
158    }
159    /// True physical spring simulation - duration is emergent, no fixed duration needed.
160    pub fn spring(spring: SpringSpec) -> Self {
161        Self {
162            duration: Duration::ZERO,
163            easing: Easing::Linear,
164            delay: Duration::ZERO,
165            spring: Some(spring),
166        }
167    }
168    /// Gentle underdamped preset (small overshoot). Uses true spring physics.
169    pub fn spring_gentle() -> Self {
170        Self::spring(SpringSpec::gentle())
171    }
172    /// Bouncier underdamped preset. Uses true spring physics.
173    pub fn spring_bouncy() -> Self {
174        Self::spring(SpringSpec::bouncy())
175    }
176    /// Critically damped spring with given omega (angular frequency). Uses true spring physics.
177    pub fn spring_crit(omega: f32) -> Self {
178        Self::spring(SpringSpec::new(1.0, omega * omega))
179    }
180
181    pub fn fast() -> Self {
182        Self {
183            duration: Duration::from_millis(150),
184            easing: Easing::EaseOut,
185            delay: Duration::ZERO,
186            spring: None,
187        }
188    }
189
190    pub fn slow() -> Self {
191        Self {
192            duration: Duration::from_millis(600),
193            easing: Easing::EaseInOut,
194            delay: Duration::ZERO,
195            spring: None,
196        }
197    }
198
199    /// 120ms FastOutSlowIn
200    pub fn m3_elevation_in() -> Self {
201        Self {
202            duration: Duration::from_millis(120),
203            easing: Easing::FastOutSlowIn,
204            delay: Duration::ZERO,
205            spring: None,
206        }
207    }
208
209    /// 150ms with standard deceleration bezier
210    pub fn m3_elevation_out() -> Self {
211        Self {
212            duration: Duration::from_millis(150),
213            easing: Easing::EaseOut,
214            delay: Duration::ZERO,
215            spring: None,
216        }
217    }
218}
219
220/// A keyframe animation specification.
221///
222/// Defines a sequence of keyframes at specific timestamps (0.0 to 1.0),
223/// with target values and optional easing between each pair.
224#[derive(Clone, Debug)]
225pub struct KeyframesSpec<T: Clone> {
226    /// Keyframes as (timestamp 0.0-1.0, value, optional easing between previous and this).
227    /// The first keyframe should be at t=0.0 and uses no easing.
228    pub keyframes: Vec<(f32, T, Option<Easing>)>,
229}
230
231impl<T: Clone + Interpolate> KeyframesSpec<T> {
232    pub fn new(keyframes: Vec<(f32, T)>) -> Self {
233        let with_easing = keyframes.into_iter().map(|(t, v)| (t, v, None)).collect();
234        Self {
235            keyframes: with_easing,
236        }
237    }
238
239    /// Add easing between the previous keyframe and this one.
240    pub fn with_easing(mut self, easing: Easing) -> Self {
241        if let Some(last) = self.keyframes.last_mut() {
242            last.2 = Some(easing);
243        }
244        self
245    }
246
247    pub fn evaluate(&self, t: f32) -> T {
248        let t = t.clamp(0.0, 1.0);
249        let kf = &self.keyframes;
250        if kf.is_empty() {
251            panic!("KeyframesSpec must have at least one keyframe");
252        }
253        // Find the segment containing t
254        for i in 0..kf.len() - 1 {
255            let (t0, _, _) = kf[i];
256            let (t1, ref v1, easing) = kf[i + 1];
257            if t >= t0 && t <= t1 {
258                let segment_t = if (t1 - t0).abs() < f32::EPSILON {
259                    1.0
260                } else {
261                    (t - t0) / (t1 - t0)
262                };
263                let eased_t = match easing {
264                    Some(e) => e.interpolate(segment_t),
265                    None => segment_t,
266                };
267                return kf[i].1.interpolate(v1, eased_t);
268            }
269        }
270        kf.last().unwrap().1.clone()
271    }
272}
273
274/// A repeatable animation specification.
275///
276/// Wraps another animation spec and causes it to repeat.
277/// Default: infinite repeat with no reverse.
278#[derive(Clone, Copy, Debug)]
279pub struct RepeatableSpec {
280    /// Number of repetitions. `None` means infinite.
281    pub iterations: Option<u32>,
282    /// If true, alternate direction each iteration (forward, backward, forward...).
283    pub reverse: bool,
284    /// Delay between each iteration.
285    pub delay_between: Duration,
286}
287
288impl Default for RepeatableSpec {
289    fn default() -> Self {
290        Self {
291            iterations: None,
292            reverse: false,
293            delay_between: Duration::ZERO,
294        }
295    }
296}
297
298impl RepeatableSpec {
299    pub fn new(iterations: u32) -> Self {
300        Self {
301            iterations: Some(iterations),
302            reverse: false,
303            delay_between: Duration::ZERO,
304        }
305    }
306
307    pub fn infinite() -> Self {
308        Self {
309            iterations: None,
310            reverse: false,
311            delay_between: Duration::ZERO,
312        }
313    }
314
315    pub fn reverse(mut self) -> Self {
316        self.reverse = true;
317        self
318    }
319
320    pub fn delay_between(mut self, d: Duration) -> Self {
321        self.delay_between = d;
322        self
323    }
324}
325
326/// Decay animation configuration.
327///
328/// Models a damped decay (e.g., for fling-to-stop animations).
329#[derive(Clone, Copy, Debug)]
330pub struct DecayAnimationSpec {
331    /// How quickly the animation decelerates. Lower = faster stop.
332    pub friction: f32,
333    /// Minimum velocity threshold to stop.
334    pub stop_threshold: f32,
335}
336
337impl Default for DecayAnimationSpec {
338    fn default() -> Self {
339        Self {
340            friction: 0.8,
341            stop_threshold: 1.0,
342        }
343    }
344}
345
346impl DecayAnimationSpec {
347    pub fn new(friction: f32) -> Self {
348        Self {
349            friction: friction.clamp(0.01, 1.0),
350            stop_threshold: 1.0,
351        }
352    }
353}
354
355impl AnimatedValue<f32> {
356    /// Tick the decay animation. Returns `true` if still animating.
357    pub fn update_decay(&mut self, friction: f32, stop_threshold: f32) -> bool {
358        let start = match self.start_time {
359            Some(s) => s,
360            None => return false,
361        };
362
363        let now = now();
364        let dt = match self.last_update {
365            Some(last) => now.saturating_duration_since(last).as_secs_f32().min(0.05),
366            None => 0.0,
367        };
368        self.last_update = Some(now);
369
370        if dt <= 0.0 {
371            return true;
372        }
373
374        if self.velocity.abs() < stop_threshold {
375            self.velocity = 0.0;
376            self.start_time = None;
377            return false;
378        }
379
380        self.velocity *= friction.powf(dt * 60.0);
381        let delta = self.velocity * dt;
382        // We store the "current value" as a single f32 offset
383        // that accumulates. But AnimatedValue<f32> stores explicit
384        // start/target. For decay we just accumulate the current.
385        // Because of the AnimatedValue structure, we use progress as
386        // the accumulated value relative to start.
387        let new_progress = self.progress + delta;
388        self.progress = new_progress;
389        // current = start + (target - start) * progress but target = ???.
390        // For decay, progress IS the value (starting from 0).
391        // We repurpose: current = start + progress (progress is offset from start).
392        // Since T = f32, we can just set current directly.
393        if self.progress.abs() < 0.001 && self.velocity.abs() < stop_threshold {
394            self.progress = 0.0;
395            self.velocity = 0.0;
396            self.start_time = None;
397            return false;
398        }
399
400        self.current = self.start.interpolate(&self.target, self.progress);
401        true
402    }
403}
404
405pub trait Interpolate {
406    fn interpolate(&self, other: &Self, t: f32) -> Self;
407}
408
409impl Interpolate for f32 {
410    fn interpolate(&self, other: &Self, t: f32) -> Self {
411        self + (other - self) * t
412    }
413}
414
415impl Interpolate for crate::Color {
416    fn interpolate(&self, other: &Self, t: f32) -> Self {
417        let lerp = |a: u8, b: u8| {
418            (a as f32 + (b as f32 - a as f32) * t)
419                .round()
420                .clamp(0.0, 255.0) as u8
421        };
422        crate::Color(
423            lerp(self.0, other.0),
424            lerp(self.1, other.1),
425            lerp(self.2, other.2),
426            lerp(self.3, other.3),
427        )
428    }
429}
430
431// Animation clock
432pub trait Clock: Send + Sync + 'static {
433    fn now(&self) -> Instant;
434}
435
436pub struct SystemClock;
437impl Clock for SystemClock {
438    fn now(&self) -> Instant {
439        Instant::now()
440    }
441}
442
443static CLOCK: OnceLock<RwLock<Box<dyn Clock>>> = OnceLock::new();
444
445/// Install a global animation clock. Platform sets this to SystemClock; tests can set TestClock.
446pub fn set_clock(clock: Box<dyn Clock>) {
447    let lock = CLOCK.get_or_init(|| RwLock::new(Box::new(SystemClock) as Box<dyn Clock>));
448    *lock.write() = clock;
449}
450/// Install default system clock if none present (idempotent).
451pub fn ensure_system_clock() {
452    let _ = CLOCK.get_or_init(|| RwLock::new(Box::new(SystemClock) as Box<dyn Clock>));
453}
454
455/// A test clock you can drive deterministically.
456#[derive(Clone)]
457pub struct TestClock {
458    pub t: Instant,
459}
460impl Clock for TestClock {
461    fn now(&self) -> Instant {
462        self.t
463    }
464}
465
466/// Animated value that transitions smoothly.
467///
468/// Supports two modes:
469/// - **Tween** (when `spec.spring` is `None`): interpolates between `start` and `target`
470///   over a fixed duration using an easing curve.
471/// - **Spring** (when `spec.spring` is `Some`): numerically integrates a physical spring ODE
472///   (`x'' = -k·(x - target) - d·x'`) with emergent duration. When the target changes
473///   mid-animation, the current value and velocity carry forward seamlessly.
474pub struct AnimatedValue<T: Interpolate + Clone> {
475    current: T,
476    target: T,
477    start: T,
478    spec: AnimationSpec,
479    start_time: Option<Instant>,
480    // Spring simulation state (progress-based, works for any T: Interpolate)
481    progress: f32,
482    velocity: f32,
483    last_update: Option<Instant>,
484}
485
486impl<T: Interpolate + Clone> AnimatedValue<T> {
487    pub fn new(initial: T, spec: AnimationSpec) -> Self {
488        Self {
489            current: initial.clone(),
490            target: initial.clone(),
491            start: initial,
492            spec,
493            start_time: None,
494            progress: 1.0,
495            velocity: 0.0,
496            last_update: None,
497        }
498    }
499
500    pub fn set_spec(&mut self, spec: AnimationSpec) {
501        self.spec = spec;
502    }
503
504    pub fn set_target(&mut self, target: T) {
505        if self.start_time.is_some() {
506            self.update();
507        }
508        self.start = self.current.clone();
509        self.target = target;
510        self.start_time = Some(now());
511        self.last_update = None;
512        if self.spec.spring.is_some() {
513            // Spring mode: start progress at 0 (the current value), carry velocity forward
514            self.progress = 0.0;
515        }
516    }
517
518    /// Snap immediately to a value without animating.
519    pub fn snap_to(&mut self, value: T) {
520        self.current = value.clone();
521        self.target = value.clone();
522        self.start = value;
523        self.start_time = None;
524        self.progress = 1.0;
525        self.velocity = 0.0;
526        self.last_update = None;
527    }
528
529    pub fn update(&mut self) -> bool {
530        // Clone the spring spec to avoid borrowing self.spec during mutable self access
531        let spring_spec = self.spec.spring;
532        if let Some(spring) = spring_spec {
533            self.update_spring(&spring)
534        } else {
535            self.update_tween()
536        }
537    }
538
539    fn update_spring(&mut self, spring: &SpringSpec) -> bool {
540        let start = match self.start_time {
541            Some(s) => s,
542            None => return false,
543        };
544
545        let now = now();
546        let dt = match self.last_update {
547            Some(last) => now.saturating_duration_since(last).as_secs_f32().min(0.05),
548            None => 0.0,
549        };
550        self.last_update = Some(now);
551
552        // Still in delay phase
553        let elapsed = now.saturating_duration_since(start);
554        if elapsed < self.spec.delay {
555            return true;
556        }
557
558        if dt <= 0.0 {
559            return true;
560        }
561
562        // Spring ODE (progress-based, target progress = 1.0)
563        let k = spring.stiffness;
564        let d = 2.0 * spring.damping_ratio * k.sqrt();
565        let displacement = self.progress - 1.0;
566
567        if displacement.abs() < 0.005 && self.velocity.abs() < 0.1 {
568            // Settled
569            self.progress = 1.0;
570            self.velocity = 0.0;
571            self.current = self.target.clone();
572            self.start_time = None;
573            self.last_update = None;
574            return false;
575        }
576
577        // Semi-implicit Euler (symplectic integrator, more stable than explicit)
578        let acceleration = -k * displacement - d * self.velocity;
579        self.velocity += acceleration * dt;
580        self.progress += self.velocity * dt;
581
582        // Clamp progress to prevent extreme overshoot
583        self.progress = self.progress.clamp(-0.1, 2.0);
584
585        self.current = self.start.interpolate(&self.target, self.progress);
586        true
587    }
588
589    fn update_tween(&mut self) -> bool {
590        if let Some(start) = self.start_time {
591            let elapsed = now().saturating_duration_since(start);
592
593            if elapsed < self.spec.delay {
594                return true;
595            }
596
597            let animation_time = elapsed - self.spec.delay;
598
599            if animation_time >= self.spec.duration {
600                self.current = self.target.clone();
601                self.start_time = None;
602                return false;
603            }
604
605            let t =
606                (animation_time.as_secs_f32() / self.spec.duration.as_secs_f32()).clamp(0.0, 1.0);
607            let eased_t = self.spec.easing.interpolate(t);
608            let eased_t = eased_t.clamp(0.0, 1.0);
609
610            self.current = self.start.interpolate(&self.target, eased_t);
611            true
612        } else {
613            false
614        }
615    }
616
617    pub fn get(&self) -> &T {
618        &self.current
619    }
620
621    pub fn is_animating(&self) -> bool {
622        self.start_time.is_some()
623    }
624}