repose_core/
animation.rs

1use parking_lot::RwLock;
2use std::sync::OnceLock;
3use std::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#[derive(Clone, Copy, Debug)]
11pub enum Easing {
12    Linear,
13    EaseIn,
14    EaseOut,
15    EaseInOut,
16    /// Monotonic, critically-damped, y(t)=1-(1+ω t)e^{-ω t}, t∈[0,1].
17    SpringCrit {
18        omega: f32,
19    },
20    /// Underdamped, low-overshoot preset (ζ≈0.5, ω≈8)
21    SpringGentle,
22    /// Underdamped, bouncier preset (ζ≈0.2, ω≈12)
23    SpringBouncy,
24}
25
26impl Easing {
27    pub fn interpolate(&self, t: f32) -> f32 {
28        match self {
29            Easing::Linear => t,
30            Easing::EaseIn => t * t,
31            Easing::EaseOut => t * (2.0 - t),
32            Easing::EaseInOut => {
33                if t < 0.5 {
34                    2.0 * t * t
35                } else {
36                    -1.0 + (4.0 - 2.0 * t) * t
37                }
38            }
39            Easing::SpringCrit { omega } => {
40                let w = (*omega).max(0.0);
41                let tt = t.max(0.0);
42                // y = 1 - (1 + w t) e^{-w t}
43                1.0 - (1.0 + w * tt) * (-(w * tt)).exp()
44            }
45            Easing::SpringGentle => spring_underdamped_normalized(t, 0.5, 8.0),
46            Easing::SpringBouncy => spring_underdamped_normalized(t, 0.2, 12.0),
47        }
48    }
49}
50
51fn spring_underdamped_normalized(t: f32, zeta: f32, omega: f32) -> f32 {
52    let tt = t.max(0.0);
53    let z = zeta.clamp(0.0, 0.999);
54    let w = omega.max(0.0);
55    let wd = w * (1.0 - z * z).sqrt();
56    let exp_term = (-z * w * tt).exp();
57    let cos_term = (wd * tt).cos();
58    let sin_term = (wd * tt).sin();
59    // Standard second-order underdamped unit-step response
60    let c = z / (1.0 - z * z).sqrt();
61    let y = 1.0 - exp_term * (cos_term + c * sin_term);
62    y.clamp(0.0, 1.0)
63}
64
65#[derive(Clone, Copy, Debug)]
66pub struct AnimationSpec {
67    pub duration: Duration,
68    pub easing: Easing,
69    pub delay: Duration,
70}
71
72impl Default for AnimationSpec {
73    fn default() -> Self {
74        Self {
75            duration: Duration::from_millis(300),
76            easing: Easing::EaseInOut,
77            delay: Duration::ZERO,
78        }
79    }
80}
81
82impl AnimationSpec {
83    pub fn tween(duration: Duration, easing: Easing) -> Self {
84        Self {
85            duration,
86            easing,
87            delay: Duration::ZERO,
88        }
89    }
90    /// Critically-damped monotonic spring (no overshoot).
91    pub fn spring_crit(omega: f32, duration: Duration) -> Self {
92        Self {
93            duration,
94            easing: Easing::SpringCrit { omega },
95            delay: Duration::ZERO,
96        }
97    }
98    /// Gentle underdamped preset (small overshoot).
99    pub fn spring_gentle() -> Self {
100        Self {
101            duration: Duration::from_millis(450),
102            easing: Easing::SpringGentle,
103            delay: Duration::ZERO,
104        }
105    }
106    /// Bouncier underdamped preset.
107    pub fn spring_bouncy() -> Self {
108        Self {
109            duration: Duration::from_millis(700),
110            easing: Easing::SpringBouncy,
111            delay: Duration::ZERO,
112        }
113    }
114
115    pub fn fast() -> Self {
116        Self {
117            duration: Duration::from_millis(150),
118            easing: Easing::EaseOut,
119            delay: Duration::ZERO,
120        }
121    }
122
123    pub fn slow() -> Self {
124        Self {
125            duration: Duration::from_millis(600),
126            easing: Easing::EaseInOut,
127            delay: Duration::ZERO,
128        }
129    }
130}
131
132pub trait Interpolate {
133    fn interpolate(&self, other: &Self, t: f32) -> Self;
134}
135
136impl Interpolate for f32 {
137    fn interpolate(&self, other: &Self, t: f32) -> Self {
138        self + (other - self) * t
139    }
140}
141
142impl Interpolate for crate::Color {
143    fn interpolate(&self, other: &Self, t: f32) -> Self {
144        crate::Color(
145            (self.0 as f32 + (other.0 as f32 - self.0 as f32) * t) as u8,
146            (self.1 as f32 + (other.1 as f32 - self.1 as f32) * t) as u8,
147            (self.2 as f32 + (other.2 as f32 - self.2 as f32) * t) as u8,
148            (self.3 as f32 + (other.3 as f32 - self.3 as f32) * t) as u8,
149        )
150    }
151}
152
153// Animation clock
154pub trait Clock: Send + Sync + 'static {
155    fn now(&self) -> Instant;
156}
157
158pub struct SystemClock;
159impl Clock for SystemClock {
160    fn now(&self) -> Instant {
161        Instant::now()
162    }
163}
164
165static CLOCK: OnceLock<RwLock<Box<dyn Clock>>> = OnceLock::new();
166
167/// Install a global animation clock. Platform sets this to SystemClock; tests can set TestClock.
168pub fn set_clock(clock: Box<dyn Clock>) {
169    let lock = CLOCK.get_or_init(|| RwLock::new(Box::new(SystemClock) as Box<dyn Clock>));
170    *lock.write() = clock;
171}
172/// Install default system clock if none present (idempotent).
173pub(crate) fn ensure_system_clock() {
174    let lock = CLOCK.get_or_init(|| RwLock::new(Box::new(SystemClock) as Box<dyn Clock>));
175}
176
177/// A test clock you can drive deterministically.
178#[derive(Clone)]
179pub struct TestClock {
180    pub t: Instant,
181}
182impl Clock for TestClock {
183    fn now(&self) -> Instant {
184        self.t
185    }
186}
187
188/// Animated value that transitions smoothly
189pub struct AnimatedValue<T: Interpolate + Clone> {
190    current: T,
191    target: T,
192    start: T,
193    spec: AnimationSpec,
194    start_time: Option<Instant>,
195}
196
197impl<T: Interpolate + Clone> AnimatedValue<T> {
198    pub fn new(initial: T, spec: AnimationSpec) -> Self {
199        Self {
200            current: initial.clone(),
201            target: initial.clone(),
202            start: initial,
203            spec,
204            start_time: None,
205        }
206    }
207
208    pub fn set_target(&mut self, target: T) {
209        if self.start_time.is_some() {
210            self.update();
211            self.start = self.current.clone();
212        } else {
213            self.start = self.current.clone();
214        }
215
216        self.target = target;
217        self.start_time = Some(now());
218    }
219
220    pub fn update(&mut self) -> bool {
221        if let Some(start) = self.start_time {
222            let elapsed = now().saturating_duration_since(start);
223
224            if elapsed < self.spec.delay {
225                return true; // Still in delay phase
226            }
227
228            let animation_time = elapsed - self.spec.delay;
229
230            if animation_time >= self.spec.duration {
231                // Animation complete
232                self.current = self.target.clone();
233                self.start_time = None;
234                return false;
235            }
236
237            let t =
238                (animation_time.as_secs_f32() / self.spec.duration.as_secs_f32()).clamp(0.0, 1.0);
239            let eased_t = self.spec.easing.interpolate(t);
240
241            let eased_t = eased_t.clamp(0.0, 1.0);
242
243            self.current = self.start.interpolate(&self.target, eased_t);
244            true
245        } else {
246            false
247        }
248    }
249
250    pub fn get(&self) -> &T {
251        &self.current
252    }
253
254    pub fn is_animating(&self) -> bool {
255        self.start_time.is_some()
256    }
257}