Skip to main content

animato_js/
tween.rs

1//! Tween bindings.
2
3use crate::easing::{easing_name, parse_easing};
4use crate::error::non_negative;
5use crate::types::{f32_array, loop_from_count, parse_loop_mode};
6use animato_core::{Playable, Update};
7use animato_tween::{Loop, Tween as CoreTween, TweenState};
8use js_sys::Float32Array;
9use std::sync::{Arc, Mutex};
10use wasm_bindgen::prelude::*;
11
12type Shared<T> = Arc<Mutex<CoreTween<T>>>;
13
14pub(crate) fn lock<T>(shared: &Arc<Mutex<T>>) -> std::sync::MutexGuard<'_, T> {
15    shared
16        .lock()
17        .expect("animato-js shared animation lock poisoned")
18}
19
20pub(crate) fn state_name(state: &TweenState) -> &'static str {
21    match state {
22        TweenState::Idle => "idle",
23        TweenState::Running => "running",
24        TweenState::Paused => "paused",
25        TweenState::Completed => "completed",
26    }
27}
28
29macro_rules! shared_update {
30    ($name:ident, $value_ty:ty) => {
31        #[derive(Clone, Debug)]
32        pub(crate) struct $name {
33            inner: Shared<$value_ty>,
34        }
35
36        impl $name {
37            pub(crate) fn new(inner: Shared<$value_ty>) -> Self {
38                Self { inner }
39            }
40        }
41
42        impl Update for $name {
43            fn update(&mut self, dt: f32) -> bool {
44                lock(&self.inner).update(dt)
45            }
46        }
47
48        impl Playable for $name {
49            fn duration(&self) -> f32 {
50                lock(&self.inner).duration
51            }
52
53            fn reset(&mut self) {
54                lock(&self.inner).reset();
55            }
56
57            fn seek_to(&mut self, progress: f32) {
58                lock(&self.inner).seek(progress);
59            }
60
61            fn is_complete(&self) -> bool {
62                lock(&self.inner).is_complete()
63            }
64
65            fn as_any(&self) -> &dyn core::any::Any {
66                self
67            }
68
69            fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
70                self
71            }
72        }
73    };
74}
75
76shared_update!(SharedTween, f32);
77shared_update!(SharedTween2D, [f32; 2]);
78shared_update!(SharedTween3D, [f32; 3]);
79shared_update!(SharedTween4D, [f32; 4]);
80
81/// Scalar tween from one number to another.
82#[wasm_bindgen(js_name = Tween)]
83#[derive(Clone, Debug)]
84pub struct Tween {
85    inner: Shared<f32>,
86}
87
88#[wasm_bindgen(js_class = Tween)]
89impl Tween {
90    /// Create a scalar tween.
91    #[wasm_bindgen(constructor)]
92    pub fn new(from: f32, to: f32, duration: f32) -> Self {
93        Self {
94            inner: Arc::new(Mutex::new(
95                CoreTween::new(from, to)
96                    .duration(non_negative(duration, 1.0))
97                    .build(),
98            )),
99        }
100    }
101
102    /// Advance by `dt` seconds.
103    pub fn update(&self, dt: f32) -> bool {
104        lock(&self.inner).update(dt)
105    }
106
107    /// Current animated value.
108    pub fn value(&self) -> f32 {
109        lock(&self.inner).value()
110    }
111
112    /// Current raw progress in `[0, 1]`.
113    pub fn progress(&self) -> f32 {
114        lock(&self.inner).progress()
115    }
116
117    /// Current eased progress in `[0, 1]`.
118    #[wasm_bindgen(js_name = easedProgress)]
119    pub fn eased_progress(&self) -> f32 {
120        lock(&self.inner).eased_progress()
121    }
122
123    /// Current runtime state.
124    pub fn state(&self) -> String {
125        state_name(lock(&self.inner).state()).to_owned()
126    }
127
128    /// Easing name for this tween.
129    pub fn easing(&self) -> String {
130        easing_name(&lock(&self.inner).easing).to_owned()
131    }
132
133    /// Whether playback is complete.
134    #[wasm_bindgen(js_name = isComplete)]
135    pub fn is_complete(&self) -> bool {
136        lock(&self.inner).is_complete()
137    }
138
139    /// Pause playback.
140    pub fn pause(&self) {
141        lock(&self.inner).pause();
142    }
143
144    /// Resume playback.
145    pub fn resume(&self) {
146        lock(&self.inner).resume();
147    }
148
149    /// Reset to the beginning.
150    pub fn reset(&self) {
151        lock(&self.inner).reset();
152    }
153
154    /// Reverse direction while preserving progress.
155    pub fn reverse(&self) {
156        lock(&self.inner).reverse();
157    }
158
159    /// Seek to normalized progress.
160    pub fn seek(&self, progress: f32) {
161        lock(&self.inner).seek(progress);
162    }
163
164    /// Set easing by name.
165    #[wasm_bindgen(js_name = setEasing)]
166    pub fn set_easing(&self, name: &str) -> Result<(), JsValue> {
167        lock(&self.inner).easing = parse_easing(name)?;
168        Ok(())
169    }
170
171    /// Set CSS cubic-bezier easing.
172    #[wasm_bindgen(js_name = setCubicBezier)]
173    pub fn set_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) {
174        lock(&self.inner).easing = animato_core::Easing::CubicBezier(x1, y1, x2, y2);
175    }
176
177    /// Set playback time scale.
178    #[wasm_bindgen(js_name = setTimeScale)]
179    pub fn set_time_scale(&self, scale: f32) {
180        lock(&self.inner).time_scale = non_negative(scale, 1.0);
181    }
182
183    /// Set start delay in seconds.
184    #[wasm_bindgen(js_name = setDelay)]
185    pub fn set_delay(&self, delay: f32) {
186        lock(&self.inner).delay = non_negative(delay, 0.0);
187    }
188
189    /// Loop a fixed number of passes.
190    #[wasm_bindgen(js_name = setLoopCount)]
191    pub fn set_loop_count(&self, count: u32) {
192        lock(&self.inner).looping = loop_from_count(count);
193    }
194
195    /// Set loop mode by string: `once`, `forever`, `pingPong`, or `timesN`.
196    #[wasm_bindgen(js_name = setLoopMode)]
197    pub fn set_loop_mode(&self, mode: &str) -> Result<(), JsValue> {
198        lock(&self.inner).looping = parse_loop_mode(mode)?;
199        Ok(())
200    }
201
202    /// Use ping-pong looping.
203    #[wasm_bindgen(js_name = setPingPong)]
204    pub fn set_ping_pong(&self) {
205        lock(&self.inner).looping = Loop::PingPong;
206    }
207
208    /// Use infinite looping.
209    #[wasm_bindgen(js_name = setForever)]
210    pub fn set_forever(&self) {
211        lock(&self.inner).looping = Loop::Forever;
212    }
213
214    pub(crate) fn shared(&self) -> SharedTween {
215        SharedTween::new(Arc::clone(&self.inner))
216    }
217}
218
219macro_rules! vector_tween {
220    (
221        $class:ident,
222        $js_name:ident,
223        $shared:ident,
224        $value_ty:ty,
225        [$($from:ident),+],
226        [$($to:ident),+],
227        $array_fn:ident,
228        [$($component:ident : $index:tt),+]
229    ) => {
230        /// Vector tween.
231        #[wasm_bindgen(js_name = $js_name)]
232        #[derive(Clone, Debug)]
233        pub struct $class {
234            inner: Shared<$value_ty>,
235        }
236
237        #[wasm_bindgen(js_class = $js_name)]
238        impl $class {
239            /// Create a vector tween.
240            #[wasm_bindgen(constructor)]
241            #[allow(clippy::too_many_arguments)]
242            pub fn new($($from: f32,)+ $($to: f32,)+ duration: f32) -> Self {
243                Self {
244                    inner: Arc::new(Mutex::new(
245                        CoreTween::new([$($from),+], [$($to),+])
246                            .duration(non_negative(duration, 1.0))
247                            .build(),
248                    )),
249                }
250            }
251
252            /// Advance by `dt` seconds.
253            pub fn update(&self, dt: f32) -> bool {
254                lock(&self.inner).update(dt)
255            }
256
257            /// Return all vector components.
258            #[wasm_bindgen(js_name = toArray)]
259            pub fn to_array(&self) -> Float32Array {
260                let value = lock(&self.inner).value();
261                f32_array(&value)
262            }
263
264            $(
265                /// Current vector component.
266                pub fn $component(&self) -> f32 {
267                    lock(&self.inner).value()[$index]
268                }
269            )+
270
271            /// Current raw progress in `[0, 1]`.
272            pub fn progress(&self) -> f32 {
273                lock(&self.inner).progress()
274            }
275
276            /// Current eased progress in `[0, 1]`.
277            #[wasm_bindgen(js_name = easedProgress)]
278            pub fn eased_progress(&self) -> f32 {
279                lock(&self.inner).eased_progress()
280            }
281
282            /// Whether playback is complete.
283            #[wasm_bindgen(js_name = isComplete)]
284            pub fn is_complete(&self) -> bool {
285                lock(&self.inner).is_complete()
286            }
287
288            /// Pause playback.
289            pub fn pause(&self) {
290                lock(&self.inner).pause();
291            }
292
293            /// Resume playback.
294            pub fn resume(&self) {
295                lock(&self.inner).resume();
296            }
297
298            /// Reset to the beginning.
299            pub fn reset(&self) {
300                lock(&self.inner).reset();
301            }
302
303            /// Reverse direction while preserving progress.
304            pub fn reverse(&self) {
305                lock(&self.inner).reverse();
306            }
307
308            /// Seek to normalized progress.
309            pub fn seek(&self, progress: f32) {
310                lock(&self.inner).seek(progress);
311            }
312
313            /// Set easing by name.
314            #[wasm_bindgen(js_name = setEasing)]
315            pub fn set_easing(&self, name: &str) -> Result<(), JsValue> {
316                lock(&self.inner).easing = parse_easing(name)?;
317                Ok(())
318            }
319
320            /// Set playback time scale.
321            #[wasm_bindgen(js_name = setTimeScale)]
322            pub fn set_time_scale(&self, scale: f32) {
323                lock(&self.inner).time_scale = non_negative(scale, 1.0);
324            }
325
326            /// Set loop mode by string.
327            #[wasm_bindgen(js_name = setLoopMode)]
328            pub fn set_loop_mode(&self, mode: &str) -> Result<(), JsValue> {
329                lock(&self.inner).looping = parse_loop_mode(mode)?;
330                Ok(())
331            }
332
333            pub(crate) fn shared(&self) -> $shared {
334                $shared::new(Arc::clone(&self.inner))
335            }
336        }
337    };
338}
339
340vector_tween!(
341    Tween2D,
342    Tween2D,
343    SharedTween2D,
344    [f32; 2],
345    [from_x, from_y],
346    [to_x, to_y],
347    vec2,
348    [x: 0, y: 1]
349);
350
351vector_tween!(
352    Tween3D,
353    Tween3D,
354    SharedTween3D,
355    [f32; 3],
356    [from_x, from_y, from_z],
357    [to_x, to_y, to_z],
358    vec3,
359    [x: 0, y: 1, z: 2]
360);
361
362vector_tween!(
363    Tween4D,
364    Tween4D,
365    SharedTween4D,
366    [f32; 4],
367    [from_x, from_y, from_z, from_w],
368    [to_x, to_y, to_z, to_w],
369    vec4,
370    [x: 0, y: 1, z: 2, w: 3]
371);
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn scalar_tween_updates() {
379        let tween = Tween::new(0.0, 10.0, 1.0);
380        tween.set_easing("linear").unwrap();
381        assert!(tween.update(0.5));
382        assert_eq!(tween.value(), 5.0);
383    }
384
385    #[test]
386    fn vector_tween_updates() {
387        let tween = Tween2D::new(0.0, 0.0, 10.0, 20.0, 1.0);
388        tween.update(0.5);
389        assert_eq!(tween.x(), 5.0);
390        assert_eq!(tween.y(), 10.0);
391    }
392}