Skip to main content

animato_js/
driver.rs

1//! rAF and scroll driver bindings.
2
3use crate::error::non_negative;
4use crate::keyframe::{KeyframeTrack, KeyframeTrack2D, KeyframeTrack3D, KeyframeTrack4D};
5use crate::path::MotionPath;
6use crate::physics::{Inertia, Inertia2D};
7use crate::spring::{Spring, Spring2D, Spring3D, Spring4D};
8use crate::timeline::Timeline;
9use crate::tween::{Tween, Tween2D, Tween3D, Tween4D};
10use animato_core::Update;
11use animato_driver::ScrollDriver as CoreScrollDriver;
12use std::sync::{Arc, Mutex};
13use wasm_bindgen::prelude::*;
14
15struct DriverSlot {
16    id: u32,
17    animation: Box<dyn Update + Send>,
18    active: bool,
19}
20
21impl core::fmt::Debug for DriverSlot {
22    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
23        f.debug_struct("DriverSlot").field("id", &self.id).finish()
24    }
25}
26
27/// requestAnimationFrame timestamp driver for JavaScript-owned animations.
28#[wasm_bindgen(js_name = RafDriver)]
29#[derive(Debug)]
30pub struct RafDriver {
31    slots: Vec<DriverSlot>,
32    next_id: u32,
33    last_timestamp_ms: Option<f64>,
34    paused: bool,
35    time_scale: f32,
36    max_dt: f32,
37}
38
39#[wasm_bindgen(js_class = RafDriver)]
40impl RafDriver {
41    /// Create an empty rAF driver.
42    #[wasm_bindgen(constructor)]
43    pub fn new() -> Self {
44        Self {
45            slots: Vec::new(),
46            next_id: 1,
47            last_timestamp_ms: None,
48            paused: false,
49            time_scale: 1.0,
50            max_dt: 0.25,
51        }
52    }
53
54    /// Register a scalar tween and return its id.
55    #[wasm_bindgen(js_name = addTween)]
56    pub fn add_tween(&mut self, tween: &Tween) -> u32 {
57        self.add_boxed(Box::new(tween.shared()))
58    }
59
60    /// Register a 2D tween.
61    #[wasm_bindgen(js_name = addTween2D)]
62    pub fn add_tween_2d(&mut self, tween: &Tween2D) -> u32 {
63        self.add_boxed(Box::new(tween.shared()))
64    }
65
66    /// Register a 3D tween.
67    #[wasm_bindgen(js_name = addTween3D)]
68    pub fn add_tween_3d(&mut self, tween: &Tween3D) -> u32 {
69        self.add_boxed(Box::new(tween.shared()))
70    }
71
72    /// Register a 4D tween.
73    #[wasm_bindgen(js_name = addTween4D)]
74    pub fn add_tween_4d(&mut self, tween: &Tween4D) -> u32 {
75        self.add_boxed(Box::new(tween.shared()))
76    }
77
78    /// Register a scalar spring.
79    #[wasm_bindgen(js_name = addSpring)]
80    pub fn add_spring(&mut self, spring: &Spring) -> u32 {
81        self.add_boxed(Box::new(spring.shared()))
82    }
83
84    /// Register a 2D spring.
85    #[wasm_bindgen(js_name = addSpring2D)]
86    pub fn add_spring_2d(&mut self, spring: &Spring2D) -> u32 {
87        self.add_boxed(Box::new(spring.shared()))
88    }
89
90    /// Register a 3D spring.
91    #[wasm_bindgen(js_name = addSpring3D)]
92    pub fn add_spring_3d(&mut self, spring: &Spring3D) -> u32 {
93        self.add_boxed(Box::new(spring.shared()))
94    }
95
96    /// Register a 4D spring.
97    #[wasm_bindgen(js_name = addSpring4D)]
98    pub fn add_spring_4d(&mut self, spring: &Spring4D) -> u32 {
99        self.add_boxed(Box::new(spring.shared()))
100    }
101
102    /// Register a scalar keyframe track.
103    #[wasm_bindgen(js_name = addKeyframes)]
104    pub fn add_keyframes(&mut self, track: &KeyframeTrack) -> u32 {
105        self.add_boxed(Box::new(track.shared()))
106    }
107
108    /// Register a 2D keyframe track.
109    #[wasm_bindgen(js_name = addKeyframes2D)]
110    pub fn add_keyframes_2d(&mut self, track: &KeyframeTrack2D) -> u32 {
111        self.add_boxed(Box::new(track.shared()))
112    }
113
114    /// Register a 3D keyframe track.
115    #[wasm_bindgen(js_name = addKeyframes3D)]
116    pub fn add_keyframes_3d(&mut self, track: &KeyframeTrack3D) -> u32 {
117        self.add_boxed(Box::new(track.shared()))
118    }
119
120    /// Register a 4D keyframe track.
121    #[wasm_bindgen(js_name = addKeyframes4D)]
122    pub fn add_keyframes_4d(&mut self, track: &KeyframeTrack4D) -> u32 {
123        self.add_boxed(Box::new(track.shared()))
124    }
125
126    /// Register a timeline.
127    #[wasm_bindgen(js_name = addTimeline)]
128    pub fn add_timeline(&mut self, timeline: &Timeline) -> u32 {
129        self.add_boxed(Box::new(timeline.shared()))
130    }
131
132    /// Register a motion path.
133    #[wasm_bindgen(js_name = addMotionPath)]
134    pub fn add_motion_path(&mut self, motion: &MotionPath) -> u32 {
135        self.add_boxed(Box::new(motion.shared()))
136    }
137
138    /// Register scalar inertia.
139    #[wasm_bindgen(js_name = addInertia)]
140    pub fn add_inertia(&mut self, inertia: &Inertia) -> u32 {
141        self.add_boxed(Box::new(inertia.shared()))
142    }
143
144    /// Register 2D inertia.
145    #[wasm_bindgen(js_name = addInertia2D)]
146    pub fn add_inertia_2d(&mut self, inertia: &Inertia2D) -> u32 {
147        self.add_boxed(Box::new(inertia.shared()))
148    }
149
150    /// Tick from a browser rAF timestamp in milliseconds.
151    ///
152    /// Returns the seconds delta applied to animations.
153    pub fn tick(&mut self, timestamp_ms: f64) -> f32 {
154        if !timestamp_ms.is_finite() {
155            return 0.0;
156        }
157        let raw_dt = match self.last_timestamp_ms.replace(timestamp_ms) {
158            Some(last) => ((timestamp_ms - last) / 1000.0).max(0.0) as f32,
159            None => 0.0,
160        };
161        if self.paused {
162            return 0.0;
163        }
164        let dt = raw_dt.min(self.max_dt) * self.time_scale;
165        self.tick_dt(dt);
166        dt
167    }
168
169    /// Tick by an explicit seconds delta.
170    #[wasm_bindgen(js_name = tickDt)]
171    pub fn tick_dt(&mut self, dt: f32) {
172        let dt = non_negative(dt, 0.0);
173        for slot in &mut self.slots {
174            slot.active = slot.animation.update(dt);
175        }
176        self.slots.retain(|slot| slot.active);
177    }
178
179    /// Pause ticking.
180    pub fn pause(&mut self) {
181        self.paused = true;
182    }
183
184    /// Resume ticking.
185    pub fn resume(&mut self) {
186        self.paused = false;
187    }
188
189    /// Whether ticking is paused.
190    #[wasm_bindgen(js_name = isPaused)]
191    pub fn is_paused(&self) -> bool {
192        self.paused
193    }
194
195    /// Reset stored timestamp.
196    #[wasm_bindgen(js_name = resetTimestamp)]
197    pub fn reset_timestamp(&mut self) {
198        self.last_timestamp_ms = None;
199    }
200
201    /// Set time scale.
202    #[wasm_bindgen(js_name = setTimeScale)]
203    pub fn set_time_scale(&mut self, scale: f32) {
204        self.time_scale = non_negative(scale, 1.0);
205    }
206
207    /// Set maximum accepted frame delta.
208    #[wasm_bindgen(js_name = setMaxDt)]
209    pub fn set_max_dt(&mut self, max_dt: f32) {
210        self.max_dt = non_negative(max_dt, 0.25);
211    }
212
213    /// Cancel an animation by id.
214    pub fn cancel(&mut self, id: u32) {
215        self.slots.retain(|slot| slot.id != id);
216    }
217
218    /// Cancel all animations.
219    #[wasm_bindgen(js_name = cancelAll)]
220    pub fn cancel_all(&mut self) {
221        self.slots.clear();
222    }
223
224    /// Number of active animations.
225    #[wasm_bindgen(js_name = activeCount)]
226    pub fn active_count(&self) -> usize {
227        self.slots.len()
228    }
229
230    /// Whether an animation id is active.
231    #[wasm_bindgen(js_name = isActive)]
232    pub fn is_active(&self, id: u32) -> bool {
233        self.slots.iter().any(|slot| slot.id == id)
234    }
235
236    fn add_boxed(&mut self, animation: Box<dyn Update + Send>) -> u32 {
237        let id = self.next_id;
238        self.next_id = self.next_id.saturating_add(1).max(1);
239        self.slots.push(DriverSlot {
240            id,
241            animation,
242            active: true,
243        });
244        id
245    }
246}
247
248impl Default for RafDriver {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254/// Scroll position driver for scroll-linked animations.
255#[wasm_bindgen(js_name = ScrollDriver)]
256#[derive(Clone, Debug)]
257pub struct ScrollDriver {
258    inner: Arc<Mutex<CoreScrollDriver>>,
259}
260
261#[wasm_bindgen(js_class = ScrollDriver)]
262impl ScrollDriver {
263    /// Create a scroll driver with a min and max scroll position.
264    #[wasm_bindgen(constructor)]
265    pub fn new(min: f32, max: f32) -> Self {
266        Self {
267            inner: Arc::new(Mutex::new(CoreScrollDriver::new(min, max))),
268        }
269    }
270
271    /// Register a tween.
272    #[wasm_bindgen(js_name = addTween)]
273    pub fn add_tween(&self, tween: &Tween) {
274        self.inner
275            .lock()
276            .expect("animato-js scroll driver lock poisoned")
277            .add(tween.shared());
278    }
279
280    /// Set the current scroll position.
281    #[wasm_bindgen(js_name = setPosition)]
282    pub fn set_position(&self, position: f32) {
283        self.inner
284            .lock()
285            .expect("animato-js scroll driver lock poisoned")
286            .set_position(position);
287    }
288
289    /// Current normalized scroll progress.
290    pub fn progress(&self) -> f32 {
291        self.inner
292            .lock()
293            .expect("animato-js scroll driver lock poisoned")
294            .progress()
295    }
296
297    /// Current scroll position.
298    pub fn position(&self) -> f32 {
299        self.inner
300            .lock()
301            .expect("animato-js scroll driver lock poisoned")
302            .position()
303    }
304
305    /// Number of registered animations.
306    #[wasm_bindgen(js_name = animationCount)]
307    pub fn animation_count(&self) -> usize {
308        self.inner
309            .lock()
310            .expect("animato-js scroll driver lock poisoned")
311            .animation_count()
312    }
313
314    /// Remove completed animations.
315    #[wasm_bindgen(js_name = clearCompleted)]
316    pub fn clear_completed(&self) {
317        self.inner
318            .lock()
319            .expect("animato-js scroll driver lock poisoned")
320            .clear_completed();
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use crate::error::require_index;
328
329    #[test]
330    fn raf_driver_ticks_tween() {
331        let tween = Tween::new(0.0, 1.0, 1.0);
332        let mut driver = RafDriver::new();
333        let id = driver.add_tween(&tween);
334        driver.tick_dt(0.5);
335        assert!(driver.is_active(id));
336        assert_eq!(tween.value(), 0.5);
337    }
338
339    #[test]
340    fn invalid_index_helper_errors() {
341        assert!(require_index(4, 2, "test").is_err());
342    }
343}