Skip to main content

animato_js/
spring.rs

1//! Spring bindings.
2
3use crate::error::{JsResult, js_error, non_negative};
4use crate::tween::lock;
5use crate::types::f32_array;
6use animato_core::Update;
7use animato_spring::{Spring as CoreSpring, SpringConfig, SpringN};
8use js_sys::Float32Array;
9use std::sync::{Arc, Mutex};
10use wasm_bindgen::prelude::*;
11
12fn preset(name: &str) -> JsResult<SpringConfig> {
13    match crate::types::normalize_name(name).as_str() {
14        "gentle" => Ok(SpringConfig::gentle()),
15        "wobbly" => Ok(SpringConfig::wobbly()),
16        "stiff" => Ok(SpringConfig::stiff()),
17        "slow" => Ok(SpringConfig::slow()),
18        "snappy" => Ok(SpringConfig::snappy()),
19        _ => Err(js_error(format!("unknown spring preset `{name}`"))),
20    }
21}
22
23fn config(stiffness: f32, damping: f32, mass: f32, epsilon: f32) -> SpringConfig {
24    SpringConfig {
25        stiffness: non_negative(stiffness, 100.0),
26        damping: non_negative(damping, 10.0),
27        mass: non_negative(mass, 1.0).max(f32::EPSILON),
28        epsilon: non_negative(epsilon, 0.001),
29    }
30}
31
32/// Shared scalar spring update adapter.
33#[derive(Clone, Debug)]
34pub(crate) struct SharedSpring {
35    inner: Arc<Mutex<CoreSpring>>,
36}
37
38impl SharedSpring {
39    pub(crate) fn new(inner: Arc<Mutex<CoreSpring>>) -> Self {
40        Self { inner }
41    }
42}
43
44impl Update for SharedSpring {
45    fn update(&mut self, dt: f32) -> bool {
46        lock(&self.inner).update(dt)
47    }
48}
49
50macro_rules! shared_spring_n {
51    ($name:ident, $value_ty:ty) => {
52        /// Shared vector spring update adapter.
53        #[derive(Clone, Debug)]
54        pub(crate) struct $name {
55            inner: Arc<Mutex<SpringN<$value_ty>>>,
56        }
57
58        impl $name {
59            pub(crate) fn new(inner: Arc<Mutex<SpringN<$value_ty>>>) -> Self {
60                Self { inner }
61            }
62        }
63
64        impl Update for $name {
65            fn update(&mut self, dt: f32) -> bool {
66                lock(&self.inner).update(dt)
67            }
68        }
69    };
70}
71
72shared_spring_n!(SharedSpring2D, [f32; 2]);
73shared_spring_n!(SharedSpring3D, [f32; 3]);
74shared_spring_n!(SharedSpring4D, [f32; 4]);
75
76/// Scalar damped spring.
77#[wasm_bindgen(js_name = Spring)]
78#[derive(Clone, Debug)]
79pub struct Spring {
80    inner: Arc<Mutex<CoreSpring>>,
81}
82
83#[wasm_bindgen(js_class = Spring)]
84impl Spring {
85    /// Create a spring at `initial`, targeting `target`.
86    #[wasm_bindgen(constructor)]
87    pub fn new(initial: f32, target: f32) -> Self {
88        let mut spring = CoreSpring::new(SpringConfig::default());
89        spring.snap_to(initial);
90        spring.set_target(target);
91        Self {
92            inner: Arc::new(Mutex::new(spring)),
93        }
94    }
95
96    /// Advance by `dt` seconds.
97    pub fn update(&self, dt: f32) -> bool {
98        lock(&self.inner).update(dt)
99    }
100
101    /// Current position.
102    pub fn position(&self) -> f32 {
103        lock(&self.inner).position()
104    }
105
106    /// Current velocity.
107    pub fn velocity(&self) -> f32 {
108        lock(&self.inner).velocity()
109    }
110
111    /// Whether the spring has settled.
112    #[wasm_bindgen(js_name = isSettled)]
113    pub fn is_settled(&self) -> bool {
114        lock(&self.inner).is_settled()
115    }
116
117    /// Set a target.
118    #[wasm_bindgen(js_name = setTarget)]
119    pub fn set_target(&self, target: f32) {
120        lock(&self.inner).set_target(target);
121    }
122
123    /// Snap instantly to a position.
124    #[wasm_bindgen(js_name = snapTo)]
125    pub fn snap_to(&self, position: f32) {
126        lock(&self.inner).snap_to(position);
127    }
128
129    /// Apply a named preset.
130    #[wasm_bindgen(js_name = setPreset)]
131    pub fn set_preset(&self, name: &str) -> Result<(), JsValue> {
132        lock(&self.inner).config = preset(name)?;
133        Ok(())
134    }
135
136    /// Set custom spring parameters.
137    #[wasm_bindgen(js_name = setConfig)]
138    pub fn set_config(&self, stiffness: f32, damping: f32, mass: f32, epsilon: f32) {
139        lock(&self.inner).config = config(stiffness, damping, mass, epsilon);
140    }
141
142    pub(crate) fn shared(&self) -> SharedSpring {
143        SharedSpring::new(Arc::clone(&self.inner))
144    }
145}
146
147macro_rules! vector_spring {
148    (
149        $class:ident,
150        $js_name:ident,
151        $shared:ident,
152        $value_ty:ty,
153        [$($initial:ident),+],
154        [$($target:ident),+],
155        $array_fn:ident
156    ) => {
157        /// Vector damped spring.
158        #[wasm_bindgen(js_name = $js_name)]
159        #[derive(Clone, Debug)]
160        pub struct $class {
161            inner: Arc<Mutex<SpringN<$value_ty>>>,
162        }
163
164        #[wasm_bindgen(js_class = $js_name)]
165        impl $class {
166            /// Create a vector spring.
167            #[wasm_bindgen(constructor)]
168            #[allow(clippy::too_many_arguments)]
169            pub fn new($($initial: f32,)+ $($target: f32),+) -> Self {
170                let mut spring = SpringN::new(SpringConfig::default(), [$($initial),+]);
171                spring.set_target([$($target),+]);
172                Self {
173                    inner: Arc::new(Mutex::new(spring)),
174                }
175            }
176
177            /// Advance by `dt` seconds.
178            pub fn update(&self, dt: f32) -> bool {
179                lock(&self.inner).update(dt)
180            }
181
182            /// Current position as a typed array.
183            #[wasm_bindgen(js_name = toArray)]
184            pub fn to_array(&self) -> Float32Array {
185                let value = lock(&self.inner).position();
186                f32_array(&value)
187            }
188
189            /// Whether the spring has settled.
190            #[wasm_bindgen(js_name = isSettled)]
191            pub fn is_settled(&self) -> bool {
192                lock(&self.inner).is_settled()
193            }
194
195            /// Snap instantly to a position.
196            #[wasm_bindgen(js_name = snapTo)]
197            pub fn snap_to(&self, $($initial: f32),+) {
198                lock(&self.inner).snap_to([$($initial),+]);
199            }
200
201            /// Set a target.
202            #[wasm_bindgen(js_name = setTarget)]
203            pub fn set_target(&self, $($target: f32),+) {
204                lock(&self.inner).set_target([$($target),+]);
205            }
206
207            pub(crate) fn shared(&self) -> $shared {
208                $shared::new(Arc::clone(&self.inner))
209            }
210        }
211    };
212}
213
214vector_spring!(
215    Spring2D,
216    Spring2D,
217    SharedSpring2D,
218    [f32; 2],
219    [x, y],
220    [target_x, target_y],
221    vec2
222);
223vector_spring!(
224    Spring3D,
225    Spring3D,
226    SharedSpring3D,
227    [f32; 3],
228    [x, y, z],
229    [target_x, target_y, target_z],
230    vec3
231);
232vector_spring!(
233    Spring4D,
234    Spring4D,
235    SharedSpring4D,
236    [f32; 4],
237    [x, y, z, w],
238    [target_x, target_y, target_z, target_w],
239    vec4
240);
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn scalar_spring_moves() {
248        let spring = Spring::new(0.0, 100.0);
249        spring.update(1.0 / 60.0);
250        assert!(spring.position() > 0.0);
251    }
252}