Skip to main content

animato_js/
wasm_dom.rs

1//! Browser DOM helper bindings.
2
3use animato_wasm::ScrollSmoother as CoreScrollSmoother;
4use wasm_bindgen::prelude::*;
5
6/// Momentum-style scroll smoothing helper.
7#[wasm_bindgen(js_name = ScrollSmoother)]
8#[derive(Clone, Debug)]
9pub struct ScrollSmoother {
10    inner: CoreScrollSmoother,
11}
12
13#[wasm_bindgen(js_class = ScrollSmoother)]
14impl ScrollSmoother {
15    /// Create a scroll smoother.
16    #[wasm_bindgen(constructor)]
17    pub fn new() -> Self {
18        Self {
19            inner: CoreScrollSmoother::new(),
20        }
21    }
22
23    /// Snap instantly to a value.
24    #[wasm_bindgen(js_name = snapTo)]
25    pub fn snap_to(&mut self, value: f32) {
26        self.inner.snap_to(value);
27    }
28
29    /// Set target scroll position.
30    #[wasm_bindgen(js_name = scrollTo)]
31    pub fn scroll_to(&mut self, value: f32) {
32        self.inner.scroll_to(value);
33    }
34
35    /// Feed a wheel delta.
36    #[wasm_bindgen(js_name = onWheel)]
37    pub fn on_wheel(&mut self, delta_y: f32) {
38        self.inner.on_wheel(delta_y);
39    }
40
41    /// Advance by `dt` seconds.
42    pub fn update(&mut self, dt: f32) -> bool {
43        self.inner.update(dt)
44    }
45
46    /// Current smoothed position.
47    pub fn current(&self) -> f32 {
48        self.inner.current()
49    }
50
51    /// Target scroll position.
52    pub fn target(&self) -> f32 {
53        self.inner.target()
54    }
55
56    /// Whether smoothed scroll has settled.
57    #[wasm_bindgen(js_name = isSettled)]
58    pub fn is_settled(&self) -> bool {
59        self.inner.is_settled()
60    }
61}
62
63impl Default for ScrollSmoother {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69#[cfg(target_arch = "wasm32")]
70mod dom {
71    use super::*;
72    use crate::easing::parse_easing;
73    use crate::error::non_negative;
74    use animato_wasm::{
75        Draggable as CoreDraggable, FlipAnimation as CoreFlipAnimation, FlipState,
76        LayoutAnimator as CoreLayoutAnimator, Observer as CoreObserver, ObserverEvent, SplitMode,
77        SplitText as CoreSplitText,
78    };
79    use web_sys::{Element, PointerEvent, WheelEvent};
80
81    /// FLIP layout transition between two DOM element boxes.
82    #[wasm_bindgen(js_name = FlipAnimation)]
83    #[derive(Clone, Debug)]
84    pub struct FlipAnimation {
85        inner: CoreFlipAnimation,
86    }
87
88    #[wasm_bindgen(js_class = FlipAnimation)]
89    impl FlipAnimation {
90        /// Capture a first and last element state.
91        #[wasm_bindgen(constructor)]
92        pub fn new(first: &Element, last: &Element) -> Self {
93            Self {
94                inner: CoreFlipAnimation::new(FlipState::capture(first), FlipState::capture(last)),
95            }
96        }
97
98        /// Set duration.
99        pub fn duration(&mut self, seconds: f32) {
100            self.inner = self.inner.clone().duration(non_negative(seconds, 0.3));
101        }
102
103        /// Set easing.
104        #[wasm_bindgen(js_name = setEasing)]
105        pub fn set_easing(&mut self, easing: &str) -> Result<(), JsValue> {
106            self.inner = self.inner.clone().easing(parse_easing(easing)?);
107            Ok(())
108        }
109
110        /// Advance by `dt`.
111        pub fn update(&mut self, dt: f32) -> bool {
112            self.inner.update(dt)
113        }
114
115        /// Reset animation.
116        pub fn reset(&mut self) {
117            self.inner.reset();
118        }
119
120        /// Current progress.
121        pub fn progress(&self) -> f32 {
122            self.inner.progress()
123        }
124
125        /// Current CSS transform string.
126        #[wasm_bindgen(js_name = cssTransform)]
127        pub fn css_transform(&self) -> String {
128            self.inner.css_transform()
129        }
130
131        /// Apply current transform to an element.
132        #[wasm_bindgen(js_name = applyTo)]
133        pub fn apply_to(&self, element: &Element) -> Result<(), JsValue> {
134            self.inner.apply_to(element)
135        }
136    }
137
138    /// Multi-element FLIP layout animator.
139    #[wasm_bindgen(js_name = LayoutAnimator)]
140    #[derive(Debug, Default)]
141    pub struct LayoutAnimator {
142        inner: CoreLayoutAnimator,
143    }
144
145    #[wasm_bindgen(js_class = LayoutAnimator)]
146    impl LayoutAnimator {
147        /// Create a layout animator.
148        #[wasm_bindgen(constructor)]
149        pub fn new() -> Self {
150            Self::default()
151        }
152
153        /// Snapshot an element under a key.
154        pub fn snapshot(&mut self, key: &str, element: &Element) {
155            self.inner.snapshot(key, element);
156        }
157
158        /// Compute one transition from the stored snapshot to the current element.
159        #[wasm_bindgen(js_name = computeTransition)]
160        pub fn compute_transition(
161            &mut self,
162            key: &str,
163            element: &Element,
164            duration: f32,
165            easing: &str,
166        ) -> Result<(), JsValue> {
167            self.inner
168                .compute_transitions(&[(key, element)], duration, parse_easing(easing)?);
169            Ok(())
170        }
171
172        /// Advance all transitions.
173        pub fn update(&mut self, dt: f32) {
174            self.inner.update(dt);
175        }
176
177        /// Apply one keyed transition to an element.
178        pub fn apply(&self, key: &str, element: &Element) -> Result<(), JsValue> {
179            self.inner.apply(&[(key, element)])
180        }
181
182        /// Transform string for a key, or an empty string.
183        #[wasm_bindgen(js_name = cssTransform)]
184        pub fn css_transform(&self, key: &str) -> String {
185            self.inner.css_transform(key).unwrap_or_default()
186        }
187
188        /// Whether all transitions are complete.
189        #[wasm_bindgen(js_name = isComplete)]
190        pub fn is_complete(&self) -> bool {
191            self.inner.is_complete()
192        }
193
194        /// Active animation count.
195        #[wasm_bindgen(js_name = animationCount)]
196        pub fn animation_count(&self) -> usize {
197            self.inner.animation_count()
198        }
199
200        /// Clear completed transitions.
201        #[wasm_bindgen(js_name = clearCompleted)]
202        pub fn clear_completed(&mut self) {
203            self.inner.clear_completed();
204        }
205    }
206
207    /// Text splitting helper.
208    #[wasm_bindgen(js_name = SplitText)]
209    #[derive(Debug)]
210    pub struct SplitText {
211        inner: CoreSplitText,
212    }
213
214    #[wasm_bindgen(js_class = SplitText)]
215    impl SplitText {
216        /// Split element text by characters or words.
217        #[wasm_bindgen(constructor)]
218        pub fn new(element: &Element, mode: &str) -> Result<Self, JsValue> {
219            let mode = match crate::types::normalize_name(mode).as_str() {
220                "chars" | "characters" => SplitMode::Chars,
221                "words" => SplitMode::Words,
222                _ => return Err(JsValue::from_str("split mode must be `chars` or `words`")),
223            };
224            Ok(Self {
225                inner: CoreSplitText::split(element, mode)?,
226            })
227        }
228
229        /// Number of generated spans.
230        pub fn len(&self) -> usize {
231            self.inner.spans().len()
232        }
233
234        /// Whether no spans were generated.
235        #[wasm_bindgen(js_name = isEmpty)]
236        pub fn is_empty(&self) -> bool {
237            self.inner.spans().is_empty()
238        }
239
240        /// Restore original text.
241        pub fn restore(&self) {
242            self.inner.restore();
243        }
244    }
245
246    /// Pointer drag DOM helper.
247    #[wasm_bindgen(js_name = Draggable)]
248    #[derive(Debug)]
249    pub struct Draggable {
250        inner: CoreDraggable,
251    }
252
253    #[wasm_bindgen(js_class = Draggable)]
254    impl Draggable {
255        /// Attach to an element.
256        #[wasm_bindgen(constructor)]
257        pub fn new(element: Element, x: f32, y: f32) -> Result<Self, JsValue> {
258            Ok(Self {
259                inner: CoreDraggable::attach(element, [x, y])?,
260            })
261        }
262
263        /// Current position.
264        #[wasm_bindgen(js_name = toArray)]
265        pub fn to_array(&self) -> js_sys::Float32Array {
266            crate::types::f32_array(&self.inner.position())
267        }
268
269        /// Whether the element is being dragged.
270        #[wasm_bindgen(js_name = isDragging)]
271        pub fn is_dragging(&self) -> bool {
272            self.inner.is_dragging()
273        }
274    }
275
276    /// Low-level DOM event observer helpers.
277    #[wasm_bindgen(js_name = Observer)]
278    #[derive(Clone, Debug, Default)]
279    pub struct Observer;
280
281    #[wasm_bindgen(js_class = Observer)]
282    impl Observer {
283        /// Create an observer helper.
284        #[wasm_bindgen(constructor)]
285        pub fn new() -> Self {
286            Self
287        }
288
289        /// Convert a pointer-down event to a plain JS object.
290        #[wasm_bindgen(js_name = pointerDown)]
291        pub fn pointer_down(event: &PointerEvent) -> Result<JsValue, JsValue> {
292            observer_event_to_value(CoreObserver::pointer_down(event))
293        }
294
295        /// Convert a pointer-move event to a plain JS object.
296        #[wasm_bindgen(js_name = pointerMove)]
297        pub fn pointer_move(event: &PointerEvent) -> Result<JsValue, JsValue> {
298            observer_event_to_value(CoreObserver::pointer_move(event))
299        }
300
301        /// Convert a pointer-up event to a plain JS object.
302        #[wasm_bindgen(js_name = pointerUp)]
303        pub fn pointer_up(event: &PointerEvent) -> Result<JsValue, JsValue> {
304            observer_event_to_value(CoreObserver::pointer_up(event))
305        }
306
307        /// Convert a wheel event to a plain JS object.
308        pub fn wheel(event: &WheelEvent) -> Result<JsValue, JsValue> {
309            observer_event_to_value(CoreObserver::wheel(event))
310        }
311    }
312
313    fn observer_event_to_value(event: ObserverEvent) -> Result<JsValue, JsValue> {
314        Ok(match event {
315            ObserverEvent::PointerDown { x, y, pointer_id } => object(&[
316                ("type", JsValue::from_str("pointerDown")),
317                ("x", JsValue::from_f64(x as f64)),
318                ("y", JsValue::from_f64(y as f64)),
319                ("pointerId", JsValue::from_f64(pointer_id as f64)),
320            ]),
321            ObserverEvent::PointerMove { x, y, pointer_id } => object(&[
322                ("type", JsValue::from_str("pointerMove")),
323                ("x", JsValue::from_f64(x as f64)),
324                ("y", JsValue::from_f64(y as f64)),
325                ("pointerId", JsValue::from_f64(pointer_id as f64)),
326            ]),
327            ObserverEvent::PointerUp { x, y, pointer_id } => object(&[
328                ("type", JsValue::from_str("pointerUp")),
329                ("x", JsValue::from_f64(x as f64)),
330                ("y", JsValue::from_f64(y as f64)),
331                ("pointerId", JsValue::from_f64(pointer_id as f64)),
332            ]),
333            ObserverEvent::Wheel { delta_x, delta_y } => object(&[
334                ("type", JsValue::from_str("wheel")),
335                ("deltaX", JsValue::from_f64(delta_x as f64)),
336                ("deltaY", JsValue::from_f64(delta_y as f64)),
337            ]),
338        })
339    }
340
341    fn object(entries: &[(&str, JsValue)]) -> JsValue {
342        let object = js_sys::Object::new();
343        for (key, value) in entries {
344            let _ = js_sys::Reflect::set(&object, &JsValue::from_str(key), value);
345        }
346        object.into()
347    }
348}
349
350#[cfg(not(target_arch = "wasm32"))]
351mod dom {
352    use super::*;
353
354    macro_rules! stub_class {
355        ($name:ident) => {
356            #[wasm_bindgen(js_name = $name)]
357            /// Non-WASM placeholder for browser-only DOM helpers.
358            #[derive(Clone, Debug, Default)]
359            pub struct $name;
360        };
361    }
362
363    stub_class!(FlipAnimation);
364    stub_class!(LayoutAnimator);
365    stub_class!(SplitText);
366    stub_class!(Draggable);
367    stub_class!(Observer);
368}
369
370pub use dom::{Draggable, FlipAnimation, LayoutAnimator, Observer, SplitText};