1#![allow(clippy::unwrap_used, clippy::disallowed_methods)]
2use crate::geometry::Point;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum Easing {
16 #[default]
18 Linear,
19 EaseIn,
21 EaseOut,
23 EaseInOut,
25 CubicIn,
27 CubicOut,
29 CubicInOut,
31 ExpoIn,
33 ExpoOut,
35 ElasticOut,
37 BounceOut,
39 BackOut,
41}
42
43impl Easing {
44 #[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#[derive(Debug, Clone, Copy, PartialEq)]
154pub struct SpringConfig {
155 pub mass: f64,
157 pub stiffness: f64,
159 pub damping: f64,
161}
162
163impl Default for SpringConfig {
164 fn default() -> Self {
165 Self::GENTLE
166 }
167}
168
169impl SpringConfig {
170 pub const GENTLE: Self = Self {
172 mass: 1.0,
173 stiffness: 100.0,
174 damping: 15.0,
175 };
176
177 pub const WOBBLY: Self = Self {
179 mass: 1.0,
180 stiffness: 180.0,
181 damping: 12.0,
182 };
183
184 pub const STIFF: Self = Self {
186 mass: 1.0,
187 stiffness: 400.0,
188 damping: 30.0,
189 };
190
191 pub const MOLASSES: Self = Self {
193 mass: 1.0,
194 stiffness: 50.0,
195 damping: 20.0,
196 };
197
198 #[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 #[must_use]
210 pub fn damping_ratio(&self) -> f64 {
211 self.damping / (2.0 * (self.mass * self.stiffness).sqrt())
212 }
213
214 #[must_use]
216 pub fn is_underdamped(&self) -> bool {
217 self.damping_ratio() < 1.0
218 }
219
220 #[must_use]
222 pub fn is_critically_damped(&self) -> bool {
223 (self.damping_ratio() - 1.0).abs() < 0.01
224 }
225
226 #[must_use]
228 pub fn is_overdamped(&self) -> bool {
229 self.damping_ratio() > 1.0
230 }
231}
232
233#[derive(Debug, Clone)]
239pub struct Spring {
240 pub value: f64,
242 pub target: f64,
244 pub velocity: f64,
246 pub config: SpringConfig,
248 pub at_rest: bool,
250 pub precision: f64,
252}
253
254impl Spring {
255 #[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 #[must_use]
270 pub fn with_config(mut self, config: SpringConfig) -> Self {
271 self.config = config;
272 self
273 }
274
275 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 pub fn update(&mut self, dt: f64) {
285 if self.at_rest {
286 return;
287 }
288
289 let displacement = self.value - self.target;
291 let spring_force = -self.config.stiffness * displacement;
292
293 let damping_force = -self.config.damping * self.velocity;
295
296 let acceleration = (spring_force + damping_force) / self.config.mass;
298
299 self.velocity += acceleration * dt;
301 self.value += self.velocity * dt;
302
303 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 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#[derive(Debug, Clone)]
326pub enum AnimatedValue {
327 Eased(EasedValue),
329 Spring(Spring),
331}
332
333impl AnimatedValue {
334 #[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 #[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 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#[derive(Debug, Clone)]
363pub struct EasedValue {
364 pub from: f64,
366 pub to: f64,
368 pub duration: f64,
370 pub elapsed: f64,
372 pub easing: Easing,
374}
375
376impl EasedValue {
377 #[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 #[must_use]
391 pub fn with_easing(mut self, easing: Easing) -> Self {
392 self.easing = easing;
393 self
394 }
395
396 #[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 #[must_use]
410 pub fn is_complete(&self) -> bool {
411 self.elapsed >= self.duration
412 }
413
414 pub fn update(&mut self, dt: f64) {
416 self.elapsed = (self.elapsed + dt).min(self.duration);
417 }
418
419 #[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#[derive(Debug, Clone)]
436pub struct Keyframe<T: Clone> {
437 pub time: f64,
439 pub value: T,
441 pub easing: Easing,
443}
444
445impl<T: Clone> Keyframe<T> {
446 #[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 #[must_use]
458 pub fn with_easing(mut self, easing: Easing) -> Self {
459 self.easing = easing;
460 self
461 }
462}
463
464#[derive(Debug, Clone)]
466pub struct KeyframeTrack<T: Clone + Interpolate> {
467 keyframes: Vec<Keyframe<T>>,
469 pub duration: f64,
471 pub elapsed: f64,
473 pub looping: bool,
475}
476
477impl<T: Clone + Interpolate> KeyframeTrack<T> {
478 #[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 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 #[must_use]
498 pub fn with_loop(mut self, looping: bool) -> Self {
499 self.looping = looping;
500 self
501 }
502
503 #[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 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 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 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 #[must_use]
565 pub fn is_complete(&self) -> bool {
566 !self.looping && self.elapsed >= self.duration
567 }
568
569 pub fn reset(&mut self) {
571 self.elapsed = 0.0;
572 }
573}
574
575pub trait Interpolate {
581 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#[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#[derive(Debug, Default)]
659pub struct AnimationController {
660 springs: HashMap<String, Spring>,
662 eased: HashMap<String, EasedValue>,
664 active_count: usize,
666}
667
668impl AnimationController {
669 #[must_use]
671 pub fn new() -> Self {
672 Self::default()
673 }
674
675 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 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 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 #[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 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 #[must_use]
727 pub fn is_animating(&self) -> bool {
728 self.active_count > 0
729 }
730
731 #[must_use]
733 pub fn active_count(&self) -> usize {
734 self.active_count
735 }
736
737 pub fn remove(&mut self, name: &str) {
739 self.springs.remove(name);
740 self.eased.remove(name);
741 }
742
743 pub fn clear(&mut self) {
745 self.springs.clear();
746 self.eased.clear();
747 self.active_count = 0;
748 }
749}
750
751#[cfg(test)]
756mod tests {
757 use super::*;
758
759 #[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); }
781
782 #[test]
783 fn test_easing_ease_out() {
784 let val = Easing::EaseOut.apply(0.5);
785 assert!(val > 0.5); }
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); }
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 #[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 let underdamped = SpringConfig::custom(1.0, 100.0, 5.0);
845 assert!(underdamped.is_underdamped());
846
847 let overdamped = SpringConfig::custom(1.0, 100.0, 50.0);
849 assert!(overdamped.is_overdamped());
850 }
851
852 #[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 for _ in 0..100 {
879 spring.update(1.0 / 60.0); }
881
882 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 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 #[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); 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 #[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 #[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); }
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 #[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 #[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 #[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 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 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 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 let val_mid = Easing::BackOut.apply(0.5);
1248 assert!(val_mid > 0.5); }
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 #[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 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 #[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); assert!(spring.at_rest); }
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); 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; spring.set_target(0.05); spring.update(0.016);
1357 }
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 #[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); 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); assert!(eased.elapsed <= eased.duration);
1402 assert!(eased.value() >= 0.0 && eased.value() <= 100.0);
1404 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 #[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 #[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 #[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 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); track.elapsed = 0.75;
1521 let val = track.value().unwrap();
1522 assert!(val > 70.0 && val < 80.0); }
1524
1525 #[test]
1526 fn test_keyframe_track_keyframe_sorting() {
1527 let mut track: KeyframeTrack<f64> = KeyframeTrack::new(1.0);
1528 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 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); 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); assert!((track.elapsed - 1.0).abs() < 0.001); 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 #[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 #[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); }
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 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()); }
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 #[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}