repose_core/
animation.rs

1use std::sync::OnceLock;
2use std::time::{Duration, Instant};
3
4pub(crate) fn now() -> Instant {
5    CLOCK.get().map(|c| c.now()).unwrap_or_else(Instant::now)
6}
7
8#[derive(Clone, Copy, Debug)]
9pub enum Easing {
10    Linear,
11    EaseIn,
12    EaseOut,
13    EaseInOut,
14    Spring { damping: f32, stiffness: f32 },
15}
16
17impl Easing {
18    pub fn interpolate(&self, t: f32) -> f32 {
19        match self {
20            Easing::Linear => t,
21            Easing::EaseIn => t * t,
22            Easing::EaseOut => t * (2.0 - t),
23            Easing::EaseInOut => {
24                if t < 0.5 {
25                    2.0 * t * t
26                } else {
27                    -1.0 + (4.0 - 2.0 * t) * t
28                }
29            }
30            Easing::Spring { damping, stiffness } => {
31                // Simplified spring physics
32                let omega = (stiffness / damping).sqrt();
33                let zeta = damping / (2.0 * (stiffness * damping).sqrt());
34
35                if zeta < 1.0 {
36                    // Underdamped
37                    let omega_d = omega * (1.0 - zeta * zeta).sqrt();
38                    let t = t * 2.0; // Adjust time scale
39                    1.0 - ((-zeta * omega * t).exp() * (omega_d * t).cos())
40                } else {
41                    // Overdamped or critically damped - fallback to ease out
42                    t * (2.0 - t)
43                }
44            }
45        }
46    }
47}
48
49#[derive(Clone, Copy, Debug)]
50pub struct AnimationSpec {
51    pub duration: Duration,
52    pub easing: Easing,
53    pub delay: Duration,
54}
55
56impl Default for AnimationSpec {
57    fn default() -> Self {
58        Self {
59            duration: Duration::from_millis(300),
60            easing: Easing::EaseInOut,
61            delay: Duration::ZERO,
62        }
63    }
64}
65
66impl AnimationSpec {
67    pub fn tween(duration: Duration, easing: Easing) -> Self {
68        Self {
69            duration,
70            easing,
71            delay: Duration::ZERO,
72        }
73    }
74    pub fn spring() -> Self {
75        Self {
76            duration: Duration::from_millis(500),
77            easing: Easing::Spring {
78                damping: 0.8,
79                stiffness: 200.0,
80            },
81            delay: Duration::ZERO,
82        }
83    }
84    pub fn spring_phys(damping: f32, stiffness: f32, duration: Duration) -> Self {
85        Self {
86            duration,
87            easing: Easing::Spring { damping, stiffness },
88            delay: Duration::ZERO,
89        }
90    }
91    pub fn fast() -> Self {
92        Self {
93            duration: Duration::from_millis(150),
94            easing: Easing::EaseOut,
95            delay: Duration::ZERO,
96        }
97    }
98
99    pub fn slow() -> Self {
100        Self {
101            duration: Duration::from_millis(600),
102            easing: Easing::EaseInOut,
103            delay: Duration::ZERO,
104        }
105    }
106}
107
108pub trait Interpolate {
109    fn interpolate(&self, other: &Self, t: f32) -> Self;
110}
111
112impl Interpolate for f32 {
113    fn interpolate(&self, other: &Self, t: f32) -> Self {
114        self + (other - self) * t
115    }
116}
117
118impl Interpolate for crate::Color {
119    fn interpolate(&self, other: &Self, t: f32) -> Self {
120        crate::Color(
121            (self.0 as f32 + (other.0 as f32 - self.0 as f32) * t) as u8,
122            (self.1 as f32 + (other.1 as f32 - self.1 as f32) * t) as u8,
123            (self.2 as f32 + (other.2 as f32 - self.2 as f32) * t) as u8,
124            (self.3 as f32 + (other.3 as f32 - self.3 as f32) * t) as u8,
125        )
126    }
127}
128
129// Animation clock
130pub trait Clock: Send + Sync + 'static {
131    fn now(&self) -> Instant;
132}
133
134pub struct SystemClock;
135impl Clock for SystemClock {
136    fn now(&self) -> Instant {
137        Instant::now()
138    }
139}
140
141static CLOCK: OnceLock<Box<dyn Clock>> = OnceLock::new();
142
143/// Install a global animation clock. Platform sets this to SystemClock; tests can set TestClock.
144pub fn set_clock(clock: Box<dyn Clock>) {
145    let _ = CLOCK.set(clock);
146}
147/// Install default system clock if none present (idempotent).
148pub(crate) fn ensure_system_clock() {
149    let _ = CLOCK.set(Box::new(SystemClock));
150}
151
152/// A test clock you can drive deterministically.
153#[derive(Clone)]
154pub struct TestClock {
155    pub t: Instant,
156}
157impl Clock for TestClock {
158    fn now(&self) -> Instant {
159        self.t
160    }
161}
162
163/// Animated value that transitions smoothly
164pub struct AnimatedValue<T: Interpolate + Clone> {
165    current: T,
166    target: T,
167    start: T,
168    spec: AnimationSpec,
169    start_time: Option<Instant>,
170}
171
172impl<T: Interpolate + Clone> AnimatedValue<T> {
173    pub fn new(initial: T, spec: AnimationSpec) -> Self {
174        Self {
175            current: initial.clone(),
176            target: initial.clone(),
177            start: initial,
178            spec,
179            start_time: None,
180        }
181    }
182
183    pub fn set_target(&mut self, target: T) {
184        if self.start_time.is_none() {
185            self.start = self.current.clone();
186        }
187        self.target = target;
188        self.start_time = Some(now());
189    }
190
191    pub fn update(&mut self) -> bool {
192        if let Some(start) = self.start_time {
193            let elapsed = now().saturating_duration_since(start);
194
195            if elapsed < self.spec.delay {
196                return true; // Still waiting for delay
197            }
198
199            let animation_time = elapsed - self.spec.delay;
200
201            if animation_time >= self.spec.duration {
202                self.current = self.target.clone();
203                self.start_time = None;
204                return false; // Animation complete
205            }
206
207            let t = animation_time.as_secs_f32() / self.spec.duration.as_secs_f32();
208            let eased_t = self.spec.easing.interpolate(t);
209            self.current = self.start.interpolate(&self.target, eased_t);
210
211            true // Animation ongoing
212        } else {
213            false // No animation
214        }
215    }
216
217    pub fn get(&self) -> &T {
218        &self.current
219    }
220
221    pub fn is_animating(&self) -> bool {
222        self.start_time.is_some()
223    }
224}