1use crate::error::{JsResult, js_error, non_negative};
4use crate::tween::lock;
5use crate::types::{f32_array, normalize_name, vec2};
6use animato_core::Update;
7use animato_physics::{
8 DragAxis, DragConstraints, DragState as CoreDragState, Gesture, GestureConfig,
9 GestureRecognizer as CoreGestureRecognizer, Inertia as CoreInertia, InertiaBounds,
10 InertiaConfig, InertiaN, PointerData, SwipeDirection,
11};
12use js_sys::Float32Array;
13use std::sync::{Arc, Mutex};
14use wasm_bindgen::prelude::*;
15
16fn inertia_config(friction: f32, min_velocity: f32) -> InertiaConfig<f32> {
17 InertiaConfig::new(
18 non_negative(friction, 1400.0),
19 non_negative(min_velocity, 2.0),
20 )
21}
22
23fn inertia_config_2d(friction: f32, min_velocity: f32) -> InertiaConfig<[f32; 2]> {
24 InertiaConfig::new(
25 non_negative(friction, 1400.0),
26 non_negative(min_velocity, 2.0),
27 )
28}
29
30fn axis(name: &str) -> JsResult<DragAxis> {
31 match normalize_name(name).as_str() {
32 "both" | "xy" => Ok(DragAxis::Both),
33 "x" => Ok(DragAxis::X),
34 "y" => Ok(DragAxis::Y),
35 _ => Err(js_error(format!("unknown drag axis `{name}`"))),
36 }
37}
38
39#[wasm_bindgen(js_name = Inertia)]
41#[derive(Clone, Debug)]
42pub struct Inertia {
43 inner: Arc<Mutex<CoreInertia>>,
44}
45
46#[wasm_bindgen(js_class = Inertia)]
47impl Inertia {
48 #[wasm_bindgen(constructor)]
50 pub fn new(position: f32, velocity: f32, friction: f32, min_velocity: f32) -> Self {
51 let mut inertia =
52 CoreInertia::with_position(inertia_config(friction, min_velocity), position);
53 inertia.kick(velocity);
54 Self {
55 inner: Arc::new(Mutex::new(inertia)),
56 }
57 }
58
59 #[wasm_bindgen(js_name = withPreset)]
61 pub fn with_preset(position: f32, velocity: f32, preset: &str) -> Result<Inertia, JsValue> {
62 let config = match normalize_name(preset).as_str() {
63 "smooth" => InertiaConfig::smooth(),
64 "snappy" => InertiaConfig::snappy(),
65 "heavy" => InertiaConfig::heavy(),
66 _ => return Err(js_error(format!("unknown inertia preset `{preset}`"))),
67 };
68 let mut inertia = CoreInertia::with_position(config, position);
69 inertia.kick(velocity);
70 Ok(Self {
71 inner: Arc::new(Mutex::new(inertia)),
72 })
73 }
74
75 #[wasm_bindgen(js_name = setBounds)]
77 pub fn set_bounds(&self, min: f32, max: f32) {
78 lock(&self.inner).config.bounds = Some(InertiaBounds::new(min, max));
79 }
80
81 pub fn kick(&self, velocity: f32) {
83 lock(&self.inner).kick(velocity);
84 }
85
86 pub fn update(&self, dt: f32) -> bool {
88 lock(&self.inner).update(dt)
89 }
90
91 pub fn position(&self) -> f32 {
93 lock(&self.inner).position()
94 }
95
96 pub fn velocity(&self) -> f32 {
98 lock(&self.inner).velocity()
99 }
100
101 #[wasm_bindgen(js_name = snapTo)]
103 pub fn snap_to(&self, position: f32) {
104 lock(&self.inner).snap_to(position);
105 }
106
107 #[wasm_bindgen(js_name = isSettled)]
109 pub fn is_settled(&self) -> bool {
110 lock(&self.inner).is_settled()
111 }
112
113 pub(crate) fn shared(&self) -> SharedInertia {
114 SharedInertia {
115 inner: Arc::clone(&self.inner),
116 }
117 }
118}
119
120#[derive(Clone, Debug)]
122pub(crate) struct SharedInertia {
123 inner: Arc<Mutex<CoreInertia>>,
124}
125
126impl Update for SharedInertia {
127 fn update(&mut self, dt: f32) -> bool {
128 lock(&self.inner).update(dt)
129 }
130}
131
132#[wasm_bindgen(js_name = Inertia2D)]
134#[derive(Clone, Debug)]
135pub struct Inertia2D {
136 inner: Arc<Mutex<InertiaN<[f32; 2]>>>,
137}
138
139impl Inertia2D {
140 fn from_core(inner: InertiaN<[f32; 2]>) -> Self {
141 Self {
142 inner: Arc::new(Mutex::new(inner)),
143 }
144 }
145}
146
147#[wasm_bindgen(js_class = Inertia2D)]
148impl Inertia2D {
149 #[wasm_bindgen(constructor)]
151 pub fn new(
152 x: f32,
153 y: f32,
154 velocity_x: f32,
155 velocity_y: f32,
156 friction: f32,
157 min_velocity: f32,
158 ) -> Self {
159 let mut inertia = InertiaN::new(inertia_config_2d(friction, min_velocity), [x, y]);
160 inertia.kick([velocity_x, velocity_y]);
161 Self::from_core(inertia)
162 }
163
164 #[wasm_bindgen(js_name = setBounds)]
166 pub fn set_bounds(&self, min_x: f32, max_x: f32, min_y: f32, max_y: f32) {
167 let mut current = lock(&self.inner);
168 let pos = current.position();
169 let vel = current.velocity();
170 let mut config = inertia_config_2d(1400.0, 2.0);
171 config.bounds = Some(InertiaBounds::new([min_x, min_y], [max_x, max_y]));
172 let mut next = InertiaN::new(config, pos);
173 next.kick(vel);
174 *current = next;
175 }
176
177 pub fn kick(&self, velocity_x: f32, velocity_y: f32) {
179 lock(&self.inner).kick([velocity_x, velocity_y]);
180 }
181
182 pub fn update(&self, dt: f32) -> bool {
184 lock(&self.inner).update(dt)
185 }
186
187 #[wasm_bindgen(js_name = toArray)]
189 pub fn to_array(&self) -> Float32Array {
190 let pos = lock(&self.inner).position();
191 f32_array(&pos)
192 }
193
194 #[wasm_bindgen(js_name = velocityArray)]
196 pub fn velocity_array(&self) -> Float32Array {
197 let velocity = lock(&self.inner).velocity();
198 f32_array(&velocity)
199 }
200
201 #[wasm_bindgen(js_name = isSettled)]
203 pub fn is_settled(&self) -> bool {
204 lock(&self.inner).is_settled()
205 }
206
207 pub(crate) fn shared(&self) -> SharedInertia2D {
208 SharedInertia2D {
209 inner: Arc::clone(&self.inner),
210 }
211 }
212}
213
214#[derive(Clone, Debug)]
216pub(crate) struct SharedInertia2D {
217 inner: Arc<Mutex<InertiaN<[f32; 2]>>>,
218}
219
220impl Update for SharedInertia2D {
221 fn update(&mut self, dt: f32) -> bool {
222 lock(&self.inner).update(dt)
223 }
224}
225
226#[wasm_bindgen(js_name = DragState)]
228#[derive(Clone, Debug)]
229pub struct DragState {
230 inner: Arc<Mutex<CoreDragState>>,
231}
232
233#[wasm_bindgen(js_class = DragState)]
234impl DragState {
235 #[wasm_bindgen(constructor)]
237 pub fn new(x: f32, y: f32) -> Self {
238 Self {
239 inner: Arc::new(Mutex::new(CoreDragState::new([x, y]))),
240 }
241 }
242
243 #[wasm_bindgen(js_name = setAxis)]
245 pub fn set_axis(&self, axis_name: &str) -> Result<(), JsValue> {
246 let mut drag = lock(&self.inner);
247 let next =
248 core::mem::replace(&mut *drag, CoreDragState::new([0.0, 0.0])).axis(axis(axis_name)?);
249 *drag = next;
250 Ok(())
251 }
252
253 #[wasm_bindgen(js_name = setBounds)]
255 pub fn set_bounds(&self, min_x: f32, max_x: f32, min_y: f32, max_y: f32) {
256 lock(&self.inner).set_constraints(DragConstraints::bounded(min_x, max_x, min_y, max_y));
257 }
258
259 #[wasm_bindgen(js_name = setGridSnap)]
261 pub fn set_grid_snap(&self, grid: f32) {
262 lock(&self.inner).set_constraints(DragConstraints::unbounded().with_grid_snap(grid));
263 }
264
265 #[wasm_bindgen(js_name = toArray)]
267 pub fn to_array(&self) -> Float32Array {
268 let pos = lock(&self.inner).position();
269 vec2(pos[0], pos[1])
270 }
271
272 #[wasm_bindgen(js_name = velocityArray)]
274 pub fn velocity_array(&self) -> Float32Array {
275 let velocity = lock(&self.inner).velocity();
276 vec2(velocity[0], velocity[1])
277 }
278
279 #[wasm_bindgen(js_name = isDragging)]
281 pub fn is_dragging(&self) -> bool {
282 lock(&self.inner).is_dragging()
283 }
284
285 #[wasm_bindgen(js_name = pointerDown)]
287 pub fn pointer_down(&self, x: f32, y: f32, pointer_id: u32) {
288 lock(&self.inner).on_pointer_down(PointerData::new(x, y, pointer_id as u64));
289 }
290
291 #[wasm_bindgen(js_name = pointerMove)]
293 pub fn pointer_move(&self, x: f32, y: f32, pointer_id: u32, dt: f32) {
294 lock(&self.inner).on_pointer_move(
295 PointerData::new(x, y, pointer_id as u64),
296 non_negative(dt, 0.0),
297 );
298 }
299
300 #[wasm_bindgen(js_name = pointerUp)]
302 pub fn pointer_up(&self, x: f32, y: f32, pointer_id: u32) -> Option<Inertia2D> {
303 lock(&self.inner)
304 .on_pointer_up(PointerData::new(x, y, pointer_id as u64))
305 .map(Inertia2D::from_core)
306 }
307
308 #[wasm_bindgen(js_name = snapTo)]
310 pub fn snap_to(&self, x: f32, y: f32) {
311 lock(&self.inner).snap_to([x, y]);
312 }
313}
314
315#[wasm_bindgen(js_name = GestureRecognizer)]
317#[derive(Clone, Debug)]
318pub struct GestureRecognizer {
319 inner: Arc<Mutex<CoreGestureRecognizer>>,
320}
321
322#[wasm_bindgen(js_class = GestureRecognizer)]
323impl GestureRecognizer {
324 #[wasm_bindgen(constructor)]
326 pub fn new() -> Self {
327 Self {
328 inner: Arc::new(Mutex::new(CoreGestureRecognizer::default())),
329 }
330 }
331
332 #[wasm_bindgen(js_name = setConfig)]
334 pub fn set_config(
335 &self,
336 tap_max_distance: f32,
337 tap_max_duration: f32,
338 swipe_min_distance: f32,
339 long_press_duration: f32,
340 double_tap_max_interval: f32,
341 ) {
342 *lock(&self.inner) = CoreGestureRecognizer::new(GestureConfig {
343 tap_max_distance: non_negative(tap_max_distance, 8.0),
344 tap_max_duration: non_negative(tap_max_duration, 0.25),
345 swipe_min_distance: non_negative(swipe_min_distance, 40.0),
346 long_press_duration: non_negative(long_press_duration, 0.5),
347 double_tap_max_interval: non_negative(double_tap_max_interval, 0.3),
348 });
349 }
350
351 #[wasm_bindgen(js_name = pointerDown)]
353 pub fn pointer_down(&self, x: f32, y: f32, pointer_id: u32, time_seconds: f32) {
354 lock(&self.inner).on_pointer_down(
355 PointerData::new(x, y, pointer_id as u64),
356 non_negative(time_seconds, 0.0),
357 );
358 }
359
360 #[wasm_bindgen(js_name = pointerMove)]
362 pub fn pointer_move(&self, x: f32, y: f32, pointer_id: u32, time_seconds: f32) {
363 lock(&self.inner).on_pointer_move(
364 PointerData::new(x, y, pointer_id as u64),
365 non_negative(time_seconds, 0.0),
366 );
367 }
368
369 #[wasm_bindgen(js_name = pointerUp)]
371 pub fn pointer_up(
372 &self,
373 x: f32,
374 y: f32,
375 pointer_id: u32,
376 time_seconds: f32,
377 ) -> Result<JsValue, JsValue> {
378 let gesture = lock(&self.inner).on_pointer_up(
379 PointerData::new(x, y, pointer_id as u64),
380 non_negative(time_seconds, 0.0),
381 );
382 match gesture {
383 Some(gesture) => Ok(gesture_to_value(gesture)),
384 None => Ok(JsValue::UNDEFINED),
385 }
386 }
387}
388
389impl Default for GestureRecognizer {
390 fn default() -> Self {
391 Self::new()
392 }
393}
394
395fn gesture_to_value(gesture: Gesture) -> JsValue {
396 match gesture {
397 Gesture::Tap { position } => object(&[
398 ("type", JsValue::from_str("tap")),
399 ("x", JsValue::from_f64(position[0] as f64)),
400 ("y", JsValue::from_f64(position[1] as f64)),
401 ]),
402 Gesture::DoubleTap { position } => object(&[
403 ("type", JsValue::from_str("doubleTap")),
404 ("x", JsValue::from_f64(position[0] as f64)),
405 ("y", JsValue::from_f64(position[1] as f64)),
406 ]),
407 Gesture::LongPress { position, duration } => object(&[
408 ("type", JsValue::from_str("longPress")),
409 ("x", JsValue::from_f64(position[0] as f64)),
410 ("y", JsValue::from_f64(position[1] as f64)),
411 ("duration", JsValue::from_f64(duration as f64)),
412 ]),
413 Gesture::Swipe {
414 direction,
415 velocity,
416 distance,
417 } => object(&[
418 ("type", JsValue::from_str("swipe")),
419 (
420 "direction",
421 JsValue::from_str(match direction {
422 SwipeDirection::Up => "up",
423 SwipeDirection::Down => "down",
424 SwipeDirection::Left => "left",
425 SwipeDirection::Right => "right",
426 }),
427 ),
428 ("velocity", JsValue::from_f64(velocity as f64)),
429 ("distance", JsValue::from_f64(distance as f64)),
430 ]),
431 Gesture::Pinch { scale, center } => object(&[
432 ("type", JsValue::from_str("pinch")),
433 ("scale", JsValue::from_f64(scale as f64)),
434 ("centerX", JsValue::from_f64(center[0] as f64)),
435 ("centerY", JsValue::from_f64(center[1] as f64)),
436 ]),
437 Gesture::Rotation {
438 angle_delta,
439 center,
440 } => object(&[
441 ("type", JsValue::from_str("rotation")),
442 ("angleDelta", JsValue::from_f64(angle_delta as f64)),
443 ("centerX", JsValue::from_f64(center[0] as f64)),
444 ("centerY", JsValue::from_f64(center[1] as f64)),
445 ]),
446 }
447}
448
449fn object(entries: &[(&str, JsValue)]) -> JsValue {
450 let object = js_sys::Object::new();
451 for (key, value) in entries {
452 let _ = js_sys::Reflect::set(&object, &JsValue::from_str(key), value);
453 }
454 object.into()
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460
461 #[test]
462 fn inertia_moves() {
463 let inertia = Inertia::new(0.0, 100.0, 1000.0, 1.0);
464 inertia.update(0.016);
465 assert!(inertia.position() > 0.0);
466 }
467
468 #[test]
469 fn drag_tracks_position() {
470 let drag = DragState::new(0.0, 0.0);
471 drag.pointer_down(0.0, 0.0, 1);
472 drag.pointer_move(20.0, 10.0, 1, 0.016);
473 assert!(drag.is_dragging());
474 }
475}