1use animato_core::{Easing, Update};
4use animato_spring::{Decompose, SpringConfig, SpringN};
5use animato_tween::{KeyframeTrack, Tween};
6use dioxus::prelude::{Signal, use_signal};
7use std::fmt;
8use std::sync::{Arc, Mutex};
9
10#[derive(Clone, Debug)]
12pub enum MotionConfig {
13 Tween {
15 duration: f32,
17 easing: Easing,
19 delay: f32,
21 },
22 Spring(SpringConfig),
24}
25
26enum ActiveMotion<T: Decompose + Send + Sync + Clone + 'static> {
27 Idle,
28 Tween(Tween<T>),
29 Spring(SpringN<T>),
30 Keyframes(KeyframeTrack<T>),
31}
32
33#[derive(Clone)]
35pub struct MotionHandle<T: Decompose + Send + Sync + Clone + 'static> {
36 value: Signal<T>,
37 active: Arc<Mutex<ActiveMotion<T>>>,
38}
39
40impl<T: Decompose + Send + Sync + Clone + 'static> fmt::Debug for MotionHandle<T> {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 f.debug_struct("MotionHandle").finish_non_exhaustive()
43 }
44}
45
46impl<T: Decompose + Send + Sync + Clone + 'static> MotionHandle<T> {
47 pub fn signal(&self) -> Signal<T> {
49 self.value
50 }
51
52 pub fn value(&self) -> T {
54 crate::read_signal(self.value)
55 }
56
57 pub fn animate_to(&self, target: T, config: MotionConfig) {
59 match config {
60 MotionConfig::Tween {
61 duration,
62 easing,
63 delay,
64 } => {
65 let tween = Tween::new(self.value(), target)
66 .duration(duration.max(0.0))
67 .delay(delay.max(0.0))
68 .easing(easing)
69 .build();
70 crate::with_lock(&self.active, |active| *active = ActiveMotion::Tween(tween));
71 }
72 MotionConfig::Spring(config) => self.spring_to(target, config),
73 }
74 }
75
76 pub fn spring_to(&self, target: T, config: SpringConfig) {
78 let mut spring = SpringN::new(config, self.value());
79 spring.set_target(target);
80 crate::with_lock(&self.active, |active| {
81 *active = ActiveMotion::Spring(spring)
82 });
83 }
84
85 pub fn keyframes(&self, track: KeyframeTrack<T>) {
87 crate::with_lock(&self.active, |active| {
88 *active = ActiveMotion::Keyframes(track)
89 });
90 }
91
92 pub fn stop(&self) {
94 crate::with_lock(&self.active, |active| *active = ActiveMotion::Idle);
95 }
96
97 pub fn snap_to(&self, value: T) {
99 crate::set_signal(self.value, value);
100 self.stop();
101 }
102
103 pub fn is_animating(&self) -> bool {
105 crate::with_lock(&self.active, |active| !matches!(active, ActiveMotion::Idle))
106 }
107
108 pub fn tick(&self, dt: f32) -> bool {
110 crate::with_lock(&self.active, |active| match active {
111 ActiveMotion::Idle => false,
112 ActiveMotion::Tween(tween) => {
113 let running = tween.update(dt.max(0.0));
114 crate::set_signal(self.value, tween.value());
115 if !running {
116 *active = ActiveMotion::Idle;
117 }
118 running
119 }
120 ActiveMotion::Spring(spring) => {
121 let running = spring.update(dt.max(0.0));
122 crate::set_signal(self.value, spring.position());
123 if !running {
124 *active = ActiveMotion::Idle;
125 }
126 running
127 }
128 ActiveMotion::Keyframes(track) => {
129 let running = track.update(dt.max(0.0));
130 if let Some(value) = track.value() {
131 crate::set_signal(self.value, value);
132 }
133 if !running {
134 *active = ActiveMotion::Idle;
135 }
136 running
137 }
138 })
139 }
140}
141
142pub fn use_motion<T>(initial: T) -> MotionHandle<T>
144where
145 T: Decompose + Send + Sync + Clone + 'static,
146{
147 let value = use_signal(move || initial);
148 let handle = MotionHandle {
149 value,
150 active: Arc::new(Mutex::new(ActiveMotion::Idle)),
151 };
152
153 let loop_handle = handle.clone();
154 crate::spawn_animation_loop(move |dt| {
155 loop_handle.tick(dt);
156 true
157 });
158
159 handle
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use approx::assert_relative_eq;
166 use dioxus::prelude::*;
167 use std::cell::RefCell;
168
169 thread_local! {
170 static MOTION_CAPTURE: RefCell<Option<MotionHandle<f32>>> = const { RefCell::new(None) };
171 }
172
173 #[allow(non_snake_case)]
174 fn MotionHookApp() -> Element {
175 let handle = use_motion(0.0_f32);
176 MOTION_CAPTURE.with(|slot| *slot.borrow_mut() = Some(handle));
177
178 rsx! { div {} }
179 }
180
181 fn mount_motion() -> (VirtualDom, MotionHandle<f32>) {
182 MOTION_CAPTURE.with(|slot| *slot.borrow_mut() = None);
183 let mut dom = VirtualDom::new(MotionHookApp);
184 dom.rebuild_in_place();
185 let handle = MOTION_CAPTURE.with(|slot| {
186 slot.borrow()
187 .as_ref()
188 .cloned()
189 .expect("motion hook captured")
190 });
191 (dom, handle)
192 }
193
194 #[test]
195 fn motion_tween_stop_and_snap_are_deterministic() {
196 let (_dom, handle) = mount_motion();
197
198 assert_relative_eq!(handle.value(), 0.0);
199 assert!(!handle.is_animating());
200 assert!(!handle.tick(0.1));
201
202 handle.animate_to(
203 10.0,
204 MotionConfig::Tween {
205 duration: 1.0,
206 easing: Easing::Linear,
207 delay: 0.0,
208 },
209 );
210 assert!(handle.is_animating());
211 assert!(handle.tick(0.25));
212 assert_relative_eq!(handle.value(), 2.5, epsilon = 0.001);
213 assert_relative_eq!(crate::read_signal(handle.signal()), 2.5, epsilon = 0.001);
214
215 handle.stop();
216 assert!(!handle.is_animating());
217 assert!(!handle.tick(0.25));
218 assert_relative_eq!(handle.value(), 2.5, epsilon = 0.001);
219
220 handle.snap_to(7.0);
221 assert_relative_eq!(handle.value(), 7.0, epsilon = 0.001);
222 assert!(!handle.is_animating());
223 }
224
225 #[test]
226 fn motion_tween_delay_spring_and_keyframes_update_value() {
227 let (_dom, handle) = mount_motion();
228
229 handle.animate_to(
230 10.0,
231 MotionConfig::Tween {
232 duration: 1.0,
233 easing: Easing::Linear,
234 delay: 0.25,
235 },
236 );
237 assert!(handle.tick(0.1));
238 assert_relative_eq!(handle.value(), 0.0, epsilon = 0.001);
239 assert!(handle.tick(0.15));
240 assert_relative_eq!(handle.value(), 0.0, epsilon = 0.001);
241 assert!(handle.tick(0.25));
242 assert_relative_eq!(handle.value(), 2.5, epsilon = 0.001);
243
244 handle.animate_to(1.0, MotionConfig::Spring(SpringConfig::snappy()));
245 assert!(handle.is_animating());
246 assert!(handle.tick(1.0 / 60.0));
247 assert!(handle.value() < 2.5);
248
249 handle.keyframes(
250 KeyframeTrack::new()
251 .push(0.0, 4.0_f32)
252 .push(0.5, 8.0)
253 .push(1.0, 12.0),
254 );
255 assert!(handle.tick(0.5));
256 assert_relative_eq!(handle.value(), 8.0, epsilon = 0.001);
257 assert!(!handle.tick(0.5));
258 assert_relative_eq!(handle.value(), 12.0, epsilon = 0.001);
259 assert!(!handle.is_animating());
260 }
261}