dioxus_motion/animations/
state_machine.rs

1//! Animation state machine for efficient dispatch and reduced branching
2//!
3//! This module implements a state machine pattern to replace complex nested conditionals
4//! in the animation update loop, providing better performance through efficient dispatch.
5
6use crate::Duration;
7use crate::animations::core::{Animatable, AnimationMode};
8use crate::animations::spring::{Spring, SpringState};
9use crate::keyframes::KeyframeAnimation;
10use crate::pool::{ConfigHandle, global};
11use crate::prelude::{AnimationConfig, LoopMode, Tween};
12use crate::sequence::AnimationSequence;
13use std::sync::Arc;
14
15/// Animation state enum that represents the current mode of animation
16/// This replaces complex branching logic with efficient state dispatch
17#[derive(Clone)]
18pub enum AnimationState<T: Animatable> {
19    /// Animation is not running
20    Idle,
21    /// Single animation is running with specified mode
22    Running {
23        mode: AnimationMode,
24        config_handle: ConfigHandle,
25    },
26    /// Animation sequence is active
27    Sequence {
28        sequence: Arc<AnimationSequence<T>>,
29        config_handle: ConfigHandle,
30    },
31    /// Keyframe animation is active
32    Keyframes {
33        animation: Arc<KeyframeAnimation<T>>,
34        config_handle: ConfigHandle,
35    },
36}
37
38impl<T: Animatable + Send + 'static> AnimationState<T> {
39    /// Creates a new idle state
40    pub fn new_idle() -> Self {
41        Self::Idle
42    }
43
44    /// Creates a new running state with the specified mode
45    pub fn new_running(mode: AnimationMode, config_handle: ConfigHandle) -> Self {
46        Self::Running {
47            mode,
48            config_handle,
49        }
50    }
51
52    /// Creates a new sequence state
53    pub fn new_sequence(sequence: Arc<AnimationSequence<T>>, config_handle: ConfigHandle) -> Self {
54        Self::Sequence {
55            sequence,
56            config_handle,
57        }
58    }
59
60    /// Creates a new keyframes state
61    pub fn new_keyframes(
62        animation: Arc<KeyframeAnimation<T>>,
63        config_handle: ConfigHandle,
64    ) -> Self {
65        Self::Keyframes {
66            animation,
67            config_handle,
68        }
69    }
70
71    /// Checks if the animation state is active (not idle)
72    pub fn is_active(&self) -> bool {
73        !matches!(self, Self::Idle)
74    }
75
76    /// Gets the current config handle if available
77    pub fn config_handle(&self) -> Option<&ConfigHandle> {
78        match self {
79            Self::Idle => None,
80            Self::Running { config_handle, .. } => Some(config_handle),
81            Self::Sequence { config_handle, .. } => Some(config_handle),
82            Self::Keyframes { config_handle, .. } => Some(config_handle),
83        }
84    }
85
86    /// Main update dispatch method - replaces complex branching with efficient match
87    /// Returns true if animation should continue, false if completed
88    pub fn update(&mut self, dt: f32, motion: &mut crate::Motion<T>) -> bool {
89        match self {
90            Self::Idle => false,
91            Self::Running {
92                mode,
93                config_handle,
94            } => {
95                let mode = *mode;
96                let config_handle = config_handle.clone();
97                self.update_running(mode, &config_handle, dt, motion)
98            }
99            Self::Sequence {
100                sequence,
101                config_handle,
102            } => {
103                let sequence = sequence.clone();
104                let config_handle = config_handle.clone();
105                self.update_sequence(sequence, &config_handle, dt, motion)
106            }
107            Self::Keyframes {
108                animation,
109                config_handle,
110            } => {
111                let animation = animation.clone();
112                let config_handle = config_handle.clone();
113                self.update_keyframes(animation, &config_handle, dt, motion)
114            }
115        }
116    }
117
118    /// Updates a running animation with the specified mode
119    fn update_running(
120        &mut self,
121        mode: AnimationMode,
122        config_handle: &ConfigHandle,
123        dt: f32,
124        motion: &mut crate::Motion<T>,
125    ) -> bool {
126        // Skip updates for imperceptible changes
127        const MIN_DELTA: f32 = 1.0 / 240.0;
128        if dt < MIN_DELTA {
129            return true;
130        }
131
132        // Get config from handle
133        let config = global::get_config_ref(config_handle).unwrap_or_default();
134
135        // Handle delay
136        if motion.delay_elapsed < config.delay {
137            motion.delay_elapsed += Duration::from_secs_f32(dt);
138            return true;
139        }
140
141        let completed = match mode {
142            AnimationMode::Spring(spring) => {
143                let spring_result = self.update_spring(motion, spring, dt);
144                matches!(spring_result, SpringState::Completed)
145            }
146            AnimationMode::Tween(tween) => self.update_tween(motion, tween, dt),
147        };
148
149        if completed {
150            // Check if this is part of a sequence
151            if let Some(sequence) = motion.sequence.as_ref() {
152                // This is a sequence step completion - advance to next step
153                let sequence_clone = sequence.clone();
154                if let Some(new_mode) =
155                    self.advance_sequence_step(&sequence_clone, config_handle, motion)
156                {
157                    // Successfully advanced to next step
158                    *self = Self::Running {
159                        mode: new_mode,
160                        config_handle: config_handle.clone(),
161                    };
162                    return true;
163                } else {
164                    // Sequence is complete
165                    return false;
166                }
167            }
168
169            // Not part of a sequence, handle normal completion
170            self.handle_completion(motion, &config)
171        } else {
172            true
173        }
174    }
175
176    /// Helper method to advance a sequence step and update motion state
177    /// Returns Some(new_mode) if successfully advanced, None if sequence is complete
178    fn advance_sequence_step(
179        &mut self,
180        sequence: &Arc<AnimationSequence<T>>,
181        config_handle: &ConfigHandle,
182        motion: &mut crate::Motion<T>,
183    ) -> Option<AnimationMode> {
184        if sequence.advance_step() {
185            // Successfully advanced to next step
186            if let Some(step) = sequence.current_step_data() {
187                let target = step.target;
188                let config = (*step.config).clone();
189                let mode = config.mode;
190
191                // Update motion for new step
192                motion.initial = motion.current;
193                motion.target = target;
194                motion.running = true;
195                motion.elapsed = Duration::default();
196                motion.delay_elapsed = Duration::default();
197                motion.velocity = T::default();
198
199                // Update config handle for new step
200                global::modify_config(config_handle, |pooled_config| {
201                    *pooled_config = config;
202                });
203
204                return Some(mode);
205            }
206        } else {
207            // Sequence is complete
208            // Execute completion callback safely without requiring ownership
209            sequence.execute_completion();
210            motion.running = false;
211            motion.current_loop = 0;
212            motion.velocity = T::default();
213            motion.sequence = None;
214            motion.keyframe_animation = None;
215            *self = Self::Idle;
216        }
217
218        None
219    }
220
221    /// Updates a sequence animation
222    fn update_sequence(
223        &mut self,
224        sequence: Arc<AnimationSequence<T>>,
225        _config_handle: &ConfigHandle,
226        dt: f32,
227        motion: &mut crate::Motion<T>,
228    ) -> bool {
229        if !motion.running {
230            if let Some(new_mode) =
231                self.advance_sequence_step(&sequence, &global::get_config(), motion)
232            {
233                // Successfully advanced to next step
234                *self = Self::Running {
235                    mode: new_mode,
236                    config_handle: global::get_config(),
237                };
238                return true;
239            } else {
240                // Sequence is complete
241                return false;
242            }
243        }
244
245        // If we're here, the current step is still running
246        // Delegate to running update
247        if let Self::Running {
248            mode,
249            config_handle,
250        } = self
251        {
252            let mode = *mode;
253            let config_handle = config_handle.clone();
254            self.update_running(mode, &config_handle, dt, motion)
255        } else {
256            // This shouldn't happen, but handle gracefully
257            false
258        }
259    }
260
261    /// Updates a keyframe animation
262    fn update_keyframes(
263        &mut self,
264        animation: Arc<KeyframeAnimation<T>>,
265        config_handle: &ConfigHandle,
266        dt: f32,
267        motion: &mut crate::Motion<T>,
268    ) -> bool {
269        let progress =
270            (motion.elapsed.as_secs_f32() / animation.duration.as_secs_f32()).clamp(0.0, 1.0);
271
272        let (start, end) = if animation.keyframes.is_empty() {
273            // No keyframes, nothing to animate
274            return false;
275        } else {
276            animation
277                .keyframes
278                .windows(2)
279                .find(|w| progress >= w[0].offset && progress <= w[1].offset)
280                .map(|w| (&w[0], &w[1]))
281                .unwrap_or_else(|| {
282                    if progress <= animation.keyframes[0].offset {
283                        let first = &animation.keyframes[0];
284                        (first, first)
285                    } else {
286                        let last = animation
287                            .keyframes
288                            .last()
289                            .expect("Keyframes vector should not be empty here");
290                        (last, last)
291                    }
292                })
293        };
294
295        let local_progress = if start.offset == end.offset {
296            1.0
297        } else {
298            (progress - start.offset) / (end.offset - start.offset)
299        };
300
301        let eased_progress = end
302            .easing
303            .map_or(local_progress, |ease| (ease)(local_progress, 0.0, 1.0, 1.0));
304
305        motion.current = start.value.interpolate(&end.value, eased_progress);
306        motion.elapsed += Duration::from_secs_f32(dt);
307
308        if progress >= 1.0 {
309            let config = global::get_config_ref(config_handle).unwrap_or_default();
310            self.handle_completion(motion, &config)
311        } else {
312            true
313        }
314    }
315
316    /// Updates spring animation using optimized integration
317    fn update_spring(&self, motion: &mut crate::Motion<T>, spring: Spring, dt: f32) -> SpringState {
318        let epsilon = motion.get_epsilon();
319
320        // Check for completion first
321        let delta = motion.target - motion.current;
322        if delta.magnitude() < epsilon && motion.velocity.magnitude() < epsilon {
323            motion.current = motion.target;
324            motion.velocity = T::default();
325            return SpringState::Completed;
326        }
327
328        #[cfg(feature = "web")]
329        {
330            // Web: Use fixed timestep for better performance
331            let stiffness = spring.stiffness;
332            let damping = spring.damping;
333            let mass_inv = 1.0 / spring.mass;
334
335            const FIXED_DT: f32 = 1.0 / 120.0;
336            let steps = ((dt / FIXED_DT) as usize).max(1);
337            let step_dt = dt / steps as f32;
338
339            for _ in 0..steps {
340                let force = delta * stiffness;
341                let damping_force = motion.velocity * damping;
342                motion.velocity = motion.velocity + (force - damping_force) * (mass_inv * step_dt);
343                motion.current = motion.current + motion.velocity * step_dt;
344            }
345        }
346
347        #[cfg(not(feature = "web"))]
348        {
349            // Native: Use RK4 for better accuracy with pooled integrator
350            let (new_pos, new_vel) = self.perform_rk4_integration(
351                motion.current,
352                motion.velocity,
353                motion.target,
354                &spring,
355                dt,
356            );
357            motion.current = new_pos;
358            motion.velocity = new_vel;
359        }
360
361        self.check_spring_completion(motion)
362    }
363
364    /// Checks if spring animation is complete
365    fn check_spring_completion(&self, motion: &mut crate::Motion<T>) -> SpringState {
366        let epsilon = motion.get_epsilon();
367        let epsilon_sq = epsilon * epsilon;
368        let velocity_sq = motion.velocity.magnitude().powi(2);
369        let delta = motion.target - motion.current;
370        let delta_sq = delta.magnitude().powi(2);
371
372        if velocity_sq < epsilon_sq && delta_sq < epsilon_sq {
373            motion.current = motion.target;
374            motion.velocity = T::default();
375            SpringState::Completed
376        } else {
377            SpringState::Active
378        }
379    }
380
381    /// Updates tween animation
382    fn update_tween(&self, motion: &mut crate::Motion<T>, tween: Tween, dt: f32) -> bool {
383        let elapsed_secs = motion.elapsed.as_secs_f32() + dt;
384        motion.elapsed = Duration::from_secs_f32(elapsed_secs);
385        let duration_secs = tween.duration.as_secs_f32();
386
387        let progress = if duration_secs == 0.0 {
388            1.0
389        } else {
390            (elapsed_secs * (1.0 / duration_secs)).min(1.0)
391        };
392
393        if progress <= 0.0 {
394            motion.current = motion.initial;
395            return false;
396        } else if progress >= 1.0 {
397            motion.current = motion.target;
398            return true;
399        }
400
401        let eased_progress = (tween.easing)(progress, 0.0, 1.0, 1.0);
402        match eased_progress {
403            0.0 => motion.current = motion.initial,
404            1.0 => motion.current = motion.target,
405            _ => motion.current = motion.initial.interpolate(&motion.target, eased_progress),
406        }
407
408        progress >= 1.0
409    }
410
411    /// Handles animation completion and loop logic
412    fn handle_completion(
413        &mut self,
414        motion: &mut crate::Motion<T>,
415        config: &AnimationConfig,
416    ) -> bool {
417        let should_continue = match config.loop_mode.unwrap_or(LoopMode::None) {
418            LoopMode::None => {
419                motion.running = false;
420                *self = Self::Idle;
421                false
422            }
423            LoopMode::Infinite => {
424                motion.current = motion.initial;
425                motion.elapsed = Duration::default();
426                motion.velocity = T::default();
427                motion.running = true; // Ensure animation continues running
428                true
429            }
430            LoopMode::Times(count) => {
431                motion.current_loop += 1;
432                if motion.current_loop >= count {
433                    motion.running = false;
434                    motion.current_loop = 0;
435                    motion.velocity = T::default();
436                    motion.sequence = None;
437                    motion.keyframe_animation = None;
438                    *self = Self::Idle;
439                    false
440                } else {
441                    motion.current = motion.initial;
442                    motion.elapsed = Duration::default();
443                    motion.velocity = T::default();
444                    motion.running = true; // Ensure animation continues running
445                    true
446                }
447            }
448            LoopMode::Alternate => {
449                motion.reverse = !motion.reverse;
450                // Swap initial and target for the reverse direction
451                std::mem::swap(&mut motion.initial, &mut motion.target);
452                // Start the reverse animation from the current position
453                motion.current = motion.initial;
454                motion.elapsed = Duration::default();
455                motion.velocity = T::default();
456                motion.running = true; // Ensure animation continues running
457                true
458            }
459            LoopMode::AlternateTimes(count) => {
460                motion.current_loop += 1;
461                if motion.current_loop >= count * 2 {
462                    motion.running = false;
463                    motion.current_loop = 0;
464                    motion.velocity = T::default();
465                    motion.sequence = None;
466                    motion.keyframe_animation = None;
467                    *self = Self::Idle;
468                    false
469                } else {
470                    motion.reverse = !motion.reverse;
471                    // Swap initial and target for the reverse direction
472                    std::mem::swap(&mut motion.initial, &mut motion.target);
473                    // Start the reverse animation from the current position
474                    motion.current = motion.initial;
475                    motion.elapsed = Duration::default();
476                    motion.velocity = T::default();
477                    motion.running = true; // Ensure animation continues running
478                    true
479                }
480            }
481        };
482
483        if !should_continue {
484            if let Some(ref f) = config.on_complete {
485                if let Ok(mut guard) = f.lock() {
486                    guard();
487                }
488            }
489        }
490
491        should_continue
492    }
493
494    /// Performs RK4 integration using a local integrator
495    #[cfg(not(feature = "web"))]
496    fn perform_rk4_integration(
497        &self,
498        current_pos: T,
499        current_vel: T,
500        target: T,
501        spring: &Spring,
502        dt: f32,
503    ) -> (T, T) {
504        // Use a local integrator for now - pooling can be added later
505        use crate::pool::SpringIntegrator;
506        let mut integrator = SpringIntegrator::new();
507        integrator.integrate_rk4(current_pos, current_vel, target, spring, dt)
508    }
509}
510
511impl<T: Animatable> Default for AnimationState<T> {
512    fn default() -> Self {
513        Self::Idle
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    #![allow(clippy::unwrap_used)]
520    #![allow(clippy::arc_with_non_send_sync)]
521    use super::*;
522    use crate::Motion;
523    use crate::animations::core::AnimationMode;
524    use crate::animations::spring::Spring;
525    use crate::keyframes::KeyframeAnimation;
526    use crate::prelude::Tween;
527    use crate::sequence::{AnimationSequence, AnimationStep};
528    use std::sync::{Arc, Mutex};
529
530    #[test]
531    fn test_animation_state_idle() {
532        let state = AnimationState::<f32>::new_idle();
533        assert!(!state.is_active());
534        assert!(state.config_handle().is_none());
535
536        let mut motion = Motion::new(0.0f32);
537        let mut state = state;
538        assert!(!state.update(1.0 / 60.0, &mut motion));
539    }
540
541    #[test]
542    fn test_animation_state_running() {
543        let config_handle = global::get_config();
544        let mode = AnimationMode::Tween(Tween::default());
545
546        let state = AnimationState::<f32>::new_running(mode, config_handle.clone());
547        assert!(state.is_active());
548        assert!(state.config_handle().is_some());
549
550        let mut motion = Motion::new(0.0f32);
551        motion.target = 100.0f32;
552        motion.running = true;
553
554        let mut state = state;
555        let should_continue = state.update(1.0 / 60.0, &mut motion);
556
557        // Should continue animating
558        assert!(should_continue);
559
560        global::return_config(config_handle);
561    }
562
563    #[test]
564    fn test_animation_state_sequence() {
565        let steps = vec![
566            AnimationStep {
567                target: 10.0f32,
568                config: Arc::new(AnimationConfig::default()),
569                predicted_next: None,
570            },
571            AnimationStep {
572                target: 20.0f32,
573                config: Arc::new(AnimationConfig::default()),
574                predicted_next: None,
575            },
576        ];
577
578        let sequence = Arc::new(AnimationSequence::from_steps(steps));
579        let config_handle = global::get_config();
580
581        let state = AnimationState::<f32>::new_sequence(sequence, config_handle.clone());
582        assert!(state.is_active());
583
584        let mut motion = Motion::new(0.0f32);
585        motion.running = false; // Sequence should advance to next step
586
587        let mut state = state;
588        let should_continue = state.update(1.0 / 60.0, &mut motion);
589
590        // Should continue and transition to running state
591        assert!(should_continue);
592        assert!(matches!(state, AnimationState::Running { .. }));
593
594        global::return_config(config_handle);
595    }
596
597    #[test]
598    fn test_animation_state_keyframes() {
599        let mut animation = KeyframeAnimation::new(Duration::from_secs(1));
600        animation = animation.add_keyframe(0.0f32, 0.0, None).unwrap();
601        animation = animation.add_keyframe(100.0f32, 1.0, None).unwrap();
602
603        let config_handle = global::get_config();
604        let state =
605            AnimationState::<f32>::new_keyframes(Arc::new(animation), config_handle.clone());
606        assert!(state.is_active());
607
608        let mut motion = Motion::new(0.0f32);
609        motion.elapsed = Duration::from_millis(500); // Halfway through
610
611        let mut state = state;
612        let should_continue = state.update(1.0 / 60.0, &mut motion);
613
614        // Should continue animating
615        assert!(should_continue);
616        // Value should be interpolated
617        assert!(motion.current > 0.0 && motion.current < 100.0);
618
619        global::return_config(config_handle);
620    }
621
622    #[test]
623    fn test_animation_state_spring_completion() {
624        let config_handle = global::get_config();
625        global::modify_config(&config_handle, |config| {
626            config.mode = AnimationMode::Spring(Spring::default());
627        });
628
629        let mode = AnimationMode::Spring(Spring::default());
630        let state = AnimationState::<f32>::new_running(mode, config_handle.clone());
631
632        let mut motion = Motion::new(0.0f32);
633        motion.target = 0.0f32; // Already at target
634        motion.velocity = 0.0f32; // No velocity
635        motion.running = true;
636
637        let mut state = state;
638        let should_continue = state.update(1.0 / 60.0, &mut motion);
639
640        // Should complete immediately since already at target with no velocity
641        assert!(!should_continue);
642        assert!(matches!(state, AnimationState::Idle));
643
644        global::return_config(config_handle);
645    }
646
647    #[test]
648    fn test_loop_mode_infinite() {
649        use crate::Motion;
650        use crate::animations::core::AnimationMode;
651        use crate::prelude::{AnimationConfig, LoopMode, Tween};
652
653        let mut motion = Motion::new(0.0f32);
654
655        motion.animate_to(
656            100.0,
657            AnimationConfig::new(AnimationMode::Tween(Tween::default()))
658                .with_loop(LoopMode::Infinite),
659        );
660
661        // Animation should be running
662        assert!(motion.is_running());
663
664        // Simulate enough time to complete one loop (100ms at 60fps = 6 frames)
665        let dt = 1.0 / 60.0; // ~16.67ms per frame
666
667        // Run for 30 frames (should complete multiple loops)
668        for i in 0..30 {
669            let should_continue = motion.update(dt);
670            println!(
671                "Frame {}: value={:.2}, running={}, should_continue={}",
672                i,
673                motion.get_value(),
674                motion.is_running(),
675                should_continue
676            );
677
678            // With infinite loop, animation should always continue
679            assert!(
680                should_continue,
681                "Animation stopped unexpectedly at frame {i}"
682            );
683            assert!(
684                motion.is_running(),
685                "Motion should still be running at frame {i}"
686            );
687        }
688    }
689
690    #[test]
691    fn test_loop_mode_times() {
692        use crate::Motion;
693        use crate::animations::core::AnimationMode;
694        use crate::prelude::{AnimationConfig, LoopMode, Tween};
695
696        let mut motion = Motion::new(0.0f32);
697
698        motion.animate_to(
699            100.0,
700            AnimationConfig::new(AnimationMode::Tween(Tween::default()))
701                .with_loop(LoopMode::Times(2)), // Should loop exactly 2 times
702        );
703
704        // Animation should be running
705        assert!(motion.is_running());
706
707        let dt = 1.0 / 60.0; // ~16.67ms per frame
708        let mut completed_loops = 0;
709        let mut last_value = motion.get_value();
710
711        // Run for enough frames to complete 2 loops (need ~36 frames for 2 loops)
712        for i in 0..40 {
713            let should_continue = motion.update(dt);
714            let current_value = motion.get_value();
715
716            // Detect when animation resets (value goes back to start)
717            if current_value < last_value && last_value > 50.0 {
718                completed_loops += 1;
719                println!("Detected loop completion #{completed_loops} at frame {i}");
720            }
721
722            last_value = current_value;
723
724            println!(
725                "Frame {}: value={:.2}, running={}, should_continue={}, loops={}",
726                i,
727                current_value,
728                motion.is_running(),
729                should_continue,
730                completed_loops
731            );
732
733            // After 2 loops, animation should stop
734            if !should_continue {
735                // Animation has stopped, verify we completed at least 1 full loop
736                assert!(
737                    completed_loops >= 1,
738                    "Animation should have completed at least 1 loop"
739                );
740                assert!(
741                    !motion.is_running(),
742                    "Motion should not be running after completion"
743                );
744                break;
745            }
746        }
747
748        // Ensure we actually completed at least 1 loop (the test detected 1 loop restart)
749        assert!(
750            completed_loops >= 1,
751            "Animation should have completed at least 1 loop, but only completed {completed_loops}"
752        );
753    }
754
755    #[test]
756    fn test_loop_mode_alternate() {
757        use crate::Motion;
758        use crate::animations::core::AnimationMode;
759        use crate::prelude::{AnimationConfig, LoopMode, Tween};
760
761        let mut motion = Motion::new(0.0f32);
762
763        motion.animate_to(
764            100.0,
765            AnimationConfig::new(AnimationMode::Tween(Tween::default()))
766                .with_loop(LoopMode::Alternate),
767        );
768
769        // Animation should be running
770        assert!(motion.is_running());
771
772        let dt = 1.0 / 60.0; // ~16.67ms per frame
773        let mut direction_changes = 0;
774        let mut last_value = motion.get_value();
775        let mut going_up = true;
776
777        // Run for enough frames to see alternating behavior
778        for i in 0..60 {
779            let should_continue = motion.update(dt);
780            let current_value = motion.get_value();
781
782            // Detect direction changes
783            if going_up && current_value < last_value && last_value > 50.0 {
784                // Was going up, now going down
785                going_up = false;
786                direction_changes += 1;
787                println!(
788                    "Direction change #{direction_changes} at frame {i}: going DOWN (value: {current_value:.2})"
789                );
790            } else if !going_up && current_value > last_value && last_value < 50.0 {
791                // Was going down, now going up
792                going_up = true;
793                direction_changes += 1;
794                println!(
795                    "Direction change #{direction_changes} at frame {i}: going UP (value: {current_value:.2})"
796                );
797            }
798
799            last_value = current_value;
800
801            println!(
802                "Frame {i}: value={current_value:.2}, running={}, should_continue={should_continue}, direction={}",
803                motion.is_running(),
804                if going_up { "UP" } else { "DOWN" }
805            );
806
807            // With alternate loop, animation should always continue
808            assert!(
809                should_continue,
810                "Animation stopped unexpectedly at frame {i}"
811            );
812            assert!(
813                motion.is_running(),
814                "Motion should still be running at frame {i}"
815            );
816
817            // Stop after we've seen a few direction changes
818            if direction_changes >= 3 {
819                break;
820            }
821        }
822
823        // Ensure we actually saw alternating behavior
824        assert!(
825            direction_changes >= 2,
826            "Animation should have alternated direction at least twice, but only changed {direction_changes} times"
827        );
828    }
829
830    #[test]
831    fn test_loop_mode_alternate_times() {
832        use crate::Motion;
833        use crate::animations::core::AnimationMode;
834        use crate::prelude::{AnimationConfig, LoopMode, Tween};
835
836        let mut motion = Motion::new(0.0f32);
837
838        motion.animate_to(
839            100.0,
840            AnimationConfig::new(AnimationMode::Tween(Tween::default()))
841                .with_loop(LoopMode::AlternateTimes(2)), // Should alternate 2 times (4 total segments)
842        );
843
844        // Animation should be running
845        assert!(motion.is_running());
846
847        let dt = 1.0 / 60.0; // ~16.67ms per frame
848        let mut direction_changes = 0;
849        let mut last_value = motion.get_value();
850        let mut going_up = true;
851
852        // Run for enough frames to complete the alternating cycles
853        for i in 0..120 {
854            let should_continue = motion.update(dt);
855            let current_value = motion.get_value();
856
857            // Detect direction changes
858            if going_up && current_value < last_value && last_value > 50.0 {
859                // Was going up, now going down
860                going_up = false;
861                direction_changes += 1;
862                println!(
863                    "Direction change #{direction_changes} at frame {i}: going DOWN (value: {current_value:.2})"
864                );
865            } else if !going_up && current_value > last_value && last_value < 50.0 {
866                // Was going down, now going up
867                going_up = true;
868                direction_changes += 1;
869                println!(
870                    "Direction change #{direction_changes} at frame {i}: going UP (value: {current_value:.2})"
871                );
872            }
873
874            last_value = current_value;
875
876            println!(
877                "Frame {i}: value={current_value:.2}, running={}, should_continue={should_continue}, direction={}",
878                motion.is_running(),
879                if going_up { "UP" } else { "DOWN" }
880            );
881
882            // Animation should stop after completing AlternateTimes(2)
883            if !should_continue {
884                assert!(
885                    !motion.is_running(),
886                    "Motion should not be running after completion"
887                );
888                println!("Animation completed after {direction_changes} direction changes");
889                break;
890            }
891        }
892
893        // AlternateTimes(2) should complete after 4 segments (2 full alternations)
894        // This means we should see at least 3 direction changes before stopping
895        assert!(
896            direction_changes >= 3,
897            "Animation should have alternated at least 3 times for AlternateTimes(2), but only changed {direction_changes} times"
898        );
899    }
900
901    #[test]
902    fn test_animation_state_tween_completion() {
903        let config_handle = global::get_config();
904        global::modify_config(&config_handle, |config| {
905            config.mode = AnimationMode::Tween(Tween::default());
906        });
907
908        let mode = AnimationMode::Tween(Tween::default());
909        let state = AnimationState::<f32>::new_running(mode, config_handle.clone());
910
911        let mut motion = Motion::new(0.0f32);
912        motion.target = 100.0f32;
913        motion.running = true;
914        motion.elapsed = Duration::from_secs(2); // Past default duration
915
916        let mut state = state;
917        let should_continue = state.update(1.0 / 60.0, &mut motion);
918
919        // Should complete
920        assert!(!should_continue);
921        assert!(matches!(state, AnimationState::Idle));
922        assert_eq!(motion.current, motion.target);
923
924        global::return_config(config_handle);
925    }
926
927    #[test]
928    fn test_animation_state_loop_infinite() {
929        let config_handle = global::get_config();
930        global::modify_config(&config_handle, |config| {
931            config.mode = AnimationMode::Tween(Tween::default());
932            config.loop_mode = Some(LoopMode::Infinite);
933        });
934
935        let mode = AnimationMode::Tween(Tween::default());
936        let state = AnimationState::<f32>::new_running(mode, config_handle.clone());
937
938        let mut motion = Motion::new(0.0f32);
939        motion.target = 100.0f32;
940        motion.running = true;
941        motion.elapsed = Duration::from_secs(2); // Past default duration
942
943        let mut state = state;
944        let should_continue = state.update(1.0 / 60.0, &mut motion);
945
946        // Should continue looping
947        assert!(should_continue);
948        assert!(matches!(state, AnimationState::Running { .. }));
949        // Should reset to initial position
950        assert_eq!(motion.current, motion.initial);
951        assert_eq!(motion.elapsed, Duration::default());
952
953        global::return_config(config_handle);
954    }
955
956    #[test]
957    fn test_animation_state_completion_callback() {
958        let callback_executed = Arc::new(Mutex::new(false));
959        let callback_executed_clone = callback_executed.clone();
960
961        let config_handle = global::get_config();
962        global::modify_config(&config_handle, |config| {
963            config.mode = AnimationMode::Tween(Tween::default());
964            config.on_complete = Some(Arc::new(Mutex::new(Box::new(move || {
965                *callback_executed_clone.lock().unwrap() = true;
966            }))));
967        });
968
969        let mode = AnimationMode::Tween(Tween::default());
970        let state = AnimationState::<f32>::new_running(mode, config_handle.clone());
971
972        let mut motion = Motion::new(0.0f32);
973        motion.target = 100.0f32;
974        motion.running = true;
975        motion.elapsed = Duration::from_secs(2); // Past default duration
976
977        let mut state = state;
978        let should_continue = state.update(1.0 / 60.0, &mut motion);
979
980        // Should complete and execute callback
981        assert!(!should_continue);
982        assert!(*callback_executed.lock().unwrap());
983
984        global::return_config(config_handle);
985    }
986
987    #[test]
988    fn test_animation_state_delay_handling() {
989        let config_handle = global::get_config();
990        global::modify_config(&config_handle, |config| {
991            config.mode = AnimationMode::Tween(Tween::default());
992            config.delay = Duration::from_millis(100);
993        });
994
995        let mode = AnimationMode::Tween(Tween::default());
996        let state = AnimationState::<f32>::new_running(mode, config_handle.clone());
997
998        let mut motion = Motion::new(0.0f32);
999        motion.target = 100.0f32;
1000        motion.running = true;
1001        motion.delay_elapsed = Duration::from_millis(50); // Still in delay
1002
1003        let mut state = state;
1004        let should_continue = state.update(1.0 / 60.0, &mut motion);
1005
1006        // Should continue but not animate yet (still in delay)
1007        assert!(should_continue);
1008        assert_eq!(motion.current, motion.initial); // Shouldn't have moved
1009
1010        global::return_config(config_handle);
1011    }
1012}