1use 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#[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 pub fn pause(&mut self) {
181 self.paused = true;
182 }
183
184 pub fn resume(&mut self) {
186 self.paused = false;
187 }
188
189 #[wasm_bindgen(js_name = isPaused)]
191 pub fn is_paused(&self) -> bool {
192 self.paused
193 }
194
195 #[wasm_bindgen(js_name = resetTimestamp)]
197 pub fn reset_timestamp(&mut self) {
198 self.last_timestamp_ms = None;
199 }
200
201 #[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 #[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 pub fn cancel(&mut self, id: u32) {
215 self.slots.retain(|slot| slot.id != id);
216 }
217
218 #[wasm_bindgen(js_name = cancelAll)]
220 pub fn cancel_all(&mut self) {
221 self.slots.clear();
222 }
223
224 #[wasm_bindgen(js_name = activeCount)]
226 pub fn active_count(&self) -> usize {
227 self.slots.len()
228 }
229
230 #[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#[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 #[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 #[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 #[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 pub fn progress(&self) -> f32 {
291 self.inner
292 .lock()
293 .expect("animato-js scroll driver lock poisoned")
294 .progress()
295 }
296
297 pub fn position(&self) -> f32 {
299 self.inner
300 .lock()
301 .expect("animato-js scroll driver lock poisoned")
302 .position()
303 }
304
305 #[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 #[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}