Skip to main content

animato_js/
path.rs

1//! Motion path and shape morphing bindings.
2
3use crate::easing::parse_easing;
4use crate::error::non_negative;
5use crate::tween::lock;
6use crate::types::{f32_array, flat_points, points_to_array, vec2};
7use animato_core::{Playable, Update};
8use animato_path::{
9    DrawSvg, LineSegment, MorphPath as CoreMorphPath, MotionPath as CoreMotionPath,
10    MotionPathTween as CoreMotionPathTween, PathEvaluate,
11};
12use js_sys::Float32Array;
13use std::sync::{Arc, Mutex};
14use wasm_bindgen::prelude::*;
15
16/// Shared motion path update adapter.
17#[derive(Clone, Debug)]
18pub(crate) struct SharedMotionPath {
19    inner: Arc<Mutex<CoreMotionPathTween>>,
20}
21
22impl SharedMotionPath {
23    pub(crate) fn new(inner: Arc<Mutex<CoreMotionPathTween>>) -> Self {
24        Self { inner }
25    }
26}
27
28impl Update for SharedMotionPath {
29    fn update(&mut self, dt: f32) -> bool {
30        lock(&self.inner).update(dt)
31    }
32}
33
34impl Playable for SharedMotionPath {
35    fn duration(&self) -> f32 {
36        lock(&self.inner).tween().duration
37    }
38
39    fn reset(&mut self) {
40        lock(&self.inner).reset();
41    }
42
43    fn seek_to(&mut self, progress: f32) {
44        lock(&self.inner).seek(progress);
45    }
46
47    fn is_complete(&self) -> bool {
48        lock(&self.inner).is_complete()
49    }
50
51    fn as_any(&self) -> &dyn core::any::Any {
52        self
53    }
54
55    fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
56        self
57    }
58}
59
60/// Tween-driven motion path.
61#[wasm_bindgen(js_name = MotionPath)]
62#[derive(Clone, Debug)]
63pub struct MotionPath {
64    inner: Arc<Mutex<CoreMotionPathTween>>,
65}
66
67#[wasm_bindgen(js_class = MotionPath)]
68impl MotionPath {
69    /// Create a motion path from an SVG `d` string.
70    #[wasm_bindgen(constructor)]
71    pub fn new(svg_path: &str, duration: f32) -> Result<Self, JsValue> {
72        let path = CoreMotionPath::try_from_svg(svg_path)
73            .map_err(|err| JsValue::from_str(&err.to_string()))?;
74        Ok(Self {
75            inner: Arc::new(Mutex::new(
76                CoreMotionPathTween::new(path)
77                    .duration(non_negative(duration, 1.0))
78                    .build(),
79            )),
80        })
81    }
82
83    /// Create a straight-line motion path.
84    #[wasm_bindgen(js_name = line)]
85    pub fn line(from_x: f32, from_y: f32, to_x: f32, to_y: f32, duration: f32) -> Self {
86        Self {
87            inner: Arc::new(Mutex::new(
88                CoreMotionPathTween::new(LineSegment::new([from_x, from_y], [to_x, to_y]))
89                    .duration(non_negative(duration, 1.0))
90                    .build(),
91            )),
92        }
93    }
94
95    /// Advance by `dt` seconds.
96    pub fn update(&self, dt: f32) -> bool {
97        lock(&self.inner).update(dt)
98    }
99
100    /// Current x position.
101    pub fn x(&self) -> f32 {
102        lock(&self.inner).value()[0]
103    }
104
105    /// Current y position.
106    pub fn y(&self) -> f32 {
107        lock(&self.inner).value()[1]
108    }
109
110    /// Current position as a typed array.
111    #[wasm_bindgen(js_name = toArray)]
112    pub fn to_array(&self) -> Float32Array {
113        let pos = lock(&self.inner).value();
114        vec2(pos[0], pos[1])
115    }
116
117    /// Current auto-rotation heading in degrees.
118    #[wasm_bindgen(js_name = rotationDeg)]
119    pub fn rotation_deg(&self) -> f32 {
120        lock(&self.inner).rotation_deg()
121    }
122
123    /// Current normalized path progress after offsets.
124    pub fn progress(&self) -> f32 {
125        lock(&self.inner).path_progress()
126    }
127
128    /// Whether the motion tween is complete.
129    #[wasm_bindgen(js_name = isComplete)]
130    pub fn is_complete(&self) -> bool {
131        lock(&self.inner).is_complete()
132    }
133
134    /// Reset playback.
135    pub fn reset(&self) {
136        lock(&self.inner).reset();
137    }
138
139    /// Seek normalized progress.
140    pub fn seek(&self, progress: f32) {
141        lock(&self.inner).seek(progress);
142    }
143
144    /// Set easing by name.
145    #[wasm_bindgen(js_name = setEasing)]
146    pub fn set_easing(&self, easing: &str) -> Result<(), JsValue> {
147        lock(&self.inner).tween_mut().easing = parse_easing(easing)?;
148        Ok(())
149    }
150
151    /// Enable or disable auto-rotation.
152    #[wasm_bindgen(js_name = setAutoRotate)]
153    pub fn set_auto_rotate(&self, yes: bool) {
154        lock(&self.inner).set_auto_rotate(yes);
155    }
156
157    /// Set normalized path offsets.
158    #[wasm_bindgen(js_name = setOffsets)]
159    pub fn set_offsets(&self, start: f32, end: f32) {
160        lock(&self.inner).set_offsets(start, end);
161    }
162
163    /// SVG draw values as `[dashArray, dashOffset, progress]`.
164    #[wasm_bindgen(js_name = drawOn)]
165    pub fn draw_on(&self, progress: f32) -> Float32Array {
166        let values = lock(&self.inner).path().draw_on(progress);
167        f32_array(&[values.dash_array, values.dash_offset, values.progress()])
168    }
169
170    /// SVG reverse draw values as `[dashArray, dashOffset, progress]`.
171    #[wasm_bindgen(js_name = drawOnReverse)]
172    pub fn draw_on_reverse(&self, progress: f32) -> Float32Array {
173        let values = lock(&self.inner).path().draw_on_reverse(progress);
174        f32_array(&[values.dash_array, values.dash_offset, values.progress()])
175    }
176
177    /// Total path length.
178    #[wasm_bindgen(js_name = totalLength)]
179    pub fn total_length(&self) -> f32 {
180        lock(&self.inner).path().arc_length()
181    }
182
183    pub(crate) fn shared(&self) -> SharedMotionPath {
184        SharedMotionPath::new(Arc::clone(&self.inner))
185    }
186}
187
188/// Shape morphing between two point lists.
189#[wasm_bindgen(js_name = MorphPath)]
190#[derive(Clone, Debug)]
191pub struct MorphPath {
192    inner: CoreMorphPath,
193}
194
195#[wasm_bindgen(js_class = MorphPath)]
196impl MorphPath {
197    /// Create a morph from flat `[x0, y0, x1, y1, ...]` point arrays.
198    #[wasm_bindgen(constructor)]
199    pub fn new(from: &Float32Array, to: &Float32Array, resolution: usize) -> Result<Self, JsValue> {
200        Ok(Self {
201            inner: CoreMorphPath::with_resolution(
202                flat_points(from)?,
203                flat_points(to)?,
204                resolution.max(2),
205            ),
206        })
207    }
208
209    /// Evaluate points at normalized progress.
210    pub fn evaluate(&self, progress: f32) -> Float32Array {
211        points_to_array(&self.inner.evaluate(progress))
212    }
213
214    /// Bounds at progress as `[minX, minY, maxX, maxY]`.
215    #[wasm_bindgen(js_name = boundsAt)]
216    pub fn bounds_at(&self, progress: f32) -> Float32Array {
217        f32_array(&self.inner.bounds_at(progress))
218    }
219
220    /// Point count after resampling.
221    #[wasm_bindgen(js_name = pointCount)]
222    pub fn point_count(&self) -> usize {
223        self.inner.point_count()
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn motion_path_line_updates() {
233        let path = MotionPath::line(0.0, 0.0, 100.0, 0.0, 1.0);
234        path.update(0.5);
235        assert_eq!(path.x(), 50.0);
236    }
237}