Skip to main content

dioxus_motion/
motion.rs

1use crate::Duration;
2use crate::animations::core::{Animatable, AnimationMode, LoopMode};
3use crate::animations::spring::{Spring, SpringState};
4use crate::keyframes::KeyframeAnimation;
5use crate::prelude::AnimationConfig;
6use crate::sequence::AnimationSequence;
7
8#[cfg(not(feature = "web"))]
9use crate::pool::SpringIntegrator;
10
11#[derive(Clone)]
12pub struct Motion<T: Animatable + Send + 'static> {
13    pub initial: T,
14    pub current: T,
15    pub target: T,
16    pub velocity: T,
17    pub running: bool,
18    pub elapsed: Duration,
19    pub delay_elapsed: Duration,
20    pub current_loop: u8,
21    pub reverse: bool,
22    config: AnimationConfig,
23    pub sequence: Option<AnimationSequence<T>>,
24    pub keyframe_animation: Option<KeyframeAnimation<T>>,
25}
26
27impl<T: Animatable + Send + 'static> Motion<T> {
28    pub fn new(initial: T) -> Self {
29        Self {
30            initial,
31            current: initial,
32            target: initial,
33            velocity: T::default(),
34            running: false,
35            elapsed: Duration::default(),
36            delay_elapsed: Duration::default(),
37            current_loop: 0,
38            reverse: false,
39            config: AnimationConfig::default(),
40            sequence: None,
41            keyframe_animation: None,
42        }
43    }
44
45    pub fn animate_to(&mut self, target: T, config: AnimationConfig) {
46        self.sequence = None;
47        self.keyframe_animation = None;
48        self.start_animation(target, config);
49    }
50
51    pub fn animate_sequence(&mut self, sequence: AnimationSequence<T>) {
52        sequence.reset();
53        if let Some(first_step) = sequence.current_step_data() {
54            self.start_animation(first_step.target, first_step.config.as_ref().clone());
55            self.sequence = Some(sequence);
56        }
57    }
58
59    pub fn animate_keyframes(&mut self, animation: KeyframeAnimation<T>) {
60        self.sequence = None;
61        self.keyframe_animation = Some(animation);
62        self.running = true;
63        self.elapsed = Duration::default();
64        self.delay_elapsed = Duration::default();
65        self.velocity = T::default();
66        self.current_loop = 0;
67        self.reverse = false;
68    }
69
70    pub fn get_value(&self) -> T {
71        self.current
72    }
73
74    pub fn is_running(&self) -> bool {
75        self.running
76    }
77
78    pub fn reset(&mut self) {
79        self.stop();
80        self.current = self.initial;
81        self.target = self.initial;
82        self.elapsed = Duration::default();
83        self.delay_elapsed = Duration::default();
84    }
85
86    pub fn stop(&mut self) {
87        self.running = false;
88        self.current_loop = 0;
89        self.velocity = T::default();
90        self.reverse = false;
91        self.sequence = None;
92        self.keyframe_animation = None;
93    }
94
95    pub fn delay(&mut self, duration: Duration) {
96        self.config.delay = duration;
97    }
98
99    /// Gets the effective epsilon threshold for this animation.
100    pub fn get_epsilon(&self) -> f32 {
101        self.config.epsilon.unwrap_or_else(T::epsilon)
102    }
103
104    pub fn update(&mut self, dt: f32) -> bool {
105        const MIN_DELTA: f32 = 1.0 / 240.0;
106
107        if !self.running {
108            return false;
109        }
110
111        if dt < MIN_DELTA {
112            return true;
113        }
114
115        if self.delay_elapsed < self.config.delay {
116            self.delay_elapsed += Duration::from_secs_f32(dt);
117            return true;
118        }
119
120        if self.keyframe_animation.is_some() {
121            if self.update_keyframes(dt) {
122                self.finish_motion();
123                return false;
124            }
125            return true;
126        }
127
128        let completed = match self.config.mode {
129            AnimationMode::Spring(spring) => {
130                let state = self.update_spring(spring, dt);
131                matches!(state, SpringState::Completed)
132            }
133            AnimationMode::Tween(tween) => self.update_tween(tween, dt),
134        };
135
136        if !completed {
137            return true;
138        }
139
140        if self.sequence.is_some() {
141            return self.advance_sequence_step();
142        }
143
144        self.handle_completion()
145    }
146
147    fn start_animation(&mut self, target: T, config: AnimationConfig) {
148        self.initial = self.current;
149        self.target = target;
150        self.running = true;
151        self.elapsed = Duration::default();
152        self.delay_elapsed = Duration::default();
153        self.velocity = T::default();
154        self.current_loop = 0;
155        self.reverse = false;
156        self.config = config;
157    }
158
159    fn advance_sequence_step(&mut self) -> bool {
160        let Some(sequence) = self.sequence.as_mut() else {
161            return false;
162        };
163
164        let next_step = if sequence.advance_step() {
165            sequence
166                .current_step_data()
167                .map(|step| (step.target, step.config.as_ref().clone()))
168        } else {
169            sequence.execute_completion();
170            None
171        };
172
173        if let Some((target, config)) = next_step {
174            self.start_animation(target, config);
175            return true;
176        }
177
178        self.finish_motion();
179        false
180    }
181
182    fn update_keyframes(&mut self, dt: f32) -> bool {
183        let Some(animation) = self.keyframe_animation.as_ref() else {
184            return true;
185        };
186
187        let (current, next_elapsed, completed) = {
188            let duration_secs = animation.duration.as_secs_f32();
189            let next_elapsed_secs = self.elapsed.as_secs_f32() + dt;
190            let progress = if duration_secs == 0.0 {
191                1.0
192            } else {
193                (next_elapsed_secs / duration_secs).clamp(0.0, 1.0)
194            };
195
196            if animation.keyframes.is_empty() {
197                return true;
198            }
199
200            let (start, end) = if let Some(window) = animation
201                .keyframes
202                .windows(2)
203                .find(|window| progress >= window[0].offset && progress <= window[1].offset)
204            {
205                (&window[0], &window[1])
206            } else if progress <= animation.keyframes[0].offset {
207                let first = &animation.keyframes[0];
208                (first, first)
209            } else if let Some(last) = animation.keyframes.last() {
210                (last, last)
211            } else {
212                return true;
213            };
214
215            let local_progress = if start.offset == end.offset {
216                1.0
217            } else {
218                (progress - start.offset) / (end.offset - start.offset)
219            };
220
221            let eased_progress = end
222                .easing
223                .map_or(local_progress, |ease| (ease)(local_progress, 0.0, 1.0, 1.0));
224
225            (
226                start.value.interpolate(&end.value, eased_progress),
227                Duration::from_secs_f32(next_elapsed_secs),
228                progress >= 1.0,
229            )
230        };
231
232        self.current = current;
233        self.elapsed = next_elapsed;
234
235        completed
236    }
237
238    fn update_spring(&mut self, spring: Spring, dt: f32) -> SpringState {
239        let epsilon = self.get_epsilon();
240        let delta = self.target - self.current;
241
242        if delta.magnitude() < epsilon && self.velocity.magnitude() < epsilon {
243            self.current = self.target;
244            self.velocity = T::default();
245            return SpringState::Completed;
246        }
247
248        #[cfg(feature = "web")]
249        {
250            let stiffness = spring.stiffness;
251            let damping = spring.damping;
252            let mass_inv = 1.0 / spring.mass;
253
254            const FIXED_DT: f32 = 1.0 / 120.0;
255            let steps = ((dt / FIXED_DT) as usize).max(1);
256            let step_dt = dt / steps as f32;
257
258            for _ in 0..steps {
259                let step_delta = self.target - self.current;
260                let force = step_delta * stiffness;
261                let damping_force = self.velocity * damping;
262                self.velocity = self.velocity + (force - damping_force) * (mass_inv * step_dt);
263                self.current = self.current + self.velocity * step_dt;
264            }
265        }
266
267        #[cfg(not(feature = "web"))]
268        {
269            let mut integrator = SpringIntegrator::new();
270            let (new_pos, new_vel) =
271                integrator.integrate_rk4(self.current, self.velocity, self.target, &spring, dt);
272            self.current = new_pos;
273            self.velocity = new_vel;
274        }
275
276        self.check_spring_completion()
277    }
278
279    fn check_spring_completion(&mut self) -> SpringState {
280        let epsilon = self.get_epsilon();
281        let epsilon_sq = epsilon * epsilon;
282        let velocity_sq = self.velocity.magnitude().powi(2);
283        let delta_sq = (self.target - self.current).magnitude().powi(2);
284
285        if velocity_sq < epsilon_sq && delta_sq < epsilon_sq {
286            self.current = self.target;
287            self.velocity = T::default();
288            SpringState::Completed
289        } else {
290            SpringState::Active
291        }
292    }
293
294    fn update_tween(&mut self, tween: crate::prelude::Tween, dt: f32) -> bool {
295        let elapsed_secs = self.elapsed.as_secs_f32() + dt;
296        self.elapsed = Duration::from_secs_f32(elapsed_secs);
297        let duration_secs = tween.duration.as_secs_f32();
298
299        let progress = if duration_secs == 0.0 {
300            1.0
301        } else {
302            (elapsed_secs / duration_secs).min(1.0)
303        };
304
305        if progress <= 0.0 {
306            self.current = self.initial;
307            return false;
308        }
309
310        if progress >= 1.0 {
311            self.current = self.target;
312            return true;
313        }
314
315        let eased_progress = (tween.easing)(progress, 0.0, 1.0, 1.0);
316        self.current = match eased_progress {
317            0.0 => self.initial,
318            1.0 => self.target,
319            _ => self.initial.interpolate(&self.target, eased_progress),
320        };
321
322        false
323    }
324
325    fn handle_completion(&mut self) -> bool {
326        match self.config.loop_mode.unwrap_or(LoopMode::None) {
327            LoopMode::None => {
328                self.config.execute_completion();
329                self.finish_motion();
330                false
331            }
332            LoopMode::Infinite => {
333                self.restart_motion();
334                true
335            }
336            LoopMode::Times(count) => {
337                self.current_loop += 1;
338                if self.current_loop >= count {
339                    self.config.execute_completion();
340                    self.finish_motion();
341                    false
342                } else {
343                    self.restart_motion();
344                    true
345                }
346            }
347            LoopMode::Alternate => {
348                self.reverse_motion();
349                true
350            }
351            LoopMode::AlternateTimes(count) => {
352                self.current_loop += 1;
353                if self.current_loop >= count * 2 {
354                    self.config.execute_completion();
355                    self.finish_motion();
356                    false
357                } else {
358                    self.reverse_motion();
359                    true
360                }
361            }
362        }
363    }
364
365    fn finish_motion(&mut self) {
366        self.running = false;
367        self.current_loop = 0;
368        self.velocity = T::default();
369        self.sequence = None;
370        self.keyframe_animation = None;
371    }
372
373    fn restart_motion(&mut self) {
374        self.current = self.initial;
375        self.elapsed = Duration::default();
376        self.delay_elapsed = Duration::default();
377        self.velocity = T::default();
378        self.running = true;
379    }
380
381    fn reverse_motion(&mut self) {
382        self.reverse = !self.reverse;
383        std::mem::swap(&mut self.initial, &mut self.target);
384        self.restart_motion();
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    #![allow(clippy::unwrap_used)]
391
392    use super::*;
393    use crate::animations::core::AnimationMode;
394    use crate::animations::spring::Spring;
395    use crate::prelude::Tween;
396    use std::sync::{Arc, Mutex};
397
398    fn instant_tween() -> AnimationConfig {
399        AnimationConfig::new(AnimationMode::Tween(Tween::new(Duration::from_secs(0))))
400    }
401
402    #[test]
403    fn test_motion_new() {
404        let motion = Motion::new(0.0f32);
405
406        assert_eq!(motion.initial, 0.0);
407        assert_eq!(motion.current, 0.0);
408        assert_eq!(motion.target, 0.0);
409        assert!(!motion.running);
410        assert!(motion.sequence.is_none());
411        assert!(motion.keyframe_animation.is_none());
412    }
413
414    #[test]
415    fn test_motion_animate_to() {
416        let mut motion = Motion::new(0.0f32);
417        motion.animate_to(
418            100.0,
419            AnimationConfig::new(AnimationMode::Tween(Tween::default())),
420        );
421
422        assert_eq!(motion.target, 100.0);
423        assert!(motion.running);
424        assert!(motion.sequence.is_none());
425        assert!(motion.keyframe_animation.is_none());
426    }
427
428    #[test]
429    fn test_motion_sequence_advances() {
430        let mut motion = Motion::new(0.0f32);
431        let sequence = AnimationSequence::new()
432            .then(50.0f32, instant_tween())
433            .then(100.0f32, instant_tween());
434
435        motion.animate_sequence(sequence);
436
437        assert_eq!(motion.target, 50.0);
438        assert!(motion.sequence.is_some());
439
440        assert!(motion.update(1.0 / 60.0));
441        assert_eq!(motion.target, 100.0);
442        assert!(motion.running);
443
444        assert!(!motion.update(1.0 / 60.0));
445        assert_eq!(motion.current, 100.0);
446        assert!(!motion.running);
447        assert!(motion.sequence.is_none());
448    }
449
450    #[test]
451    fn test_motion_keyframes_progress_and_complete() {
452        let mut motion = Motion::new(0.0f32);
453
454        let animation = KeyframeAnimation::new(Duration::from_secs(1))
455            .add_keyframe(0.0, 0.0, None)
456            .unwrap()
457            .add_keyframe(100.0, 1.0, None)
458            .unwrap();
459
460        motion.animate_keyframes(animation);
461
462        assert!(motion.update(0.5));
463        assert!(motion.current > 0.0);
464        assert!(motion.current < 100.0);
465
466        assert!(!motion.update(0.5));
467        assert_eq!(motion.current, 100.0);
468        assert!(!motion.running);
469        assert!(motion.keyframe_animation.is_none());
470    }
471
472    #[test]
473    fn test_motion_stop() {
474        let mut motion = Motion::new(0.0f32);
475        motion.animate_to(
476            100.0,
477            AnimationConfig::new(AnimationMode::Spring(Spring::default())),
478        );
479
480        motion.stop();
481
482        assert!(!motion.running);
483        assert!(motion.sequence.is_none());
484        assert!(motion.keyframe_animation.is_none());
485        assert_eq!(motion.velocity, 0.0);
486    }
487
488    #[test]
489    fn test_motion_get_epsilon() {
490        let mut motion = Motion::new(0.0f32);
491        assert_eq!(motion.get_epsilon(), f32::epsilon());
492
493        motion.animate_to(
494            1.0,
495            AnimationConfig::new(AnimationMode::Tween(Tween::default())).with_epsilon(0.01),
496        );
497
498        assert_eq!(motion.get_epsilon(), 0.01);
499    }
500
501    #[test]
502    fn test_motion_delay_prevents_early_update() {
503        let mut motion = Motion::new(0.0f32);
504        motion.animate_to(
505            100.0,
506            AnimationConfig::new(AnimationMode::Tween(Tween::default())),
507        );
508        motion.delay(Duration::from_millis(100));
509
510        assert!(motion.update(1.0 / 60.0));
511        assert_eq!(motion.current, motion.initial);
512    }
513
514    #[test]
515    fn test_motion_update_tween_changes_value() {
516        let mut motion = Motion::new(0.0f32);
517        motion.animate_to(
518            100.0,
519            AnimationConfig::new(AnimationMode::Tween(Tween::default())),
520        );
521
522        assert!(motion.update(1.0 / 60.0));
523        assert!(motion.current > 0.0);
524        assert!(motion.current < 100.0);
525    }
526
527    #[test]
528    fn test_motion_spring_completes_when_already_settled() {
529        let mut motion = Motion::new(0.0f32);
530        motion.animate_to(
531            0.0,
532            AnimationConfig::new(AnimationMode::Spring(Spring::default())),
533        );
534        motion.velocity = 0.0;
535
536        assert!(!motion.update(1.0 / 60.0));
537        assert_eq!(motion.current, 0.0);
538        assert!(!motion.running);
539    }
540
541    #[test]
542    fn test_motion_loop_mode_times() {
543        let mut motion = Motion::new(0.0f32);
544        motion.animate_to(100.0, instant_tween().with_loop(LoopMode::Times(2)));
545
546        assert!(motion.update(1.0 / 60.0));
547        assert_eq!(motion.current, motion.initial);
548        assert!(motion.running);
549
550        assert!(!motion.update(1.0 / 60.0));
551        assert!(!motion.running);
552    }
553
554    #[test]
555    fn test_motion_loop_mode_alternate() {
556        let mut motion = Motion::new(0.0f32);
557        motion.animate_to(100.0, instant_tween().with_loop(LoopMode::Alternate));
558
559        assert!(motion.update(1.0 / 60.0));
560        assert!(motion.running);
561        assert!(motion.reverse);
562        assert_eq!(motion.initial, 100.0);
563        assert_eq!(motion.target, 0.0);
564    }
565
566    #[test]
567    fn test_motion_completion_callback() {
568        let called = Arc::new(Mutex::new(false));
569        let called_clone = called.clone();
570        let config = instant_tween().with_on_complete(move || {
571            *called_clone.lock().unwrap() = true;
572        });
573
574        let mut motion = Motion::new(0.0f32);
575        motion.animate_to(100.0, config);
576
577        assert!(!motion.update(1.0 / 60.0));
578        assert!(*called.lock().unwrap());
579    }
580
581    #[test]
582    fn test_motion_get_value_tracks_current_directly() {
583        let mut motion = Motion::new(0.0f32);
584        motion.current = 12.5;
585        assert_eq!(motion.get_value(), 12.5);
586
587        motion.current = 42.0;
588        assert_eq!(motion.get_value(), 42.0);
589    }
590}