presentar_core/
animation.rs

1#![allow(clippy::unwrap_used, clippy::disallowed_methods)]
2//! Animation system with spring physics, easing, and keyframe support.
3//!
4//! Provides 60fps-capable animations for smooth UI transitions.
5
6use crate::geometry::Point;
7use std::collections::HashMap;
8
9// =============================================================================
10// Easing Functions - TESTS FIRST
11// =============================================================================
12
13/// Standard easing functions for animations.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum Easing {
16    /// Linear interpolation (no easing)
17    #[default]
18    Linear,
19    /// Ease in (slow start)
20    EaseIn,
21    /// Ease out (slow end)
22    EaseOut,
23    /// Ease in and out (slow start and end)
24    EaseInOut,
25    /// Cubic ease in
26    CubicIn,
27    /// Cubic ease out
28    CubicOut,
29    /// Cubic ease in and out
30    CubicInOut,
31    /// Exponential ease in
32    ExpoIn,
33    /// Exponential ease out
34    ExpoOut,
35    /// Elastic bounce at end
36    ElasticOut,
37    /// Bounce at end
38    BounceOut,
39    /// Back ease out (overshoots then returns)
40    BackOut,
41}
42
43impl Easing {
44    /// Apply easing function to a normalized time value (0.0 to 1.0).
45    #[must_use]
46    pub fn apply(self, t: f64) -> f64 {
47        let t = t.clamp(0.0, 1.0);
48        match self {
49            Self::Linear => t,
50            Self::EaseIn => Self::ease_in_quad(t),
51            Self::EaseOut => Self::ease_out_quad(t),
52            Self::EaseInOut => Self::ease_in_out_quad(t),
53            Self::CubicIn => Self::ease_in_cubic(t),
54            Self::CubicOut => Self::ease_out_cubic(t),
55            Self::CubicInOut => Self::ease_in_out_cubic(t),
56            Self::ExpoIn => Self::ease_in_expo(t),
57            Self::ExpoOut => Self::ease_out_expo(t),
58            Self::ElasticOut => Self::elastic_out(t),
59            Self::BounceOut => Self::bounce_out(t),
60            Self::BackOut => Self::back_out(t),
61        }
62    }
63
64    fn ease_in_quad(t: f64) -> f64 {
65        t * t
66    }
67
68    fn ease_out_quad(t: f64) -> f64 {
69        (1.0 - t).mul_add(-(1.0 - t), 1.0)
70    }
71
72    fn ease_in_out_quad(t: f64) -> f64 {
73        if t < 0.5 {
74            2.0 * t * t
75        } else {
76            1.0 - (-2.0f64).mul_add(t, 2.0).powi(2) / 2.0
77        }
78    }
79
80    fn ease_in_cubic(t: f64) -> f64 {
81        t * t * t
82    }
83
84    fn ease_out_cubic(t: f64) -> f64 {
85        1.0 - (1.0 - t).powi(3)
86    }
87
88    fn ease_in_out_cubic(t: f64) -> f64 {
89        if t < 0.5 {
90            4.0 * t * t * t
91        } else {
92            1.0 - (-2.0f64).mul_add(t, 2.0).powi(3) / 2.0
93        }
94    }
95
96    fn ease_in_expo(t: f64) -> f64 {
97        if t == 0.0 {
98            0.0
99        } else {
100            10.0f64.mul_add(t, -10.0).exp2()
101        }
102    }
103
104    fn ease_out_expo(t: f64) -> f64 {
105        if (t - 1.0).abs() < f64::EPSILON {
106            1.0
107        } else {
108            1.0 - (-10.0 * t).exp2()
109        }
110    }
111
112    fn elastic_out(t: f64) -> f64 {
113        if t == 0.0 || (t - 1.0).abs() < f64::EPSILON {
114            t
115        } else {
116            let c4 = (2.0 * std::f64::consts::PI) / 3.0;
117            (-10.0 * t)
118                .exp2()
119                .mul_add((t.mul_add(10.0, -0.75) * c4).sin(), 1.0)
120        }
121    }
122
123    fn bounce_out(t: f64) -> f64 {
124        const N1: f64 = 7.5625;
125        const D1: f64 = 2.75;
126
127        if t < 1.0 / D1 {
128            N1 * t * t
129        } else if t < 2.0 / D1 {
130            let t = t - 1.5 / D1;
131            (N1 * t).mul_add(t, 0.75)
132        } else if t < 2.5 / D1 {
133            let t = t - 2.25 / D1;
134            (N1 * t).mul_add(t, 0.9375)
135        } else {
136            let t = t - 2.625 / D1;
137            (N1 * t).mul_add(t, 0.984375)
138        }
139    }
140
141    fn back_out(t: f64) -> f64 {
142        const C1: f64 = 1.70158;
143        const C3: f64 = C1 + 1.0;
144        C1.mul_add((t - 1.0).powi(2), C3.mul_add((t - 1.0).powi(3), 1.0))
145    }
146}
147
148// =============================================================================
149// SpringConfig - Spring Physics Parameters
150// =============================================================================
151
152/// Spring physics configuration.
153#[derive(Debug, Clone, Copy, PartialEq)]
154pub struct SpringConfig {
155    /// Mass of the object (affects inertia)
156    pub mass: f64,
157    /// Stiffness of the spring (affects speed)
158    pub stiffness: f64,
159    /// Damping coefficient (affects bounciness)
160    pub damping: f64,
161}
162
163impl Default for SpringConfig {
164    fn default() -> Self {
165        Self::GENTLE
166    }
167}
168
169impl SpringConfig {
170    /// Gentle spring (slow, smooth)
171    pub const GENTLE: Self = Self {
172        mass: 1.0,
173        stiffness: 100.0,
174        damping: 15.0,
175    };
176
177    /// Wobbly spring (bouncy)
178    pub const WOBBLY: Self = Self {
179        mass: 1.0,
180        stiffness: 180.0,
181        damping: 12.0,
182    };
183
184    /// Stiff spring (fast, snappy)
185    pub const STIFF: Self = Self {
186        mass: 1.0,
187        stiffness: 400.0,
188        damping: 30.0,
189    };
190
191    /// Molasses spring (very slow)
192    pub const MOLASSES: Self = Self {
193        mass: 1.0,
194        stiffness: 50.0,
195        damping: 20.0,
196    };
197
198    /// Create custom spring config.
199    #[must_use]
200    pub const fn custom(mass: f64, stiffness: f64, damping: f64) -> Self {
201        Self {
202            mass,
203            stiffness,
204            damping,
205        }
206    }
207
208    /// Calculate damping ratio.
209    #[must_use]
210    pub fn damping_ratio(&self) -> f64 {
211        self.damping / (2.0 * (self.mass * self.stiffness).sqrt())
212    }
213
214    /// Whether spring is underdamped (will oscillate).
215    #[must_use]
216    pub fn is_underdamped(&self) -> bool {
217        self.damping_ratio() < 1.0
218    }
219
220    /// Whether spring is critically damped (fastest without oscillation).
221    #[must_use]
222    pub fn is_critically_damped(&self) -> bool {
223        (self.damping_ratio() - 1.0).abs() < 0.01
224    }
225
226    /// Whether spring is overdamped (slow, no oscillation).
227    #[must_use]
228    pub fn is_overdamped(&self) -> bool {
229        self.damping_ratio() > 1.0
230    }
231}
232
233// =============================================================================
234// Spring - Animated Spring Value
235// =============================================================================
236
237/// A spring-animated value.
238#[derive(Debug, Clone)]
239pub struct Spring {
240    /// Current value
241    pub value: f64,
242    /// Target value
243    pub target: f64,
244    /// Current velocity
245    pub velocity: f64,
246    /// Spring configuration
247    pub config: SpringConfig,
248    /// Whether animation is complete
249    pub at_rest: bool,
250    /// Precision threshold for settling
251    pub precision: f64,
252}
253
254impl Spring {
255    /// Create a new spring at an initial value.
256    #[must_use]
257    pub fn new(initial: f64) -> Self {
258        Self {
259            value: initial,
260            target: initial,
261            velocity: 0.0,
262            config: SpringConfig::default(),
263            at_rest: true,
264            precision: 0.001,
265        }
266    }
267
268    /// Set spring configuration.
269    #[must_use]
270    pub fn with_config(mut self, config: SpringConfig) -> Self {
271        self.config = config;
272        self
273    }
274
275    /// Set target value.
276    pub fn set_target(&mut self, target: f64) {
277        if (self.target - target).abs() > f64::EPSILON {
278            self.target = target;
279            self.at_rest = false;
280        }
281    }
282
283    /// Update spring physics for a time step (dt in seconds).
284    pub fn update(&mut self, dt: f64) {
285        if self.at_rest {
286            return;
287        }
288
289        // Spring force: F = -k * x
290        let displacement = self.value - self.target;
291        let spring_force = -self.config.stiffness * displacement;
292
293        // Damping force: F = -c * v
294        let damping_force = -self.config.damping * self.velocity;
295
296        // Total acceleration: a = F / m
297        let acceleration = (spring_force + damping_force) / self.config.mass;
298
299        // Verlet integration
300        self.velocity += acceleration * dt;
301        self.value += self.velocity * dt;
302
303        // Check if at rest
304        if displacement.abs() < self.precision && self.velocity.abs() < self.precision {
305            self.value = self.target;
306            self.velocity = 0.0;
307            self.at_rest = true;
308        }
309    }
310
311    /// Immediately set value without animation.
312    pub fn set_immediate(&mut self, value: f64) {
313        self.value = value;
314        self.target = value;
315        self.velocity = 0.0;
316        self.at_rest = true;
317    }
318}
319
320// =============================================================================
321// AnimatedValue - Generic Animated Value
322// =============================================================================
323
324/// An animated value with easing or spring physics.
325#[derive(Debug, Clone)]
326pub enum AnimatedValue {
327    /// Easing-based animation
328    Eased(EasedValue),
329    /// Spring physics animation
330    Spring(Spring),
331}
332
333impl AnimatedValue {
334    /// Get current value.
335    #[must_use]
336    pub fn value(&self) -> f64 {
337        match self {
338            Self::Eased(e) => e.value(),
339            Self::Spring(s) => s.value,
340        }
341    }
342
343    /// Whether animation is complete.
344    #[must_use]
345    pub fn is_complete(&self) -> bool {
346        match self {
347            Self::Eased(e) => e.is_complete(),
348            Self::Spring(s) => s.at_rest,
349        }
350    }
351
352    /// Update animation for a time step.
353    pub fn update(&mut self, dt: f64) {
354        match self {
355            Self::Eased(e) => e.update(dt),
356            Self::Spring(s) => s.update(dt),
357        }
358    }
359}
360
361/// An easing-based animated value.
362#[derive(Debug, Clone)]
363pub struct EasedValue {
364    /// Start value
365    pub from: f64,
366    /// End value
367    pub to: f64,
368    /// Total duration in seconds
369    pub duration: f64,
370    /// Elapsed time
371    pub elapsed: f64,
372    /// Easing function
373    pub easing: Easing,
374}
375
376impl EasedValue {
377    /// Create new eased animation.
378    #[must_use]
379    pub fn new(from: f64, to: f64, duration: f64) -> Self {
380        Self {
381            from,
382            to,
383            duration,
384            elapsed: 0.0,
385            easing: Easing::EaseInOut,
386        }
387    }
388
389    /// Set easing function.
390    #[must_use]
391    pub fn with_easing(mut self, easing: Easing) -> Self {
392        self.easing = easing;
393        self
394    }
395
396    /// Get current value.
397    #[must_use]
398    pub fn value(&self) -> f64 {
399        let t = if self.duration > 0.0 {
400            (self.elapsed / self.duration).clamp(0.0, 1.0)
401        } else {
402            1.0
403        };
404        let eased = self.easing.apply(t);
405        (self.to - self.from).mul_add(eased, self.from)
406    }
407
408    /// Whether animation is complete.
409    #[must_use]
410    pub fn is_complete(&self) -> bool {
411        self.elapsed >= self.duration
412    }
413
414    /// Update animation.
415    pub fn update(&mut self, dt: f64) {
416        self.elapsed = (self.elapsed + dt).min(self.duration);
417    }
418
419    /// Progress from 0.0 to 1.0.
420    #[must_use]
421    pub fn progress(&self) -> f64 {
422        if self.duration > 0.0 {
423            (self.elapsed / self.duration).clamp(0.0, 1.0)
424        } else {
425            1.0
426        }
427    }
428}
429
430// =============================================================================
431// Keyframe - Keyframe Animation Support
432// =============================================================================
433
434/// A keyframe in an animation.
435#[derive(Debug, Clone)]
436pub struct Keyframe<T: Clone> {
437    /// Time of this keyframe (0.0 to 1.0 normalized)
438    pub time: f64,
439    /// Value at this keyframe
440    pub value: T,
441    /// Easing to next keyframe
442    pub easing: Easing,
443}
444
445impl<T: Clone> Keyframe<T> {
446    /// Create new keyframe.
447    #[must_use]
448    pub fn new(time: f64, value: T) -> Self {
449        Self {
450            time: time.clamp(0.0, 1.0),
451            value,
452            easing: Easing::Linear,
453        }
454    }
455
456    /// Set easing to next keyframe.
457    #[must_use]
458    pub fn with_easing(mut self, easing: Easing) -> Self {
459        self.easing = easing;
460        self
461    }
462}
463
464/// Keyframe animation track.
465#[derive(Debug, Clone)]
466pub struct KeyframeTrack<T: Clone + Interpolate> {
467    /// Keyframes sorted by time
468    keyframes: Vec<Keyframe<T>>,
469    /// Total duration in seconds
470    pub duration: f64,
471    /// Current elapsed time
472    pub elapsed: f64,
473    /// Whether to loop
474    pub looping: bool,
475}
476
477impl<T: Clone + Interpolate> KeyframeTrack<T> {
478    /// Create new keyframe track.
479    #[must_use]
480    pub fn new(duration: f64) -> Self {
481        Self {
482            keyframes: Vec::new(),
483            duration,
484            elapsed: 0.0,
485            looping: false,
486        }
487    }
488
489    /// Add a keyframe.
490    pub fn add_keyframe(&mut self, keyframe: Keyframe<T>) {
491        self.keyframes.push(keyframe);
492        self.keyframes.sort_by(|a, b| {
493            a.time
494                .partial_cmp(&b.time)
495                .expect("keyframe times must be comparable")
496        });
497    }
498
499    /// Set looping.
500    #[must_use]
501    pub fn with_loop(mut self, looping: bool) -> Self {
502        self.looping = looping;
503        self
504    }
505
506    /// Get value at current time.
507    #[must_use]
508    pub fn value(&self) -> Option<T> {
509        if self.keyframes.is_empty() {
510            return None;
511        }
512
513        let t = if self.duration > 0.0 {
514            let raw = self.elapsed / self.duration;
515            if self.looping {
516                raw % 1.0
517            } else {
518                raw.clamp(0.0, 1.0)
519            }
520        } else {
521            1.0
522        };
523
524        // Find surrounding keyframes
525        let mut prev_idx = 0;
526        let mut next_idx = 0;
527
528        for (i, kf) in self.keyframes.iter().enumerate() {
529            if kf.time <= t {
530                prev_idx = i;
531            }
532            if kf.time >= t {
533                next_idx = i;
534                break;
535            }
536            next_idx = i;
537        }
538
539        let prev = &self.keyframes[prev_idx];
540        let next = &self.keyframes[next_idx];
541
542        if prev_idx == next_idx {
543            return Some(prev.value.clone());
544        }
545
546        // Interpolate between keyframes
547        let segment_duration = next.time - prev.time;
548        let segment_t = if segment_duration > 0.0 {
549            (t - prev.time) / segment_duration
550        } else {
551            1.0
552        };
553
554        let eased_t = prev.easing.apply(segment_t);
555        Some(T::interpolate(&prev.value, &next.value, eased_t))
556    }
557
558    /// Update animation.
559    pub fn update(&mut self, dt: f64) {
560        self.elapsed += dt;
561        if !self.looping && self.elapsed > self.duration {
562            self.elapsed = self.duration;
563        }
564    }
565
566    /// Whether animation is complete.
567    #[must_use]
568    pub fn is_complete(&self) -> bool {
569        !self.looping && self.elapsed >= self.duration
570    }
571
572    /// Reset to start.
573    pub fn reset(&mut self) {
574        self.elapsed = 0.0;
575    }
576}
577
578// =============================================================================
579// Interpolate Trait
580// =============================================================================
581
582/// Trait for types that can be interpolated.
583pub trait Interpolate {
584    /// Interpolate between two values.
585    fn interpolate(from: &Self, to: &Self, t: f64) -> Self;
586}
587
588impl Interpolate for f64 {
589    fn interpolate(from: &Self, to: &Self, t: f64) -> Self {
590        from + (to - from) * t
591    }
592}
593
594impl Interpolate for f32 {
595    fn interpolate(from: &Self, to: &Self, t: f64) -> Self {
596        (*to - *from).mul_add(t as Self, *from)
597    }
598}
599
600impl Interpolate for Point {
601    fn interpolate(from: &Self, to: &Self, t: f64) -> Self {
602        Self {
603            x: f32::interpolate(&from.x, &to.x, t),
604            y: f32::interpolate(&from.y, &to.y, t),
605        }
606    }
607}
608
609/// Color for animation (RGBA as f32 0-1).
610#[derive(Debug, Clone, Copy, PartialEq)]
611pub struct AnimColor {
612    pub r: f32,
613    pub g: f32,
614    pub b: f32,
615    pub a: f32,
616}
617
618impl AnimColor {
619    pub const WHITE: Self = Self {
620        r: 1.0,
621        g: 1.0,
622        b: 1.0,
623        a: 1.0,
624    };
625    pub const BLACK: Self = Self {
626        r: 0.0,
627        g: 0.0,
628        b: 0.0,
629        a: 1.0,
630    };
631    pub const TRANSPARENT: Self = Self {
632        r: 0.0,
633        g: 0.0,
634        b: 0.0,
635        a: 0.0,
636    };
637
638    #[must_use]
639    pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
640        Self { r, g, b, a }
641    }
642}
643
644impl Interpolate for AnimColor {
645    fn interpolate(from: &Self, to: &Self, t: f64) -> Self {
646        let t = t as f32;
647        Self {
648            r: (to.r - from.r).mul_add(t, from.r),
649            g: (to.g - from.g).mul_add(t, from.g),
650            b: (to.b - from.b).mul_add(t, from.b),
651            a: (to.a - from.a).mul_add(t, from.a),
652        }
653    }
654}
655
656// =============================================================================
657// AnimationController - Manages Multiple Animations
658// =============================================================================
659
660/// Controller for managing multiple animations.
661#[derive(Debug, Default)]
662pub struct AnimationController {
663    /// Named springs
664    springs: HashMap<String, Spring>,
665    /// Named eased values
666    eased: HashMap<String, EasedValue>,
667    /// Active animation count
668    active_count: usize,
669}
670
671impl AnimationController {
672    /// Create new controller.
673    #[must_use]
674    pub fn new() -> Self {
675        Self::default()
676    }
677
678    /// Add a spring animation.
679    pub fn add_spring(&mut self, name: &str, initial: f64, config: SpringConfig) {
680        let spring = Spring::new(initial).with_config(config);
681        self.springs.insert(name.to_string(), spring);
682    }
683
684    /// Add an eased animation.
685    pub fn add_eased(&mut self, name: &str, from: f64, to: f64, duration: f64, easing: Easing) {
686        let eased = EasedValue::new(from, to, duration).with_easing(easing);
687        self.eased.insert(name.to_string(), eased);
688    }
689
690    /// Set spring target.
691    pub fn set_target(&mut self, name: &str, target: f64) {
692        if let Some(spring) = self.springs.get_mut(name) {
693            spring.set_target(target);
694        }
695    }
696
697    /// Get current value.
698    #[must_use]
699    pub fn get(&self, name: &str) -> Option<f64> {
700        if let Some(spring) = self.springs.get(name) {
701            return Some(spring.value);
702        }
703        if let Some(eased) = self.eased.get(name) {
704            return Some(eased.value());
705        }
706        None
707    }
708
709    /// Update all animations.
710    pub fn update(&mut self, dt: f64) {
711        self.active_count = 0;
712
713        for spring in self.springs.values_mut() {
714            spring.update(dt);
715            if !spring.at_rest {
716                self.active_count += 1;
717            }
718        }
719
720        for eased in self.eased.values_mut() {
721            eased.update(dt);
722            if !eased.is_complete() {
723                self.active_count += 1;
724            }
725        }
726    }
727
728    /// Whether any animations are active.
729    #[must_use]
730    pub fn is_animating(&self) -> bool {
731        self.active_count > 0
732    }
733
734    /// Number of active animations.
735    #[must_use]
736    pub fn active_count(&self) -> usize {
737        self.active_count
738    }
739
740    /// Remove an animation.
741    pub fn remove(&mut self, name: &str) {
742        self.springs.remove(name);
743        self.eased.remove(name);
744    }
745
746    /// Clear all animations.
747    pub fn clear(&mut self) {
748        self.springs.clear();
749        self.eased.clear();
750        self.active_count = 0;
751    }
752}
753
754// =============================================================================
755// Tests - TDD Style
756// =============================================================================
757
758#[cfg(test)]
759mod tests {
760    use super::*;
761
762    // -------------------------------------------------------------------------
763    // Easing tests
764    // -------------------------------------------------------------------------
765
766    #[test]
767    fn test_easing_linear() {
768        assert!((Easing::Linear.apply(0.0) - 0.0).abs() < 0.001);
769        assert!((Easing::Linear.apply(0.5) - 0.5).abs() < 0.001);
770        assert!((Easing::Linear.apply(1.0) - 1.0).abs() < 0.001);
771    }
772
773    #[test]
774    fn test_easing_clamps_input() {
775        assert!((Easing::Linear.apply(-0.5) - 0.0).abs() < 0.001);
776        assert!((Easing::Linear.apply(1.5) - 1.0).abs() < 0.001);
777    }
778
779    #[test]
780    fn test_easing_ease_in() {
781        let val = Easing::EaseIn.apply(0.5);
782        assert!(val < 0.5); // Should be below linear at midpoint
783    }
784
785    #[test]
786    fn test_easing_ease_out() {
787        let val = Easing::EaseOut.apply(0.5);
788        assert!(val > 0.5); // Should be above linear at midpoint
789    }
790
791    #[test]
792    fn test_easing_ease_in_out() {
793        let val = Easing::EaseInOut.apply(0.5);
794        assert!((val - 0.5).abs() < 0.01); // Should be near 0.5 at midpoint
795    }
796
797    #[test]
798    fn test_easing_cubic() {
799        assert!((Easing::CubicIn.apply(0.0) - 0.0).abs() < 0.001);
800        assert!((Easing::CubicOut.apply(1.0) - 1.0).abs() < 0.001);
801    }
802
803    #[test]
804    fn test_easing_expo() {
805        assert!((Easing::ExpoIn.apply(0.0) - 0.0).abs() < 0.001);
806        assert!((Easing::ExpoOut.apply(1.0) - 1.0).abs() < 0.001);
807    }
808
809    #[test]
810    fn test_easing_elastic() {
811        let val = Easing::ElasticOut.apply(1.0);
812        assert!((val - 1.0).abs() < 0.001);
813    }
814
815    #[test]
816    fn test_easing_bounce() {
817        let val = Easing::BounceOut.apply(1.0);
818        assert!((val - 1.0).abs() < 0.001);
819    }
820
821    #[test]
822    fn test_easing_back() {
823        let val = Easing::BackOut.apply(1.0);
824        assert!((val - 1.0).abs() < 0.001);
825    }
826
827    // -------------------------------------------------------------------------
828    // SpringConfig tests
829    // -------------------------------------------------------------------------
830
831    #[test]
832    fn test_spring_config_presets() {
833        assert!(SpringConfig::GENTLE.stiffness < SpringConfig::STIFF.stiffness);
834        assert!(SpringConfig::WOBBLY.damping < SpringConfig::STIFF.damping);
835    }
836
837    #[test]
838    fn test_spring_config_damping_ratio() {
839        let config = SpringConfig::GENTLE;
840        let ratio = config.damping_ratio();
841        assert!(ratio > 0.0);
842    }
843
844    #[test]
845    fn test_spring_config_damping_types() {
846        // Underdamped (bouncy)
847        let underdamped = SpringConfig::custom(1.0, 100.0, 5.0);
848        assert!(underdamped.is_underdamped());
849
850        // Overdamped (slow)
851        let overdamped = SpringConfig::custom(1.0, 100.0, 50.0);
852        assert!(overdamped.is_overdamped());
853    }
854
855    // -------------------------------------------------------------------------
856    // Spring tests
857    // -------------------------------------------------------------------------
858
859    #[test]
860    fn test_spring_new() {
861        let spring = Spring::new(10.0);
862        assert!((spring.value - 10.0).abs() < 0.001);
863        assert!((spring.target - 10.0).abs() < 0.001);
864        assert!(spring.at_rest);
865    }
866
867    #[test]
868    fn test_spring_set_target() {
869        let mut spring = Spring::new(0.0);
870        spring.set_target(100.0);
871        assert!(!spring.at_rest);
872        assert!((spring.target - 100.0).abs() < 0.001);
873    }
874
875    #[test]
876    fn test_spring_update() {
877        let mut spring = Spring::new(0.0);
878        spring.set_target(100.0);
879
880        // Simulate multiple frames
881        for _ in 0..100 {
882            spring.update(1.0 / 60.0); // 60fps
883        }
884
885        // Should be near target
886        assert!((spring.value - 100.0).abs() < 1.0);
887    }
888
889    #[test]
890    fn test_spring_converges() {
891        let mut spring = Spring::new(0.0);
892        spring.set_target(100.0);
893
894        // Simulate until at rest
895        for _ in 0..1000 {
896            if spring.at_rest {
897                break;
898            }
899            spring.update(1.0 / 60.0);
900        }
901
902        assert!(spring.at_rest);
903        assert!((spring.value - 100.0).abs() < 0.01);
904    }
905
906    #[test]
907    fn test_spring_set_immediate() {
908        let mut spring = Spring::new(0.0);
909        spring.set_target(100.0);
910        spring.update(1.0 / 60.0);
911
912        spring.set_immediate(50.0);
913        assert!(spring.at_rest);
914        assert!((spring.value - 50.0).abs() < 0.001);
915    }
916
917    #[test]
918    fn test_spring_no_update_when_at_rest() {
919        let mut spring = Spring::new(100.0);
920        let initial_value = spring.value;
921        spring.update(1.0 / 60.0);
922        assert!((spring.value - initial_value).abs() < 0.001);
923    }
924
925    // -------------------------------------------------------------------------
926    // EasedValue tests
927    // -------------------------------------------------------------------------
928
929    #[test]
930    fn test_eased_value_new() {
931        let eased = EasedValue::new(0.0, 100.0, 1.0);
932        assert!((eased.value() - 0.0).abs() < 0.001);
933        assert!(!eased.is_complete());
934    }
935
936    #[test]
937    fn test_eased_value_update() {
938        let mut eased = EasedValue::new(0.0, 100.0, 1.0);
939        eased.update(0.5);
940        assert!(eased.value() > 0.0);
941        assert!(eased.value() < 100.0);
942    }
943
944    #[test]
945    fn test_eased_value_complete() {
946        let mut eased = EasedValue::new(0.0, 100.0, 1.0);
947        eased.update(2.0); // Past duration
948        assert!(eased.is_complete());
949        assert!((eased.value() - 100.0).abs() < 0.001);
950    }
951
952    #[test]
953    fn test_eased_value_progress() {
954        let mut eased = EasedValue::new(0.0, 100.0, 1.0);
955        assert!((eased.progress() - 0.0).abs() < 0.001);
956        eased.update(0.5);
957        assert!((eased.progress() - 0.5).abs() < 0.001);
958    }
959
960    #[test]
961    fn test_eased_value_with_easing() {
962        let eased = EasedValue::new(0.0, 100.0, 1.0).with_easing(Easing::CubicOut);
963        assert_eq!(eased.easing, Easing::CubicOut);
964    }
965
966    // -------------------------------------------------------------------------
967    // AnimatedValue tests
968    // -------------------------------------------------------------------------
969
970    #[test]
971    fn test_animated_value_eased() {
972        let mut anim = AnimatedValue::Eased(EasedValue::new(0.0, 100.0, 1.0));
973        assert!((anim.value() - 0.0).abs() < 0.001);
974        anim.update(1.0);
975        assert!(anim.is_complete());
976    }
977
978    #[test]
979    fn test_animated_value_spring() {
980        let mut anim = AnimatedValue::Spring(Spring::new(0.0));
981        if let AnimatedValue::Spring(ref mut s) = anim {
982            s.set_target(100.0);
983        }
984        assert!(!anim.is_complete());
985    }
986
987    // -------------------------------------------------------------------------
988    // Keyframe tests
989    // -------------------------------------------------------------------------
990
991    #[test]
992    fn test_keyframe_new() {
993        let kf: Keyframe<f64> = Keyframe::new(0.5, 50.0);
994        assert!((kf.time - 0.5).abs() < 0.001);
995        assert!((kf.value - 50.0).abs() < 0.001);
996    }
997
998    #[test]
999    fn test_keyframe_clamps_time() {
1000        let kf: Keyframe<f64> = Keyframe::new(1.5, 50.0);
1001        assert!((kf.time - 1.0).abs() < 0.001);
1002    }
1003
1004    #[test]
1005    fn test_keyframe_track_new() {
1006        let track: KeyframeTrack<f64> = KeyframeTrack::new(2.0);
1007        assert!((track.duration - 2.0).abs() < 0.001);
1008        assert!(track.value().is_none());
1009    }
1010
1011    #[test]
1012    fn test_keyframe_track_single_keyframe() {
1013        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1014        track.add_keyframe(Keyframe::new(0.0, 100.0));
1015        assert!((track.value().unwrap() - 100.0).abs() < 0.001);
1016    }
1017
1018    #[test]
1019    fn test_keyframe_track_interpolation() {
1020        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1021        track.add_keyframe(Keyframe::new(0.0, 0.0));
1022        track.add_keyframe(Keyframe::new(1.0, 100.0));
1023
1024        track.update(0.5);
1025        let val = track.value().unwrap();
1026        assert!(val > 40.0 && val < 60.0); // Should be near 50
1027    }
1028
1029    #[test]
1030    fn test_keyframe_track_looping() {
1031        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0).with_loop(true);
1032        track.add_keyframe(Keyframe::new(0.0, 0.0));
1033        track.add_keyframe(Keyframe::new(1.0, 100.0));
1034
1035        track.update(1.5);
1036        assert!(!track.is_complete());
1037    }
1038
1039    #[test]
1040    fn test_keyframe_track_reset() {
1041        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1042        track.add_keyframe(Keyframe::new(0.0, 0.0));
1043        track.update(0.5);
1044        track.reset();
1045        assert!((track.elapsed - 0.0).abs() < 0.001);
1046    }
1047
1048    // -------------------------------------------------------------------------
1049    // Interpolate tests
1050    // -------------------------------------------------------------------------
1051
1052    #[test]
1053    fn test_interpolate_f64() {
1054        let result = f64::interpolate(&0.0, &100.0, 0.5);
1055        assert!((result - 50.0).abs() < 0.001);
1056    }
1057
1058    #[test]
1059    fn test_interpolate_f32() {
1060        let result = f32::interpolate(&0.0, &100.0, 0.5);
1061        assert!((result - 50.0).abs() < 0.001);
1062    }
1063
1064    #[test]
1065    fn test_interpolate_point() {
1066        let from = Point { x: 0.0, y: 0.0 };
1067        let to = Point { x: 100.0, y: 100.0 };
1068        let result = Point::interpolate(&from, &to, 0.5);
1069        assert!((result.x - 50.0).abs() < 0.001);
1070        assert!((result.y - 50.0).abs() < 0.001);
1071    }
1072
1073    #[test]
1074    fn test_interpolate_color() {
1075        let result = AnimColor::interpolate(&AnimColor::BLACK, &AnimColor::WHITE, 0.5);
1076        assert!((result.r - 0.5).abs() < 0.001);
1077        assert!((result.g - 0.5).abs() < 0.001);
1078        assert!((result.b - 0.5).abs() < 0.001);
1079    }
1080
1081    // -------------------------------------------------------------------------
1082    // AnimationController tests
1083    // -------------------------------------------------------------------------
1084
1085    #[test]
1086    fn test_controller_new() {
1087        let controller = AnimationController::new();
1088        assert!(!controller.is_animating());
1089        assert_eq!(controller.active_count(), 0);
1090    }
1091
1092    #[test]
1093    fn test_controller_add_spring() {
1094        let mut controller = AnimationController::new();
1095        controller.add_spring("x", 0.0, SpringConfig::GENTLE);
1096        assert!((controller.get("x").unwrap() - 0.0).abs() < 0.001);
1097    }
1098
1099    #[test]
1100    fn test_controller_add_eased() {
1101        let mut controller = AnimationController::new();
1102        controller.add_eased("opacity", 0.0, 1.0, 0.3, Easing::EaseOut);
1103        assert!((controller.get("opacity").unwrap() - 0.0).abs() < 0.001);
1104    }
1105
1106    #[test]
1107    fn test_controller_set_target() {
1108        let mut controller = AnimationController::new();
1109        controller.add_spring("x", 0.0, SpringConfig::STIFF);
1110        controller.set_target("x", 100.0);
1111        controller.update(1.0 / 60.0);
1112        assert!(controller.is_animating());
1113    }
1114
1115    #[test]
1116    fn test_controller_update() {
1117        let mut controller = AnimationController::new();
1118        controller.add_eased("fade", 0.0, 1.0, 0.5, Easing::Linear);
1119        controller.update(0.25);
1120        let val = controller.get("fade").unwrap();
1121        assert!(val > 0.4 && val < 0.6);
1122    }
1123
1124    #[test]
1125    fn test_controller_remove() {
1126        let mut controller = AnimationController::new();
1127        controller.add_spring("x", 0.0, SpringConfig::GENTLE);
1128        controller.remove("x");
1129        assert!(controller.get("x").is_none());
1130    }
1131
1132    #[test]
1133    fn test_controller_clear() {
1134        let mut controller = AnimationController::new();
1135        controller.add_spring("x", 0.0, SpringConfig::GENTLE);
1136        controller.add_spring("y", 0.0, SpringConfig::GENTLE);
1137        controller.clear();
1138        assert!(controller.get("x").is_none());
1139        assert!(controller.get("y").is_none());
1140    }
1141
1142    #[test]
1143    fn test_controller_get_nonexistent() {
1144        let controller = AnimationController::new();
1145        assert!(controller.get("nonexistent").is_none());
1146    }
1147
1148    #[test]
1149    fn test_controller_active_count() {
1150        let mut controller = AnimationController::new();
1151        controller.add_spring("a", 0.0, SpringConfig::GENTLE);
1152        controller.add_spring("b", 0.0, SpringConfig::GENTLE);
1153        controller.set_target("a", 100.0);
1154        controller.set_target("b", 100.0);
1155        controller.update(1.0 / 60.0);
1156        assert_eq!(controller.active_count(), 2);
1157    }
1158
1159    // =========================================================================
1160    // Easing Additional Tests
1161    // =========================================================================
1162
1163    #[test]
1164    fn test_easing_default() {
1165        assert_eq!(Easing::default(), Easing::Linear);
1166    }
1167
1168    #[test]
1169    fn test_easing_all_variants_at_zero() {
1170        let easings = [
1171            Easing::Linear,
1172            Easing::EaseIn,
1173            Easing::EaseOut,
1174            Easing::EaseInOut,
1175            Easing::CubicIn,
1176            Easing::CubicOut,
1177            Easing::CubicInOut,
1178            Easing::ExpoIn,
1179            Easing::ExpoOut,
1180            Easing::ElasticOut,
1181            Easing::BounceOut,
1182            Easing::BackOut,
1183        ];
1184        for easing in easings {
1185            let val = easing.apply(0.0);
1186            assert!(val.abs() < 0.01, "{:?} at 0.0 = {}", easing, val);
1187        }
1188    }
1189
1190    #[test]
1191    fn test_easing_all_variants_at_one() {
1192        let easings = [
1193            Easing::Linear,
1194            Easing::EaseIn,
1195            Easing::EaseOut,
1196            Easing::EaseInOut,
1197            Easing::CubicIn,
1198            Easing::CubicOut,
1199            Easing::CubicInOut,
1200            Easing::ExpoIn,
1201            Easing::ExpoOut,
1202            Easing::ElasticOut,
1203            Easing::BounceOut,
1204            Easing::BackOut,
1205        ];
1206        for easing in easings {
1207            let val = easing.apply(1.0);
1208            assert!((val - 1.0).abs() < 0.01, "{:?} at 1.0 = {}", easing, val);
1209        }
1210    }
1211
1212    #[test]
1213    fn test_easing_cubic_in_out_midpoint() {
1214        let val = Easing::CubicInOut.apply(0.5);
1215        assert!((val - 0.5).abs() < 0.01);
1216    }
1217
1218    #[test]
1219    fn test_easing_expo_in_zero() {
1220        // ExpoIn has special case for 0
1221        let val = Easing::ExpoIn.apply(0.0);
1222        assert!((val - 0.0).abs() < 0.001);
1223    }
1224
1225    #[test]
1226    fn test_easing_expo_out_one() {
1227        // ExpoOut has special case for 1
1228        let val = Easing::ExpoOut.apply(1.0);
1229        assert!((val - 1.0).abs() < 0.001);
1230    }
1231
1232    #[test]
1233    fn test_easing_elastic_out_zero() {
1234        let val = Easing::ElasticOut.apply(0.0);
1235        assert!((val - 0.0).abs() < 0.001);
1236    }
1237
1238    #[test]
1239    fn test_easing_bounce_out_segments() {
1240        // Test all segments of bounce
1241        assert!(Easing::BounceOut.apply(0.1) < 0.3);
1242        assert!(Easing::BounceOut.apply(0.5) > 0.5);
1243        assert!(Easing::BounceOut.apply(0.8) > 0.9);
1244        assert!(Easing::BounceOut.apply(0.95) > 0.98);
1245    }
1246
1247    #[test]
1248    fn test_easing_back_out_overshoots() {
1249        // BackOut should overshoot slightly past 1.0 before settling
1250        let val_mid = Easing::BackOut.apply(0.5);
1251        assert!(val_mid > 0.5); // Should be ahead of linear
1252    }
1253
1254    #[test]
1255    fn test_easing_clone() {
1256        let e = Easing::CubicOut;
1257        let cloned = e;
1258        assert_eq!(e, cloned);
1259    }
1260
1261    #[test]
1262    fn test_easing_debug() {
1263        let e = Easing::ElasticOut;
1264        let debug = format!("{:?}", e);
1265        assert!(debug.contains("ElasticOut"));
1266    }
1267
1268    // =========================================================================
1269    // SpringConfig Additional Tests
1270    // =========================================================================
1271
1272    #[test]
1273    fn test_spring_config_default() {
1274        let config = SpringConfig::default();
1275        assert_eq!(config, SpringConfig::GENTLE);
1276    }
1277
1278    #[test]
1279    fn test_spring_config_custom() {
1280        let config = SpringConfig::custom(2.0, 200.0, 20.0);
1281        assert!((config.mass - 2.0).abs() < 0.001);
1282        assert!((config.stiffness - 200.0).abs() < 0.001);
1283        assert!((config.damping - 20.0).abs() < 0.001);
1284    }
1285
1286    #[test]
1287    fn test_spring_config_molasses() {
1288        let config = SpringConfig::MOLASSES;
1289        assert!(config.stiffness < SpringConfig::GENTLE.stiffness);
1290    }
1291
1292    #[test]
1293    fn test_spring_config_critically_damped() {
1294        // Critical damping = 2 * sqrt(m * k)
1295        // For m=1, k=100: critical = 2 * 10 = 20
1296        let config = SpringConfig::custom(1.0, 100.0, 20.0);
1297        assert!(config.is_critically_damped());
1298    }
1299
1300    #[test]
1301    fn test_spring_config_all_presets_valid() {
1302        let presets = [
1303            SpringConfig::GENTLE,
1304            SpringConfig::WOBBLY,
1305            SpringConfig::STIFF,
1306            SpringConfig::MOLASSES,
1307        ];
1308        for config in presets {
1309            assert!(config.mass > 0.0);
1310            assert!(config.stiffness > 0.0);
1311            assert!(config.damping > 0.0);
1312        }
1313    }
1314
1315    #[test]
1316    fn test_spring_config_clone() {
1317        let config = SpringConfig::STIFF;
1318        let cloned = config;
1319        assert_eq!(config, cloned);
1320    }
1321
1322    #[test]
1323    fn test_spring_config_debug() {
1324        let config = SpringConfig::WOBBLY;
1325        let debug = format!("{:?}", config);
1326        assert!(debug.contains("SpringConfig"));
1327    }
1328
1329    // =========================================================================
1330    // Spring Additional Tests
1331    // =========================================================================
1332
1333    #[test]
1334    fn test_spring_with_config() {
1335        let spring = Spring::new(0.0).with_config(SpringConfig::STIFF);
1336        assert_eq!(spring.config, SpringConfig::STIFF);
1337    }
1338
1339    #[test]
1340    fn test_spring_set_target_same_value() {
1341        let mut spring = Spring::new(100.0);
1342        spring.set_target(100.0); // Same as initial
1343        assert!(spring.at_rest); // Should remain at rest
1344    }
1345
1346    #[test]
1347    fn test_spring_update_small_dt() {
1348        let mut spring = Spring::new(0.0);
1349        spring.set_target(100.0);
1350        spring.update(0.001); // Very small time step
1351        assert!(spring.value > 0.0);
1352    }
1353
1354    #[test]
1355    fn test_spring_precision_threshold() {
1356        let mut spring = Spring::new(0.0);
1357        spring.precision = 0.1; // Larger precision
1358        spring.set_target(0.05); // Within precision
1359        spring.update(0.016);
1360        // Should settle quickly with larger precision
1361    }
1362
1363    #[test]
1364    fn test_spring_negative_values() {
1365        let mut spring = Spring::new(0.0);
1366        spring.set_target(-100.0);
1367        for _ in 0..200 {
1368            spring.update(1.0 / 60.0);
1369        }
1370        assert!((spring.value - (-100.0)).abs() < 1.0);
1371    }
1372
1373    #[test]
1374    fn test_spring_clone() {
1375        let spring = Spring::new(50.0);
1376        let cloned = spring.clone();
1377        assert!((cloned.value - 50.0).abs() < 0.001);
1378    }
1379
1380    #[test]
1381    fn test_spring_debug() {
1382        let spring = Spring::new(0.0);
1383        let debug = format!("{:?}", spring);
1384        assert!(debug.contains("Spring"));
1385    }
1386
1387    // =========================================================================
1388    // EasedValue Additional Tests
1389    // =========================================================================
1390
1391    #[test]
1392    fn test_eased_value_zero_duration() {
1393        let eased = EasedValue::new(0.0, 100.0, 0.0);
1394        assert!((eased.value() - 100.0).abs() < 0.001); // Instant
1395        assert!(eased.is_complete());
1396    }
1397
1398    #[test]
1399    fn test_eased_value_negative_update() {
1400        let mut eased = EasedValue::new(0.0, 100.0, 1.0);
1401        eased.update(0.5);
1402        eased.update(-0.2); // Negative dt (shouldn't happen but handle gracefully)
1403                            // elapsed should not exceed duration and value should stay in valid range
1404        assert!(eased.elapsed <= eased.duration);
1405        // value() clamps progress to [0, 1], so value is always in [from, to] range
1406        assert!(eased.value() >= 0.0 && eased.value() <= 100.0);
1407        // elapsed should be 0.3 after -0.2 from 0.5
1408        assert!((eased.elapsed - 0.3).abs() < 0.001);
1409    }
1410
1411    #[test]
1412    fn test_eased_value_progress_zero_duration() {
1413        let eased = EasedValue::new(0.0, 100.0, 0.0);
1414        assert!((eased.progress() - 1.0).abs() < 0.001);
1415    }
1416
1417    #[test]
1418    fn test_eased_value_linear_interpolation() {
1419        let mut eased = EasedValue::new(0.0, 100.0, 1.0).with_easing(Easing::Linear);
1420        eased.update(0.5);
1421        assert!((eased.value() - 50.0).abs() < 0.001);
1422    }
1423
1424    #[test]
1425    fn test_eased_value_clone() {
1426        let eased = EasedValue::new(10.0, 90.0, 2.0);
1427        let cloned = eased.clone();
1428        assert!((cloned.from - 10.0).abs() < 0.001);
1429        assert!((cloned.to - 90.0).abs() < 0.001);
1430    }
1431
1432    #[test]
1433    fn test_eased_value_debug() {
1434        let eased = EasedValue::new(0.0, 100.0, 1.0);
1435        let debug = format!("{:?}", eased);
1436        assert!(debug.contains("EasedValue"));
1437    }
1438
1439    // =========================================================================
1440    // AnimatedValue Additional Tests
1441    // =========================================================================
1442
1443    #[test]
1444    fn test_animated_value_spring_complete() {
1445        let mut spring = Spring::new(0.0);
1446        spring.set_immediate(100.0);
1447        let anim = AnimatedValue::Spring(spring);
1448        assert!(anim.is_complete());
1449    }
1450
1451    #[test]
1452    fn test_animated_value_update_eased() {
1453        let mut anim = AnimatedValue::Eased(EasedValue::new(0.0, 100.0, 1.0));
1454        anim.update(0.5);
1455        assert!(anim.value() > 0.0);
1456        assert!(anim.value() < 100.0);
1457    }
1458
1459    #[test]
1460    fn test_animated_value_update_spring() {
1461        let mut spring = Spring::new(0.0);
1462        spring.set_target(100.0);
1463        let mut anim = AnimatedValue::Spring(spring);
1464        anim.update(1.0 / 60.0);
1465        assert!(anim.value() > 0.0);
1466    }
1467
1468    // =========================================================================
1469    // Keyframe Additional Tests
1470    // =========================================================================
1471
1472    #[test]
1473    fn test_keyframe_with_easing() {
1474        let kf: Keyframe<f64> = Keyframe::new(0.5, 50.0).with_easing(Easing::CubicOut);
1475        assert_eq!(kf.easing, Easing::CubicOut);
1476    }
1477
1478    #[test]
1479    fn test_keyframe_clamps_negative_time() {
1480        let kf: Keyframe<f64> = Keyframe::new(-0.5, 50.0);
1481        assert!((kf.time - 0.0).abs() < 0.001);
1482    }
1483
1484    #[test]
1485    fn test_keyframe_clone() {
1486        let kf: Keyframe<f64> = Keyframe::new(0.5, 75.0);
1487        let cloned = kf.clone();
1488        assert!((cloned.time - 0.5).abs() < 0.001);
1489        assert!((cloned.value - 75.0).abs() < 0.001);
1490    }
1491
1492    #[test]
1493    fn test_keyframe_debug() {
1494        let kf: Keyframe<f64> = Keyframe::new(0.5, 50.0);
1495        let debug = format!("{:?}", kf);
1496        assert!(debug.contains("Keyframe"));
1497    }
1498
1499    // =========================================================================
1500    // KeyframeTrack Additional Tests
1501    // =========================================================================
1502
1503    #[test]
1504    fn test_keyframe_track_zero_duration() {
1505        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(0.0);
1506        track.add_keyframe(Keyframe::new(0.0, 0.0));
1507        track.add_keyframe(Keyframe::new(1.0, 100.0));
1508        // Zero duration should jump to end
1509        assert!((track.value().unwrap() - 100.0).abs() < 0.001);
1510    }
1511
1512    #[test]
1513    fn test_keyframe_track_multiple_keyframes() {
1514        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1515        track.add_keyframe(Keyframe::new(0.0, 0.0));
1516        track.add_keyframe(Keyframe::new(0.5, 50.0));
1517        track.add_keyframe(Keyframe::new(1.0, 100.0));
1518
1519        track.elapsed = 0.25;
1520        let val = track.value().unwrap();
1521        assert!(val > 20.0 && val < 30.0); // Between 0 and 50
1522
1523        track.elapsed = 0.75;
1524        let val = track.value().unwrap();
1525        assert!(val > 70.0 && val < 80.0); // Between 50 and 100
1526    }
1527
1528    #[test]
1529    fn test_keyframe_track_keyframe_sorting() {
1530        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1531        // Add out of order
1532        track.add_keyframe(Keyframe::new(1.0, 100.0));
1533        track.add_keyframe(Keyframe::new(0.0, 0.0));
1534        track.add_keyframe(Keyframe::new(0.5, 50.0));
1535
1536        // Should still work correctly
1537        track.elapsed = 0.0;
1538        assert!((track.value().unwrap() - 0.0).abs() < 0.001);
1539    }
1540
1541    #[test]
1542    fn test_keyframe_track_looping_wrap() {
1543        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0).with_loop(true);
1544        track.add_keyframe(Keyframe::new(0.0, 0.0));
1545        track.add_keyframe(Keyframe::new(1.0, 100.0));
1546
1547        track.update(2.5); // 2.5 seconds on 1 second loop
1548                           // Should be at 0.5 normalized time
1549        let val = track.value().unwrap();
1550        assert!(val > 40.0 && val < 60.0);
1551    }
1552
1553    #[test]
1554    fn test_keyframe_track_non_looping_clamps() {
1555        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1556        track.add_keyframe(Keyframe::new(0.0, 0.0));
1557        track.add_keyframe(Keyframe::new(1.0, 100.0));
1558
1559        track.update(5.0); // Way past duration
1560        assert!((track.elapsed - 1.0).abs() < 0.001); // Clamped to duration
1561        assert!((track.value().unwrap() - 100.0).abs() < 0.001);
1562    }
1563
1564    #[test]
1565    fn test_keyframe_track_is_complete() {
1566        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1567        track.add_keyframe(Keyframe::new(0.0, 0.0));
1568        assert!(!track.is_complete());
1569        track.update(1.0);
1570        assert!(track.is_complete());
1571    }
1572
1573    #[test]
1574    fn test_keyframe_track_looping_never_complete() {
1575        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0).with_loop(true);
1576        track.add_keyframe(Keyframe::new(0.0, 0.0));
1577        track.update(10.0);
1578        assert!(!track.is_complete());
1579    }
1580
1581    #[test]
1582    fn test_keyframe_track_clone() {
1583        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(2.0);
1584        track.add_keyframe(Keyframe::new(0.0, 0.0));
1585        let cloned = track.clone();
1586        assert!((cloned.duration - 2.0).abs() < 0.001);
1587    }
1588
1589    #[test]
1590    fn test_keyframe_track_debug() {
1591        let track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1592        let debug = format!("{:?}", track);
1593        assert!(debug.contains("KeyframeTrack"));
1594    }
1595
1596    // =========================================================================
1597    // AnimColor Additional Tests
1598    // =========================================================================
1599
1600    #[test]
1601    fn test_anim_color_new() {
1602        let color = AnimColor::new(0.5, 0.6, 0.7, 0.8);
1603        assert!((color.r - 0.5).abs() < 0.001);
1604        assert!((color.g - 0.6).abs() < 0.001);
1605        assert!((color.b - 0.7).abs() < 0.001);
1606        assert!((color.a - 0.8).abs() < 0.001);
1607    }
1608
1609    #[test]
1610    fn test_anim_color_constants() {
1611        assert!((AnimColor::WHITE.r - 1.0).abs() < 0.001);
1612        assert!((AnimColor::BLACK.r - 0.0).abs() < 0.001);
1613        assert!((AnimColor::TRANSPARENT.a - 0.0).abs() < 0.001);
1614    }
1615
1616    #[test]
1617    fn test_anim_color_interpolate_alpha() {
1618        let from = AnimColor::new(1.0, 1.0, 1.0, 0.0);
1619        let to = AnimColor::new(1.0, 1.0, 1.0, 1.0);
1620        let result = AnimColor::interpolate(&from, &to, 0.5);
1621        assert!((result.a - 0.5).abs() < 0.001);
1622    }
1623
1624    #[test]
1625    fn test_anim_color_clone() {
1626        let color = AnimColor::new(0.1, 0.2, 0.3, 0.4);
1627        let cloned = color;
1628        assert_eq!(color, cloned);
1629    }
1630
1631    #[test]
1632    fn test_anim_color_debug() {
1633        let color = AnimColor::WHITE;
1634        let debug = format!("{:?}", color);
1635        assert!(debug.contains("AnimColor"));
1636    }
1637
1638    // =========================================================================
1639    // AnimationController Additional Tests
1640    // =========================================================================
1641
1642    #[test]
1643    fn test_controller_default() {
1644        let controller = AnimationController::default();
1645        assert!(!controller.is_animating());
1646    }
1647
1648    #[test]
1649    fn test_controller_set_target_nonexistent() {
1650        let mut controller = AnimationController::new();
1651        controller.set_target("nonexistent", 100.0); // Should not panic
1652    }
1653
1654    #[test]
1655    fn test_controller_mixed_animations() {
1656        let mut controller = AnimationController::new();
1657        controller.add_spring("spring", 0.0, SpringConfig::STIFF);
1658        controller.add_eased("eased", 0.0, 100.0, 0.5, Easing::Linear);
1659
1660        controller.set_target("spring", 100.0);
1661        controller.update(0.25);
1662
1663        assert!(controller.is_animating());
1664        // Both should have values
1665        assert!(controller.get("spring").is_some());
1666        assert!(controller.get("eased").is_some());
1667    }
1668
1669    #[test]
1670    fn test_controller_eased_completes() {
1671        let mut controller = AnimationController::new();
1672        controller.add_eased("fade", 0.0, 1.0, 0.5, Easing::Linear);
1673        controller.update(0.5);
1674        assert!(!controller.is_animating()); // Should be complete
1675    }
1676
1677    #[test]
1678    fn test_controller_debug() {
1679        let controller = AnimationController::new();
1680        let debug = format!("{:?}", controller);
1681        assert!(debug.contains("AnimationController"));
1682    }
1683
1684    // =========================================================================
1685    // Interpolate Additional Tests
1686    // =========================================================================
1687
1688    #[test]
1689    fn test_interpolate_f64_boundaries() {
1690        assert!((f64::interpolate(&0.0, &100.0, 0.0) - 0.0).abs() < 0.001);
1691        assert!((f64::interpolate(&0.0, &100.0, 1.0) - 100.0).abs() < 0.001);
1692    }
1693
1694    #[test]
1695    fn test_interpolate_f32_negative() {
1696        let result = f32::interpolate(&-50.0, &50.0, 0.5);
1697        assert!((result - 0.0).abs() < 0.001);
1698    }
1699
1700    #[test]
1701    fn test_interpolate_point_negative() {
1702        let from = Point {
1703            x: -100.0,
1704            y: -100.0,
1705        };
1706        let to = Point { x: 100.0, y: 100.0 };
1707        let result = Point::interpolate(&from, &to, 0.5);
1708        assert!((result.x - 0.0).abs() < 0.001);
1709        assert!((result.y - 0.0).abs() < 0.001);
1710    }
1711
1712    #[test]
1713    fn test_interpolate_color_boundaries() {
1714        let result_start = AnimColor::interpolate(&AnimColor::BLACK, &AnimColor::WHITE, 0.0);
1715        assert!((result_start.r - 0.0).abs() < 0.001);
1716
1717        let result_end = AnimColor::interpolate(&AnimColor::BLACK, &AnimColor::WHITE, 1.0);
1718        assert!((result_end.r - 1.0).abs() < 0.001);
1719    }
1720}