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
493            .sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
494    }
495
496    /// Set looping.
497    #[must_use]
498    pub fn with_loop(mut self, looping: bool) -> Self {
499        self.looping = looping;
500        self
501    }
502
503    /// Get value at current time.
504    #[must_use]
505    pub fn value(&self) -> Option<T> {
506        if self.keyframes.is_empty() {
507            return None;
508        }
509
510        let t = if self.duration > 0.0 {
511            let raw = self.elapsed / self.duration;
512            if self.looping {
513                raw % 1.0
514            } else {
515                raw.clamp(0.0, 1.0)
516            }
517        } else {
518            1.0
519        };
520
521        // Find surrounding keyframes
522        let mut prev_idx = 0;
523        let mut next_idx = 0;
524
525        for (i, kf) in self.keyframes.iter().enumerate() {
526            if kf.time <= t {
527                prev_idx = i;
528            }
529            if kf.time >= t {
530                next_idx = i;
531                break;
532            }
533            next_idx = i;
534        }
535
536        let prev = &self.keyframes[prev_idx];
537        let next = &self.keyframes[next_idx];
538
539        if prev_idx == next_idx {
540            return Some(prev.value.clone());
541        }
542
543        // Interpolate between keyframes
544        let segment_duration = next.time - prev.time;
545        let segment_t = if segment_duration > 0.0 {
546            (t - prev.time) / segment_duration
547        } else {
548            1.0
549        };
550
551        let eased_t = prev.easing.apply(segment_t);
552        Some(T::interpolate(&prev.value, &next.value, eased_t))
553    }
554
555    /// Update animation.
556    pub fn update(&mut self, dt: f64) {
557        self.elapsed += dt;
558        if !self.looping && self.elapsed > self.duration {
559            self.elapsed = self.duration;
560        }
561    }
562
563    /// Whether animation is complete.
564    #[must_use]
565    pub fn is_complete(&self) -> bool {
566        !self.looping && self.elapsed >= self.duration
567    }
568
569    /// Reset to start.
570    pub fn reset(&mut self) {
571        self.elapsed = 0.0;
572    }
573}
574
575// =============================================================================
576// Interpolate Trait
577// =============================================================================
578
579/// Trait for types that can be interpolated.
580pub trait Interpolate {
581    /// Interpolate between two values.
582    fn interpolate(from: &Self, to: &Self, t: f64) -> Self;
583}
584
585impl Interpolate for f64 {
586    fn interpolate(from: &Self, to: &Self, t: f64) -> Self {
587        from + (to - from) * t
588    }
589}
590
591impl Interpolate for f32 {
592    fn interpolate(from: &Self, to: &Self, t: f64) -> Self {
593        (*to - *from).mul_add(t as Self, *from)
594    }
595}
596
597impl Interpolate for Point {
598    fn interpolate(from: &Self, to: &Self, t: f64) -> Self {
599        Self {
600            x: f32::interpolate(&from.x, &to.x, t),
601            y: f32::interpolate(&from.y, &to.y, t),
602        }
603    }
604}
605
606/// Color for animation (RGBA as f32 0-1).
607#[derive(Debug, Clone, Copy, PartialEq)]
608pub struct AnimColor {
609    pub r: f32,
610    pub g: f32,
611    pub b: f32,
612    pub a: f32,
613}
614
615impl AnimColor {
616    pub const WHITE: Self = Self {
617        r: 1.0,
618        g: 1.0,
619        b: 1.0,
620        a: 1.0,
621    };
622    pub const BLACK: Self = Self {
623        r: 0.0,
624        g: 0.0,
625        b: 0.0,
626        a: 1.0,
627    };
628    pub const TRANSPARENT: Self = Self {
629        r: 0.0,
630        g: 0.0,
631        b: 0.0,
632        a: 0.0,
633    };
634
635    #[must_use]
636    pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
637        Self { r, g, b, a }
638    }
639}
640
641impl Interpolate for AnimColor {
642    fn interpolate(from: &Self, to: &Self, t: f64) -> Self {
643        let t = t as f32;
644        Self {
645            r: (to.r - from.r).mul_add(t, from.r),
646            g: (to.g - from.g).mul_add(t, from.g),
647            b: (to.b - from.b).mul_add(t, from.b),
648            a: (to.a - from.a).mul_add(t, from.a),
649        }
650    }
651}
652
653// =============================================================================
654// AnimationController - Manages Multiple Animations
655// =============================================================================
656
657/// Controller for managing multiple animations.
658#[derive(Debug, Default)]
659pub struct AnimationController {
660    /// Named springs
661    springs: HashMap<String, Spring>,
662    /// Named eased values
663    eased: HashMap<String, EasedValue>,
664    /// Active animation count
665    active_count: usize,
666}
667
668impl AnimationController {
669    /// Create new controller.
670    #[must_use]
671    pub fn new() -> Self {
672        Self::default()
673    }
674
675    /// Add a spring animation.
676    pub fn add_spring(&mut self, name: &str, initial: f64, config: SpringConfig) {
677        let spring = Spring::new(initial).with_config(config);
678        self.springs.insert(name.to_string(), spring);
679    }
680
681    /// Add an eased animation.
682    pub fn add_eased(&mut self, name: &str, from: f64, to: f64, duration: f64, easing: Easing) {
683        let eased = EasedValue::new(from, to, duration).with_easing(easing);
684        self.eased.insert(name.to_string(), eased);
685    }
686
687    /// Set spring target.
688    pub fn set_target(&mut self, name: &str, target: f64) {
689        if let Some(spring) = self.springs.get_mut(name) {
690            spring.set_target(target);
691        }
692    }
693
694    /// Get current value.
695    #[must_use]
696    pub fn get(&self, name: &str) -> Option<f64> {
697        if let Some(spring) = self.springs.get(name) {
698            return Some(spring.value);
699        }
700        if let Some(eased) = self.eased.get(name) {
701            return Some(eased.value());
702        }
703        None
704    }
705
706    /// Update all animations.
707    pub fn update(&mut self, dt: f64) {
708        self.active_count = 0;
709
710        for spring in self.springs.values_mut() {
711            spring.update(dt);
712            if !spring.at_rest {
713                self.active_count += 1;
714            }
715        }
716
717        for eased in self.eased.values_mut() {
718            eased.update(dt);
719            if !eased.is_complete() {
720                self.active_count += 1;
721            }
722        }
723    }
724
725    /// Whether any animations are active.
726    #[must_use]
727    pub fn is_animating(&self) -> bool {
728        self.active_count > 0
729    }
730
731    /// Number of active animations.
732    #[must_use]
733    pub fn active_count(&self) -> usize {
734        self.active_count
735    }
736
737    /// Remove an animation.
738    pub fn remove(&mut self, name: &str) {
739        self.springs.remove(name);
740        self.eased.remove(name);
741    }
742
743    /// Clear all animations.
744    pub fn clear(&mut self) {
745        self.springs.clear();
746        self.eased.clear();
747        self.active_count = 0;
748    }
749}
750
751// =============================================================================
752// Tests - TDD Style
753// =============================================================================
754
755#[cfg(test)]
756mod tests {
757    use super::*;
758
759    // -------------------------------------------------------------------------
760    // Easing tests
761    // -------------------------------------------------------------------------
762
763    #[test]
764    fn test_easing_linear() {
765        assert!((Easing::Linear.apply(0.0) - 0.0).abs() < 0.001);
766        assert!((Easing::Linear.apply(0.5) - 0.5).abs() < 0.001);
767        assert!((Easing::Linear.apply(1.0) - 1.0).abs() < 0.001);
768    }
769
770    #[test]
771    fn test_easing_clamps_input() {
772        assert!((Easing::Linear.apply(-0.5) - 0.0).abs() < 0.001);
773        assert!((Easing::Linear.apply(1.5) - 1.0).abs() < 0.001);
774    }
775
776    #[test]
777    fn test_easing_ease_in() {
778        let val = Easing::EaseIn.apply(0.5);
779        assert!(val < 0.5); // Should be below linear at midpoint
780    }
781
782    #[test]
783    fn test_easing_ease_out() {
784        let val = Easing::EaseOut.apply(0.5);
785        assert!(val > 0.5); // Should be above linear at midpoint
786    }
787
788    #[test]
789    fn test_easing_ease_in_out() {
790        let val = Easing::EaseInOut.apply(0.5);
791        assert!((val - 0.5).abs() < 0.01); // Should be near 0.5 at midpoint
792    }
793
794    #[test]
795    fn test_easing_cubic() {
796        assert!((Easing::CubicIn.apply(0.0) - 0.0).abs() < 0.001);
797        assert!((Easing::CubicOut.apply(1.0) - 1.0).abs() < 0.001);
798    }
799
800    #[test]
801    fn test_easing_expo() {
802        assert!((Easing::ExpoIn.apply(0.0) - 0.0).abs() < 0.001);
803        assert!((Easing::ExpoOut.apply(1.0) - 1.0).abs() < 0.001);
804    }
805
806    #[test]
807    fn test_easing_elastic() {
808        let val = Easing::ElasticOut.apply(1.0);
809        assert!((val - 1.0).abs() < 0.001);
810    }
811
812    #[test]
813    fn test_easing_bounce() {
814        let val = Easing::BounceOut.apply(1.0);
815        assert!((val - 1.0).abs() < 0.001);
816    }
817
818    #[test]
819    fn test_easing_back() {
820        let val = Easing::BackOut.apply(1.0);
821        assert!((val - 1.0).abs() < 0.001);
822    }
823
824    // -------------------------------------------------------------------------
825    // SpringConfig tests
826    // -------------------------------------------------------------------------
827
828    #[test]
829    fn test_spring_config_presets() {
830        assert!(SpringConfig::GENTLE.stiffness < SpringConfig::STIFF.stiffness);
831        assert!(SpringConfig::WOBBLY.damping < SpringConfig::STIFF.damping);
832    }
833
834    #[test]
835    fn test_spring_config_damping_ratio() {
836        let config = SpringConfig::GENTLE;
837        let ratio = config.damping_ratio();
838        assert!(ratio > 0.0);
839    }
840
841    #[test]
842    fn test_spring_config_damping_types() {
843        // Underdamped (bouncy)
844        let underdamped = SpringConfig::custom(1.0, 100.0, 5.0);
845        assert!(underdamped.is_underdamped());
846
847        // Overdamped (slow)
848        let overdamped = SpringConfig::custom(1.0, 100.0, 50.0);
849        assert!(overdamped.is_overdamped());
850    }
851
852    // -------------------------------------------------------------------------
853    // Spring tests
854    // -------------------------------------------------------------------------
855
856    #[test]
857    fn test_spring_new() {
858        let spring = Spring::new(10.0);
859        assert!((spring.value - 10.0).abs() < 0.001);
860        assert!((spring.target - 10.0).abs() < 0.001);
861        assert!(spring.at_rest);
862    }
863
864    #[test]
865    fn test_spring_set_target() {
866        let mut spring = Spring::new(0.0);
867        spring.set_target(100.0);
868        assert!(!spring.at_rest);
869        assert!((spring.target - 100.0).abs() < 0.001);
870    }
871
872    #[test]
873    fn test_spring_update() {
874        let mut spring = Spring::new(0.0);
875        spring.set_target(100.0);
876
877        // Simulate multiple frames
878        for _ in 0..100 {
879            spring.update(1.0 / 60.0); // 60fps
880        }
881
882        // Should be near target
883        assert!((spring.value - 100.0).abs() < 1.0);
884    }
885
886    #[test]
887    fn test_spring_converges() {
888        let mut spring = Spring::new(0.0);
889        spring.set_target(100.0);
890
891        // Simulate until at rest
892        for _ in 0..1000 {
893            if spring.at_rest {
894                break;
895            }
896            spring.update(1.0 / 60.0);
897        }
898
899        assert!(spring.at_rest);
900        assert!((spring.value - 100.0).abs() < 0.01);
901    }
902
903    #[test]
904    fn test_spring_set_immediate() {
905        let mut spring = Spring::new(0.0);
906        spring.set_target(100.0);
907        spring.update(1.0 / 60.0);
908
909        spring.set_immediate(50.0);
910        assert!(spring.at_rest);
911        assert!((spring.value - 50.0).abs() < 0.001);
912    }
913
914    #[test]
915    fn test_spring_no_update_when_at_rest() {
916        let mut spring = Spring::new(100.0);
917        let initial_value = spring.value;
918        spring.update(1.0 / 60.0);
919        assert!((spring.value - initial_value).abs() < 0.001);
920    }
921
922    // -------------------------------------------------------------------------
923    // EasedValue tests
924    // -------------------------------------------------------------------------
925
926    #[test]
927    fn test_eased_value_new() {
928        let eased = EasedValue::new(0.0, 100.0, 1.0);
929        assert!((eased.value() - 0.0).abs() < 0.001);
930        assert!(!eased.is_complete());
931    }
932
933    #[test]
934    fn test_eased_value_update() {
935        let mut eased = EasedValue::new(0.0, 100.0, 1.0);
936        eased.update(0.5);
937        assert!(eased.value() > 0.0);
938        assert!(eased.value() < 100.0);
939    }
940
941    #[test]
942    fn test_eased_value_complete() {
943        let mut eased = EasedValue::new(0.0, 100.0, 1.0);
944        eased.update(2.0); // Past duration
945        assert!(eased.is_complete());
946        assert!((eased.value() - 100.0).abs() < 0.001);
947    }
948
949    #[test]
950    fn test_eased_value_progress() {
951        let mut eased = EasedValue::new(0.0, 100.0, 1.0);
952        assert!((eased.progress() - 0.0).abs() < 0.001);
953        eased.update(0.5);
954        assert!((eased.progress() - 0.5).abs() < 0.001);
955    }
956
957    #[test]
958    fn test_eased_value_with_easing() {
959        let eased = EasedValue::new(0.0, 100.0, 1.0).with_easing(Easing::CubicOut);
960        assert_eq!(eased.easing, Easing::CubicOut);
961    }
962
963    // -------------------------------------------------------------------------
964    // AnimatedValue tests
965    // -------------------------------------------------------------------------
966
967    #[test]
968    fn test_animated_value_eased() {
969        let mut anim = AnimatedValue::Eased(EasedValue::new(0.0, 100.0, 1.0));
970        assert!((anim.value() - 0.0).abs() < 0.001);
971        anim.update(1.0);
972        assert!(anim.is_complete());
973    }
974
975    #[test]
976    fn test_animated_value_spring() {
977        let mut anim = AnimatedValue::Spring(Spring::new(0.0));
978        if let AnimatedValue::Spring(ref mut s) = anim {
979            s.set_target(100.0);
980        }
981        assert!(!anim.is_complete());
982    }
983
984    // -------------------------------------------------------------------------
985    // Keyframe tests
986    // -------------------------------------------------------------------------
987
988    #[test]
989    fn test_keyframe_new() {
990        let kf: Keyframe<f64> = Keyframe::new(0.5, 50.0);
991        assert!((kf.time - 0.5).abs() < 0.001);
992        assert!((kf.value - 50.0).abs() < 0.001);
993    }
994
995    #[test]
996    fn test_keyframe_clamps_time() {
997        let kf: Keyframe<f64> = Keyframe::new(1.5, 50.0);
998        assert!((kf.time - 1.0).abs() < 0.001);
999    }
1000
1001    #[test]
1002    fn test_keyframe_track_new() {
1003        let track: KeyframeTrack<f64> = KeyframeTrack::new(2.0);
1004        assert!((track.duration - 2.0).abs() < 0.001);
1005        assert!(track.value().is_none());
1006    }
1007
1008    #[test]
1009    fn test_keyframe_track_single_keyframe() {
1010        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1011        track.add_keyframe(Keyframe::new(0.0, 100.0));
1012        assert!((track.value().unwrap() - 100.0).abs() < 0.001);
1013    }
1014
1015    #[test]
1016    fn test_keyframe_track_interpolation() {
1017        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1018        track.add_keyframe(Keyframe::new(0.0, 0.0));
1019        track.add_keyframe(Keyframe::new(1.0, 100.0));
1020
1021        track.update(0.5);
1022        let val = track.value().unwrap();
1023        assert!(val > 40.0 && val < 60.0); // Should be near 50
1024    }
1025
1026    #[test]
1027    fn test_keyframe_track_looping() {
1028        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0).with_loop(true);
1029        track.add_keyframe(Keyframe::new(0.0, 0.0));
1030        track.add_keyframe(Keyframe::new(1.0, 100.0));
1031
1032        track.update(1.5);
1033        assert!(!track.is_complete());
1034    }
1035
1036    #[test]
1037    fn test_keyframe_track_reset() {
1038        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1039        track.add_keyframe(Keyframe::new(0.0, 0.0));
1040        track.update(0.5);
1041        track.reset();
1042        assert!((track.elapsed - 0.0).abs() < 0.001);
1043    }
1044
1045    // -------------------------------------------------------------------------
1046    // Interpolate tests
1047    // -------------------------------------------------------------------------
1048
1049    #[test]
1050    fn test_interpolate_f64() {
1051        let result = f64::interpolate(&0.0, &100.0, 0.5);
1052        assert!((result - 50.0).abs() < 0.001);
1053    }
1054
1055    #[test]
1056    fn test_interpolate_f32() {
1057        let result = f32::interpolate(&0.0, &100.0, 0.5);
1058        assert!((result - 50.0).abs() < 0.001);
1059    }
1060
1061    #[test]
1062    fn test_interpolate_point() {
1063        let from = Point { x: 0.0, y: 0.0 };
1064        let to = Point { x: 100.0, y: 100.0 };
1065        let result = Point::interpolate(&from, &to, 0.5);
1066        assert!((result.x - 50.0).abs() < 0.001);
1067        assert!((result.y - 50.0).abs() < 0.001);
1068    }
1069
1070    #[test]
1071    fn test_interpolate_color() {
1072        let result = AnimColor::interpolate(&AnimColor::BLACK, &AnimColor::WHITE, 0.5);
1073        assert!((result.r - 0.5).abs() < 0.001);
1074        assert!((result.g - 0.5).abs() < 0.001);
1075        assert!((result.b - 0.5).abs() < 0.001);
1076    }
1077
1078    // -------------------------------------------------------------------------
1079    // AnimationController tests
1080    // -------------------------------------------------------------------------
1081
1082    #[test]
1083    fn test_controller_new() {
1084        let controller = AnimationController::new();
1085        assert!(!controller.is_animating());
1086        assert_eq!(controller.active_count(), 0);
1087    }
1088
1089    #[test]
1090    fn test_controller_add_spring() {
1091        let mut controller = AnimationController::new();
1092        controller.add_spring("x", 0.0, SpringConfig::GENTLE);
1093        assert!((controller.get("x").unwrap() - 0.0).abs() < 0.001);
1094    }
1095
1096    #[test]
1097    fn test_controller_add_eased() {
1098        let mut controller = AnimationController::new();
1099        controller.add_eased("opacity", 0.0, 1.0, 0.3, Easing::EaseOut);
1100        assert!((controller.get("opacity").unwrap() - 0.0).abs() < 0.001);
1101    }
1102
1103    #[test]
1104    fn test_controller_set_target() {
1105        let mut controller = AnimationController::new();
1106        controller.add_spring("x", 0.0, SpringConfig::STIFF);
1107        controller.set_target("x", 100.0);
1108        controller.update(1.0 / 60.0);
1109        assert!(controller.is_animating());
1110    }
1111
1112    #[test]
1113    fn test_controller_update() {
1114        let mut controller = AnimationController::new();
1115        controller.add_eased("fade", 0.0, 1.0, 0.5, Easing::Linear);
1116        controller.update(0.25);
1117        let val = controller.get("fade").unwrap();
1118        assert!(val > 0.4 && val < 0.6);
1119    }
1120
1121    #[test]
1122    fn test_controller_remove() {
1123        let mut controller = AnimationController::new();
1124        controller.add_spring("x", 0.0, SpringConfig::GENTLE);
1125        controller.remove("x");
1126        assert!(controller.get("x").is_none());
1127    }
1128
1129    #[test]
1130    fn test_controller_clear() {
1131        let mut controller = AnimationController::new();
1132        controller.add_spring("x", 0.0, SpringConfig::GENTLE);
1133        controller.add_spring("y", 0.0, SpringConfig::GENTLE);
1134        controller.clear();
1135        assert!(controller.get("x").is_none());
1136        assert!(controller.get("y").is_none());
1137    }
1138
1139    #[test]
1140    fn test_controller_get_nonexistent() {
1141        let controller = AnimationController::new();
1142        assert!(controller.get("nonexistent").is_none());
1143    }
1144
1145    #[test]
1146    fn test_controller_active_count() {
1147        let mut controller = AnimationController::new();
1148        controller.add_spring("a", 0.0, SpringConfig::GENTLE);
1149        controller.add_spring("b", 0.0, SpringConfig::GENTLE);
1150        controller.set_target("a", 100.0);
1151        controller.set_target("b", 100.0);
1152        controller.update(1.0 / 60.0);
1153        assert_eq!(controller.active_count(), 2);
1154    }
1155
1156    // =========================================================================
1157    // Easing Additional Tests
1158    // =========================================================================
1159
1160    #[test]
1161    fn test_easing_default() {
1162        assert_eq!(Easing::default(), Easing::Linear);
1163    }
1164
1165    #[test]
1166    fn test_easing_all_variants_at_zero() {
1167        let easings = [
1168            Easing::Linear,
1169            Easing::EaseIn,
1170            Easing::EaseOut,
1171            Easing::EaseInOut,
1172            Easing::CubicIn,
1173            Easing::CubicOut,
1174            Easing::CubicInOut,
1175            Easing::ExpoIn,
1176            Easing::ExpoOut,
1177            Easing::ElasticOut,
1178            Easing::BounceOut,
1179            Easing::BackOut,
1180        ];
1181        for easing in easings {
1182            let val = easing.apply(0.0);
1183            assert!(val.abs() < 0.01, "{:?} at 0.0 = {}", easing, val);
1184        }
1185    }
1186
1187    #[test]
1188    fn test_easing_all_variants_at_one() {
1189        let easings = [
1190            Easing::Linear,
1191            Easing::EaseIn,
1192            Easing::EaseOut,
1193            Easing::EaseInOut,
1194            Easing::CubicIn,
1195            Easing::CubicOut,
1196            Easing::CubicInOut,
1197            Easing::ExpoIn,
1198            Easing::ExpoOut,
1199            Easing::ElasticOut,
1200            Easing::BounceOut,
1201            Easing::BackOut,
1202        ];
1203        for easing in easings {
1204            let val = easing.apply(1.0);
1205            assert!((val - 1.0).abs() < 0.01, "{:?} at 1.0 = {}", easing, val);
1206        }
1207    }
1208
1209    #[test]
1210    fn test_easing_cubic_in_out_midpoint() {
1211        let val = Easing::CubicInOut.apply(0.5);
1212        assert!((val - 0.5).abs() < 0.01);
1213    }
1214
1215    #[test]
1216    fn test_easing_expo_in_zero() {
1217        // ExpoIn has special case for 0
1218        let val = Easing::ExpoIn.apply(0.0);
1219        assert!((val - 0.0).abs() < 0.001);
1220    }
1221
1222    #[test]
1223    fn test_easing_expo_out_one() {
1224        // ExpoOut has special case for 1
1225        let val = Easing::ExpoOut.apply(1.0);
1226        assert!((val - 1.0).abs() < 0.001);
1227    }
1228
1229    #[test]
1230    fn test_easing_elastic_out_zero() {
1231        let val = Easing::ElasticOut.apply(0.0);
1232        assert!((val - 0.0).abs() < 0.001);
1233    }
1234
1235    #[test]
1236    fn test_easing_bounce_out_segments() {
1237        // Test all segments of bounce
1238        assert!(Easing::BounceOut.apply(0.1) < 0.3);
1239        assert!(Easing::BounceOut.apply(0.5) > 0.5);
1240        assert!(Easing::BounceOut.apply(0.8) > 0.9);
1241        assert!(Easing::BounceOut.apply(0.95) > 0.98);
1242    }
1243
1244    #[test]
1245    fn test_easing_back_out_overshoots() {
1246        // BackOut should overshoot slightly past 1.0 before settling
1247        let val_mid = Easing::BackOut.apply(0.5);
1248        assert!(val_mid > 0.5); // Should be ahead of linear
1249    }
1250
1251    #[test]
1252    fn test_easing_clone() {
1253        let e = Easing::CubicOut;
1254        let cloned = e;
1255        assert_eq!(e, cloned);
1256    }
1257
1258    #[test]
1259    fn test_easing_debug() {
1260        let e = Easing::ElasticOut;
1261        let debug = format!("{:?}", e);
1262        assert!(debug.contains("ElasticOut"));
1263    }
1264
1265    // =========================================================================
1266    // SpringConfig Additional Tests
1267    // =========================================================================
1268
1269    #[test]
1270    fn test_spring_config_default() {
1271        let config = SpringConfig::default();
1272        assert_eq!(config, SpringConfig::GENTLE);
1273    }
1274
1275    #[test]
1276    fn test_spring_config_custom() {
1277        let config = SpringConfig::custom(2.0, 200.0, 20.0);
1278        assert!((config.mass - 2.0).abs() < 0.001);
1279        assert!((config.stiffness - 200.0).abs() < 0.001);
1280        assert!((config.damping - 20.0).abs() < 0.001);
1281    }
1282
1283    #[test]
1284    fn test_spring_config_molasses() {
1285        let config = SpringConfig::MOLASSES;
1286        assert!(config.stiffness < SpringConfig::GENTLE.stiffness);
1287    }
1288
1289    #[test]
1290    fn test_spring_config_critically_damped() {
1291        // Critical damping = 2 * sqrt(m * k)
1292        // For m=1, k=100: critical = 2 * 10 = 20
1293        let config = SpringConfig::custom(1.0, 100.0, 20.0);
1294        assert!(config.is_critically_damped());
1295    }
1296
1297    #[test]
1298    fn test_spring_config_all_presets_valid() {
1299        let presets = [
1300            SpringConfig::GENTLE,
1301            SpringConfig::WOBBLY,
1302            SpringConfig::STIFF,
1303            SpringConfig::MOLASSES,
1304        ];
1305        for config in presets {
1306            assert!(config.mass > 0.0);
1307            assert!(config.stiffness > 0.0);
1308            assert!(config.damping > 0.0);
1309        }
1310    }
1311
1312    #[test]
1313    fn test_spring_config_clone() {
1314        let config = SpringConfig::STIFF;
1315        let cloned = config;
1316        assert_eq!(config, cloned);
1317    }
1318
1319    #[test]
1320    fn test_spring_config_debug() {
1321        let config = SpringConfig::WOBBLY;
1322        let debug = format!("{:?}", config);
1323        assert!(debug.contains("SpringConfig"));
1324    }
1325
1326    // =========================================================================
1327    // Spring Additional Tests
1328    // =========================================================================
1329
1330    #[test]
1331    fn test_spring_with_config() {
1332        let spring = Spring::new(0.0).with_config(SpringConfig::STIFF);
1333        assert_eq!(spring.config, SpringConfig::STIFF);
1334    }
1335
1336    #[test]
1337    fn test_spring_set_target_same_value() {
1338        let mut spring = Spring::new(100.0);
1339        spring.set_target(100.0); // Same as initial
1340        assert!(spring.at_rest); // Should remain at rest
1341    }
1342
1343    #[test]
1344    fn test_spring_update_small_dt() {
1345        let mut spring = Spring::new(0.0);
1346        spring.set_target(100.0);
1347        spring.update(0.001); // Very small time step
1348        assert!(spring.value > 0.0);
1349    }
1350
1351    #[test]
1352    fn test_spring_precision_threshold() {
1353        let mut spring = Spring::new(0.0);
1354        spring.precision = 0.1; // Larger precision
1355        spring.set_target(0.05); // Within precision
1356        spring.update(0.016);
1357        // Should settle quickly with larger precision
1358    }
1359
1360    #[test]
1361    fn test_spring_negative_values() {
1362        let mut spring = Spring::new(0.0);
1363        spring.set_target(-100.0);
1364        for _ in 0..200 {
1365            spring.update(1.0 / 60.0);
1366        }
1367        assert!((spring.value - (-100.0)).abs() < 1.0);
1368    }
1369
1370    #[test]
1371    fn test_spring_clone() {
1372        let spring = Spring::new(50.0);
1373        let cloned = spring.clone();
1374        assert!((cloned.value - 50.0).abs() < 0.001);
1375    }
1376
1377    #[test]
1378    fn test_spring_debug() {
1379        let spring = Spring::new(0.0);
1380        let debug = format!("{:?}", spring);
1381        assert!(debug.contains("Spring"));
1382    }
1383
1384    // =========================================================================
1385    // EasedValue Additional Tests
1386    // =========================================================================
1387
1388    #[test]
1389    fn test_eased_value_zero_duration() {
1390        let eased = EasedValue::new(0.0, 100.0, 0.0);
1391        assert!((eased.value() - 100.0).abs() < 0.001); // Instant
1392        assert!(eased.is_complete());
1393    }
1394
1395    #[test]
1396    fn test_eased_value_negative_update() {
1397        let mut eased = EasedValue::new(0.0, 100.0, 1.0);
1398        eased.update(0.5);
1399        eased.update(-0.2); // Negative dt (shouldn't happen but handle gracefully)
1400                            // elapsed should not exceed duration and value should stay in valid range
1401        assert!(eased.elapsed <= eased.duration);
1402        // value() clamps progress to [0, 1], so value is always in [from, to] range
1403        assert!(eased.value() >= 0.0 && eased.value() <= 100.0);
1404        // elapsed should be 0.3 after -0.2 from 0.5
1405        assert!((eased.elapsed - 0.3).abs() < 0.001);
1406    }
1407
1408    #[test]
1409    fn test_eased_value_progress_zero_duration() {
1410        let eased = EasedValue::new(0.0, 100.0, 0.0);
1411        assert!((eased.progress() - 1.0).abs() < 0.001);
1412    }
1413
1414    #[test]
1415    fn test_eased_value_linear_interpolation() {
1416        let mut eased = EasedValue::new(0.0, 100.0, 1.0).with_easing(Easing::Linear);
1417        eased.update(0.5);
1418        assert!((eased.value() - 50.0).abs() < 0.001);
1419    }
1420
1421    #[test]
1422    fn test_eased_value_clone() {
1423        let eased = EasedValue::new(10.0, 90.0, 2.0);
1424        let cloned = eased.clone();
1425        assert!((cloned.from - 10.0).abs() < 0.001);
1426        assert!((cloned.to - 90.0).abs() < 0.001);
1427    }
1428
1429    #[test]
1430    fn test_eased_value_debug() {
1431        let eased = EasedValue::new(0.0, 100.0, 1.0);
1432        let debug = format!("{:?}", eased);
1433        assert!(debug.contains("EasedValue"));
1434    }
1435
1436    // =========================================================================
1437    // AnimatedValue Additional Tests
1438    // =========================================================================
1439
1440    #[test]
1441    fn test_animated_value_spring_complete() {
1442        let mut spring = Spring::new(0.0);
1443        spring.set_immediate(100.0);
1444        let anim = AnimatedValue::Spring(spring);
1445        assert!(anim.is_complete());
1446    }
1447
1448    #[test]
1449    fn test_animated_value_update_eased() {
1450        let mut anim = AnimatedValue::Eased(EasedValue::new(0.0, 100.0, 1.0));
1451        anim.update(0.5);
1452        assert!(anim.value() > 0.0);
1453        assert!(anim.value() < 100.0);
1454    }
1455
1456    #[test]
1457    fn test_animated_value_update_spring() {
1458        let mut spring = Spring::new(0.0);
1459        spring.set_target(100.0);
1460        let mut anim = AnimatedValue::Spring(spring);
1461        anim.update(1.0 / 60.0);
1462        assert!(anim.value() > 0.0);
1463    }
1464
1465    // =========================================================================
1466    // Keyframe Additional Tests
1467    // =========================================================================
1468
1469    #[test]
1470    fn test_keyframe_with_easing() {
1471        let kf: Keyframe<f64> = Keyframe::new(0.5, 50.0).with_easing(Easing::CubicOut);
1472        assert_eq!(kf.easing, Easing::CubicOut);
1473    }
1474
1475    #[test]
1476    fn test_keyframe_clamps_negative_time() {
1477        let kf: Keyframe<f64> = Keyframe::new(-0.5, 50.0);
1478        assert!((kf.time - 0.0).abs() < 0.001);
1479    }
1480
1481    #[test]
1482    fn test_keyframe_clone() {
1483        let kf: Keyframe<f64> = Keyframe::new(0.5, 75.0);
1484        let cloned = kf.clone();
1485        assert!((cloned.time - 0.5).abs() < 0.001);
1486        assert!((cloned.value - 75.0).abs() < 0.001);
1487    }
1488
1489    #[test]
1490    fn test_keyframe_debug() {
1491        let kf: Keyframe<f64> = Keyframe::new(0.5, 50.0);
1492        let debug = format!("{:?}", kf);
1493        assert!(debug.contains("Keyframe"));
1494    }
1495
1496    // =========================================================================
1497    // KeyframeTrack Additional Tests
1498    // =========================================================================
1499
1500    #[test]
1501    fn test_keyframe_track_zero_duration() {
1502        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(0.0);
1503        track.add_keyframe(Keyframe::new(0.0, 0.0));
1504        track.add_keyframe(Keyframe::new(1.0, 100.0));
1505        // Zero duration should jump to end
1506        assert!((track.value().unwrap() - 100.0).abs() < 0.001);
1507    }
1508
1509    #[test]
1510    fn test_keyframe_track_multiple_keyframes() {
1511        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1512        track.add_keyframe(Keyframe::new(0.0, 0.0));
1513        track.add_keyframe(Keyframe::new(0.5, 50.0));
1514        track.add_keyframe(Keyframe::new(1.0, 100.0));
1515
1516        track.elapsed = 0.25;
1517        let val = track.value().unwrap();
1518        assert!(val > 20.0 && val < 30.0); // Between 0 and 50
1519
1520        track.elapsed = 0.75;
1521        let val = track.value().unwrap();
1522        assert!(val > 70.0 && val < 80.0); // Between 50 and 100
1523    }
1524
1525    #[test]
1526    fn test_keyframe_track_keyframe_sorting() {
1527        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1528        // Add out of order
1529        track.add_keyframe(Keyframe::new(1.0, 100.0));
1530        track.add_keyframe(Keyframe::new(0.0, 0.0));
1531        track.add_keyframe(Keyframe::new(0.5, 50.0));
1532
1533        // Should still work correctly
1534        track.elapsed = 0.0;
1535        assert!((track.value().unwrap() - 0.0).abs() < 0.001);
1536    }
1537
1538    #[test]
1539    fn test_keyframe_track_looping_wrap() {
1540        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0).with_loop(true);
1541        track.add_keyframe(Keyframe::new(0.0, 0.0));
1542        track.add_keyframe(Keyframe::new(1.0, 100.0));
1543
1544        track.update(2.5); // 2.5 seconds on 1 second loop
1545                           // Should be at 0.5 normalized time
1546        let val = track.value().unwrap();
1547        assert!(val > 40.0 && val < 60.0);
1548    }
1549
1550    #[test]
1551    fn test_keyframe_track_non_looping_clamps() {
1552        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1553        track.add_keyframe(Keyframe::new(0.0, 0.0));
1554        track.add_keyframe(Keyframe::new(1.0, 100.0));
1555
1556        track.update(5.0); // Way past duration
1557        assert!((track.elapsed - 1.0).abs() < 0.001); // Clamped to duration
1558        assert!((track.value().unwrap() - 100.0).abs() < 0.001);
1559    }
1560
1561    #[test]
1562    fn test_keyframe_track_is_complete() {
1563        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1564        track.add_keyframe(Keyframe::new(0.0, 0.0));
1565        assert!(!track.is_complete());
1566        track.update(1.0);
1567        assert!(track.is_complete());
1568    }
1569
1570    #[test]
1571    fn test_keyframe_track_looping_never_complete() {
1572        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0).with_loop(true);
1573        track.add_keyframe(Keyframe::new(0.0, 0.0));
1574        track.update(10.0);
1575        assert!(!track.is_complete());
1576    }
1577
1578    #[test]
1579    fn test_keyframe_track_clone() {
1580        let mut track: KeyframeTrack<f64> = KeyframeTrack::new(2.0);
1581        track.add_keyframe(Keyframe::new(0.0, 0.0));
1582        let cloned = track.clone();
1583        assert!((cloned.duration - 2.0).abs() < 0.001);
1584    }
1585
1586    #[test]
1587    fn test_keyframe_track_debug() {
1588        let track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1589        let debug = format!("{:?}", track);
1590        assert!(debug.contains("KeyframeTrack"));
1591    }
1592
1593    // =========================================================================
1594    // AnimColor Additional Tests
1595    // =========================================================================
1596
1597    #[test]
1598    fn test_anim_color_new() {
1599        let color = AnimColor::new(0.5, 0.6, 0.7, 0.8);
1600        assert!((color.r - 0.5).abs() < 0.001);
1601        assert!((color.g - 0.6).abs() < 0.001);
1602        assert!((color.b - 0.7).abs() < 0.001);
1603        assert!((color.a - 0.8).abs() < 0.001);
1604    }
1605
1606    #[test]
1607    fn test_anim_color_constants() {
1608        assert!((AnimColor::WHITE.r - 1.0).abs() < 0.001);
1609        assert!((AnimColor::BLACK.r - 0.0).abs() < 0.001);
1610        assert!((AnimColor::TRANSPARENT.a - 0.0).abs() < 0.001);
1611    }
1612
1613    #[test]
1614    fn test_anim_color_interpolate_alpha() {
1615        let from = AnimColor::new(1.0, 1.0, 1.0, 0.0);
1616        let to = AnimColor::new(1.0, 1.0, 1.0, 1.0);
1617        let result = AnimColor::interpolate(&from, &to, 0.5);
1618        assert!((result.a - 0.5).abs() < 0.001);
1619    }
1620
1621    #[test]
1622    fn test_anim_color_clone() {
1623        let color = AnimColor::new(0.1, 0.2, 0.3, 0.4);
1624        let cloned = color;
1625        assert_eq!(color, cloned);
1626    }
1627
1628    #[test]
1629    fn test_anim_color_debug() {
1630        let color = AnimColor::WHITE;
1631        let debug = format!("{:?}", color);
1632        assert!(debug.contains("AnimColor"));
1633    }
1634
1635    // =========================================================================
1636    // AnimationController Additional Tests
1637    // =========================================================================
1638
1639    #[test]
1640    fn test_controller_default() {
1641        let controller = AnimationController::default();
1642        assert!(!controller.is_animating());
1643    }
1644
1645    #[test]
1646    fn test_controller_set_target_nonexistent() {
1647        let mut controller = AnimationController::new();
1648        controller.set_target("nonexistent", 100.0); // Should not panic
1649    }
1650
1651    #[test]
1652    fn test_controller_mixed_animations() {
1653        let mut controller = AnimationController::new();
1654        controller.add_spring("spring", 0.0, SpringConfig::STIFF);
1655        controller.add_eased("eased", 0.0, 100.0, 0.5, Easing::Linear);
1656
1657        controller.set_target("spring", 100.0);
1658        controller.update(0.25);
1659
1660        assert!(controller.is_animating());
1661        // Both should have values
1662        assert!(controller.get("spring").is_some());
1663        assert!(controller.get("eased").is_some());
1664    }
1665
1666    #[test]
1667    fn test_controller_eased_completes() {
1668        let mut controller = AnimationController::new();
1669        controller.add_eased("fade", 0.0, 1.0, 0.5, Easing::Linear);
1670        controller.update(0.5);
1671        assert!(!controller.is_animating()); // Should be complete
1672    }
1673
1674    #[test]
1675    fn test_controller_debug() {
1676        let controller = AnimationController::new();
1677        let debug = format!("{:?}", controller);
1678        assert!(debug.contains("AnimationController"));
1679    }
1680
1681    // =========================================================================
1682    // Interpolate Additional Tests
1683    // =========================================================================
1684
1685    #[test]
1686    fn test_interpolate_f64_boundaries() {
1687        assert!((f64::interpolate(&0.0, &100.0, 0.0) - 0.0).abs() < 0.001);
1688        assert!((f64::interpolate(&0.0, &100.0, 1.0) - 100.0).abs() < 0.001);
1689    }
1690
1691    #[test]
1692    fn test_interpolate_f32_negative() {
1693        let result = f32::interpolate(&-50.0, &50.0, 0.5);
1694        assert!((result - 0.0).abs() < 0.001);
1695    }
1696
1697    #[test]
1698    fn test_interpolate_point_negative() {
1699        let from = Point {
1700            x: -100.0,
1701            y: -100.0,
1702        };
1703        let to = Point { x: 100.0, y: 100.0 };
1704        let result = Point::interpolate(&from, &to, 0.5);
1705        assert!((result.x - 0.0).abs() < 0.001);
1706        assert!((result.y - 0.0).abs() < 0.001);
1707    }
1708
1709    #[test]
1710    fn test_interpolate_color_boundaries() {
1711        let result_start = AnimColor::interpolate(&AnimColor::BLACK, &AnimColor::WHITE, 0.0);
1712        assert!((result_start.r - 0.0).abs() < 0.001);
1713
1714        let result_end = AnimColor::interpolate(&AnimColor::BLACK, &AnimColor::WHITE, 1.0);
1715        assert!((result_end.r - 1.0).abs() < 0.001);
1716    }
1717}