1use std::time::Duration;
15
16use kurbo::{Point, Rect, Size};
17use peniko::Color;
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
25pub struct FrameTime(u64);
26
27impl FrameTime {
28 pub const ZERO: FrameTime = FrameTime(0);
30
31 pub const fn from_nanos(nanos: u64) -> Self {
33 FrameTime(nanos)
34 }
35
36 pub const fn as_nanos(self) -> u64 {
38 self.0
39 }
40
41 pub fn as_secs_f64(self) -> f64 {
43 self.0 as f64 / 1_000_000_000.0
44 }
45
46 pub fn saturating_sub(self, earlier: FrameTime) -> Duration {
50 Duration::from_nanos(self.0.saturating_sub(earlier.0))
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq)]
59pub enum Curve {
60 Linear,
62 EaseIn,
64 EaseOut,
66 EaseInOut,
68 Emphasized,
70 Cubic(f64, f64, f64, f64),
73}
74
75impl Curve {
76 pub fn transform(self, t: f64) -> f64 {
78 let t = t.clamp(0.0, 1.0);
79 match self {
80 Curve::Linear => t,
81 Curve::EaseIn => cubic_bezier(0.42, 0.0, 1.0, 1.0, t),
82 Curve::EaseOut => cubic_bezier(0.0, 0.0, 0.58, 1.0, t),
83 Curve::EaseInOut => cubic_bezier(0.42, 0.0, 0.58, 1.0, t),
84 Curve::Emphasized => cubic_bezier(0.2, 0.0, 0.0, 1.0, t),
85 Curve::Cubic(x1, y1, x2, y2) => cubic_bezier(x1, y1, x2, y2, t),
86 }
87 }
88
89 pub fn interval(self, start: f64, end: f64) -> SegmentedCurve {
98 SegmentedCurve {
99 start,
100 end,
101 inner: self,
102 }
103 }
104}
105
106#[derive(Clone, Copy, Debug, PartialEq)]
108pub struct SegmentedCurve {
109 start: f64,
110 end: f64,
111 inner: Curve,
112}
113
114impl SegmentedCurve {
115 pub fn transform(self, t: f64) -> f64 {
117 let t = t.clamp(0.0, 1.0);
118 if self.end <= self.start {
119 return if t < self.start { 0.0 } else { 1.0 };
121 }
122 if t <= self.start {
123 0.0
124 } else if t >= self.end {
125 1.0
126 } else {
127 let local = (t - self.start) / (self.end - self.start);
128 self.inner.transform(local)
129 }
130 }
131}
132
133fn cubic_bezier(x1: f64, y1: f64, x2: f64, y2: f64, x: f64) -> f64 {
137 let cx = 3.0 * x1;
139 let bx = 3.0 * (x2 - x1) - cx;
140 let ax = 1.0 - cx - bx;
141 let cy = 3.0 * y1;
142 let by = 3.0 * (y2 - y1) - cy;
143 let ay = 1.0 - cy - by;
144
145 let sample_x = |s: f64| ((ax * s + bx) * s + cx) * s;
146 let sample_y = |s: f64| ((ay * s + by) * s + cy) * s;
147 let sample_dx = |s: f64| (3.0 * ax * s + 2.0 * bx) * s + cx;
148
149 let mut s = x;
151 for _ in 0..8 {
152 let err = sample_x(s) - x;
153 if err.abs() < 1e-9 {
154 return sample_y(s);
155 }
156 let d = sample_dx(s);
157 if d.abs() < 1e-9 {
158 break;
159 }
160 s -= err / d;
161 }
162
163 let (mut lo, mut hi) = (0.0_f64, 1.0_f64);
165 let mut s = x.clamp(lo, hi);
166 for _ in 0..32 {
167 let cur = sample_x(s);
168 if (cur - x).abs() < 1e-9 {
169 break;
170 }
171 if cur < x {
172 lo = s;
173 } else {
174 hi = s;
175 }
176 s = 0.5 * (lo + hi);
177 }
178 sample_y(s)
179}
180
181#[derive(Clone, Copy, Debug, PartialEq)]
187pub struct StaggerSpec {
188 pub per_item_delay: f64,
190 pub item_duration: f64,
192 pub item_curve: Curve,
194}
195
196impl StaggerSpec {
197 pub fn total_duration(&self, n: usize) -> f64 {
200 if n == 0 {
201 return 0.0;
202 }
203 (n as f64 - 1.0) * self.per_item_delay + self.item_duration
204 }
205
206 pub fn item_progress(&self, overall: f64, i: usize, n: usize) -> f64 {
213 let total = self.total_duration(n);
214 if total <= 0.0 || i >= n {
215 return 1.0;
216 }
217 let overall = overall.clamp(0.0, 1.0);
218 let elapsed = overall * total;
219 let item_start = i as f64 * self.per_item_delay;
220 let item_end = item_start + self.item_duration;
221 if self.item_duration <= 0.0 {
222 return if elapsed < item_start { 0.0 } else { 1.0 };
223 }
224 if elapsed <= item_start {
225 0.0
226 } else if elapsed >= item_end {
227 1.0
228 } else {
229 let local = (elapsed - item_start) / self.item_duration;
230 self.item_curve.transform(local)
231 }
232 }
233}
234
235pub trait Lerp {
240 fn lerp(&self, other: &Self, t: f64) -> Self;
242}
243
244impl Lerp for f64 {
245 fn lerp(&self, other: &Self, t: f64) -> Self {
246 self + (other - self) * t
247 }
248}
249
250impl Lerp for Point {
251 fn lerp(&self, other: &Self, t: f64) -> Self {
252 Point::new(self.x.lerp(&other.x, t), self.y.lerp(&other.y, t))
253 }
254}
255
256impl Lerp for Size {
257 fn lerp(&self, other: &Self, t: f64) -> Self {
258 Size::new(
259 self.width.lerp(&other.width, t),
260 self.height.lerp(&other.height, t),
261 )
262 }
263}
264
265impl Lerp for Rect {
266 fn lerp(&self, other: &Self, t: f64) -> Self {
267 Rect::new(
268 self.x0.lerp(&other.x0, t),
269 self.y0.lerp(&other.y0, t),
270 self.x1.lerp(&other.x1, t),
271 self.y1.lerp(&other.y1, t),
272 )
273 }
274}
275
276impl Lerp for Color {
277 fn lerp(&self, other: &Self, t: f64) -> Self {
279 let a = self.components;
280 let b = other.components;
281 let t = t as f32;
282 Color::new([
283 a[0] + (b[0] - a[0]) * t,
284 a[1] + (b[1] - a[1]) * t,
285 a[2] + (b[2] - a[2]) * t,
286 a[3] + (b[3] - a[3]) * t,
287 ])
288 }
289}
290
291#[derive(Clone, Copy, Debug, PartialEq)]
293pub struct Tween<T> {
294 pub begin: T,
296 pub end: T,
298}
299
300impl<T: Lerp> Tween<T> {
301 pub fn new(begin: T, end: T) -> Self {
303 Tween { begin, end }
304 }
305
306 pub fn lerp(&self, t: f64) -> T {
309 self.begin.lerp(&self.end, t)
310 }
311}
312
313#[derive(Clone, Copy, Debug, PartialEq)]
318pub struct SpringDesc {
319 pub mass: f64,
321 pub stiffness: f64,
323 pub damping_ratio: f64,
325}
326
327#[derive(Clone, Copy, Debug)]
334pub struct Spring {
335 kind: SpringKind,
336}
337
338#[derive(Clone, Copy, Debug)]
339enum SpringKind {
340 Under {
342 w0: f64,
343 wd: f64,
344 zeta: f64,
345 a: f64,
346 b: f64,
347 },
348 Critical { w0: f64, a: f64, b: f64 },
350 Over { r1: f64, r2: f64, c1: f64, c2: f64 },
352}
353
354impl Spring {
355 pub fn new(desc: SpringDesc, x0: f64, v0: f64) -> Self {
360 let mass = if desc.mass > 0.0 { desc.mass } else { 1.0 };
361 let stiffness = if desc.stiffness > 0.0 {
362 desc.stiffness
363 } else {
364 1.0
365 };
366 let zeta = desc.damping_ratio.max(0.0);
367 let w0 = (stiffness / mass).sqrt();
368
369 let kind = if (zeta - 1.0).abs() < 1e-6 {
370 let a = x0;
371 let b = v0 + w0 * x0;
372 SpringKind::Critical { w0, a, b }
373 } else if zeta < 1.0 {
374 let wd = w0 * (1.0 - zeta * zeta).sqrt();
375 let a = x0;
376 let b = (v0 + zeta * w0 * x0) / wd;
377 SpringKind::Under { w0, wd, zeta, a, b }
378 } else {
379 let s = (zeta * zeta - 1.0).sqrt();
380 let r1 = -w0 * (zeta - s);
381 let r2 = -w0 * (zeta + s);
382 let c1 = (v0 - r2 * x0) / (r1 - r2);
383 let c2 = x0 - c1;
384 SpringKind::Over { r1, r2, c1, c2 }
385 };
386 Spring { kind }
387 }
388
389 pub fn position(&self, t: f64) -> f64 {
391 match self.kind {
392 SpringKind::Under {
393 w0, wd, zeta, a, b, ..
394 } => {
395 let e = (-zeta * w0 * t).exp();
396 e * (a * (wd * t).cos() + b * (wd * t).sin())
397 }
398 SpringKind::Critical { w0, a, b } => {
399 let e = (-w0 * t).exp();
400 e * (a + b * t)
401 }
402 SpringKind::Over { r1, r2, c1, c2 } => c1 * (r1 * t).exp() + c2 * (r2 * t).exp(),
403 }
404 }
405
406 pub fn velocity(&self, t: f64) -> f64 {
408 match self.kind {
409 SpringKind::Under {
410 w0, wd, zeta, a, b, ..
411 } => {
412 let e = (-zeta * w0 * t).exp();
413 let c = (wd * t).cos();
414 let s = (wd * t).sin();
415 e * ((b * wd - zeta * w0 * a) * c - (a * wd + zeta * w0 * b) * s)
416 }
417 SpringKind::Critical { w0, a, b } => {
418 let e = (-w0 * t).exp();
419 e * (b - w0 * (a + b * t))
420 }
421 SpringKind::Over { r1, r2, c1, c2 } => {
422 r1 * c1 * (r1 * t).exp() + r2 * c2 * (r2 * t).exp()
423 }
424 }
425 }
426
427 pub fn is_at_rest(&self, t: f64, epsilon: f64) -> bool {
429 self.position(t).abs() < epsilon && self.velocity(t).abs() < epsilon
430 }
431}
432
433#[derive(Clone, Copy, Debug, PartialEq, Eq)]
435pub enum AnimationStatus {
436 Idle,
438 Forward,
440 Reverse,
442 Completed,
444 Dismissed,
446}
447
448const SPRING_REST_EPSILON: f64 = 1e-3;
450
451#[derive(Clone, Copy, Debug)]
464pub struct AnimationController {
465 value: f64,
466 duration: Duration,
467 curve: Curve,
468 status: AnimationStatus,
469 drive: Drive,
470 last_time: Option<FrameTime>,
471}
472
473#[derive(Clone, Copy, Debug)]
475enum Drive {
476 Idle,
478 Duration {
480 start: f64,
481 target: f64,
482 elapsed: f64,
483 duration: f64,
484 },
485 Repeat { elapsed: f64, period: f64 },
487 Fling {
489 spring: Spring,
490 target: f64,
491 elapsed: f64,
492 },
493}
494
495impl AnimationController {
496 pub fn new(duration: Duration) -> Self {
498 AnimationController {
499 value: 0.0,
500 duration,
501 curve: Curve::Linear,
502 status: AnimationStatus::Idle,
503 drive: Drive::Idle,
504 last_time: None,
505 }
506 }
507
508 pub fn with_curve(mut self, curve: Curve) -> Self {
510 self.curve = curve;
511 self
512 }
513
514 pub fn value(&self) -> f64 {
518 self.value
519 }
520
521 pub fn value_clamped(&self) -> f64 {
524 self.value.clamp(0.0, 1.0)
525 }
526
527 pub fn status(&self) -> AnimationStatus {
529 self.status
530 }
531
532 pub fn is_animating(&self) -> bool {
534 !matches!(self.drive, Drive::Idle)
535 }
536
537 pub fn forward(&mut self) {
539 self.start_duration(self.value, 1.0, AnimationStatus::Forward);
540 }
541
542 pub fn reverse(&mut self) {
544 self.start_duration(self.value, 0.0, AnimationStatus::Reverse);
545 }
546
547 pub fn animate_to(&mut self, target: f64) {
550 let target = target.clamp(0.0, 1.0);
551 let status = if target >= self.value {
552 AnimationStatus::Forward
553 } else {
554 AnimationStatus::Reverse
555 };
556 self.start_duration(self.value, target, status);
557 }
558
559 pub fn repeat(&mut self) {
562 let period = self.duration.as_secs_f64();
563 self.value = 0.0;
564 self.status = AnimationStatus::Forward;
565 self.last_time = None;
566 self.drive = if period > 0.0 {
567 Drive::Repeat {
568 elapsed: 0.0,
569 period,
570 }
571 } else {
572 Drive::Idle
574 };
575 }
576
577 pub fn fling(&mut self, velocity: f64, spring: SpringDesc) {
581 let target = if velocity >= 0.0 { 1.0 } else { 0.0 };
582 let x0 = self.value - target;
583 self.status = if target >= self.value {
584 AnimationStatus::Forward
585 } else {
586 AnimationStatus::Reverse
587 };
588 self.last_time = None;
589 self.drive = Drive::Fling {
590 spring: Spring::new(spring, x0, velocity),
591 target,
592 elapsed: 0.0,
593 };
594 }
595
596 pub fn stop(&mut self) {
599 self.drive = Drive::Idle;
600 self.status = AnimationStatus::Idle;
601 self.last_time = None;
602 }
603
604 pub fn advance(&mut self, now: FrameTime) -> bool {
611 let dt = match self.last_time {
612 Some(last) => now.saturating_sub(last).as_secs_f64(),
613 None => 0.0,
614 };
615 self.last_time = Some(now);
616 let dt = if dt.is_finite() { dt.max(0.0) } else { 0.0 };
618
619 match &mut self.drive {
620 Drive::Idle => false,
621 Drive::Duration {
622 start,
623 target,
624 elapsed,
625 duration,
626 } => {
627 let (start, target, duration) = (*start, *target, *duration);
628 *elapsed += dt;
629 let elapsed = *elapsed;
630 if duration <= 0.0 || elapsed >= duration {
631 self.value = target;
632 self.status = if target >= start {
633 AnimationStatus::Completed
634 } else {
635 AnimationStatus::Dismissed
636 };
637 self.drive = Drive::Idle;
638 false
639 } else {
640 let frac = (elapsed / duration).clamp(0.0, 1.0);
641 let eased = self.curve.transform(frac);
642 self.value = start + (target - start) * eased;
643 true
644 }
645 }
646 Drive::Repeat { elapsed, period } => {
647 let period = *period;
648 *elapsed += dt;
649 let frac = if period > 0.0 {
650 (*elapsed / period).rem_euclid(1.0)
651 } else {
652 0.0
653 };
654 self.value = self.curve.transform(frac);
655 true
656 }
657 Drive::Fling {
658 spring,
659 target,
660 elapsed,
661 } => {
662 let spring = *spring;
663 let target = *target;
664 *elapsed += dt;
665 let elapsed = *elapsed;
666 if spring.is_at_rest(elapsed, SPRING_REST_EPSILON) {
667 self.value = target;
668 self.status = if target >= 0.5 {
669 AnimationStatus::Completed
670 } else {
671 AnimationStatus::Dismissed
672 };
673 self.drive = Drive::Idle;
674 false
675 } else {
676 self.value = target + spring.position(elapsed);
678 true
679 }
680 }
681 }
682 }
683
684 fn start_duration(&mut self, start: f64, target: f64, status: AnimationStatus) {
686 let duration = self.duration.as_secs_f64();
687 self.status = status;
688 self.last_time = None;
689 if duration <= 0.0 || (target - start).abs() < f64::EPSILON {
690 self.value = target;
692 self.status = if target >= start {
693 AnimationStatus::Completed
694 } else {
695 AnimationStatus::Dismissed
696 };
697 self.drive = Drive::Idle;
698 } else {
699 self.drive = Drive::Duration {
700 start,
701 target,
702 elapsed: 0.0,
703 duration,
704 };
705 }
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712
713 fn ft_secs(s: f64) -> FrameTime {
714 FrameTime::from_nanos((s * 1_000_000_000.0) as u64)
715 }
716
717 #[test]
718 fn frame_time_differences_only() {
719 let a = FrameTime::from_nanos(1_000);
720 let b = FrameTime::from_nanos(3_500);
721 assert_eq!(b.saturating_sub(a), Duration::from_nanos(2_500));
722 assert_eq!(a.saturating_sub(b), Duration::ZERO);
724 assert!((ft_secs(2.0).as_secs_f64() - 2.0).abs() < 1e-9);
725 }
726
727 #[test]
728 fn ease_in_out_matches_css_reference_at_half() {
729 assert!((Curve::EaseInOut.transform(0.5) - 0.5).abs() < 1e-4);
731 assert!((Curve::EaseInOut.transform(0.0)).abs() < 1e-9);
733 assert!((Curve::EaseInOut.transform(1.0) - 1.0).abs() < 1e-9);
734 assert!(Curve::EaseIn.transform(0.5) < 0.5);
737 assert!(Curve::EaseOut.transform(0.5) > 0.5);
738 assert!((Curve::EaseIn.transform(0.5) + Curve::EaseOut.transform(0.5) - 1.0).abs() < 1e-4);
739 }
740
741 #[test]
742 fn linear_curve_is_identity() {
743 for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
744 assert!((Curve::Linear.transform(t) - t).abs() < 1e-12);
745 }
746 assert_eq!(Curve::Linear.transform(-1.0), 0.0);
748 assert_eq!(Curve::Linear.transform(2.0), 1.0);
749 }
750
751 #[test]
752 fn interval_endpoints_and_midpoint() {
753 let seg = Curve::EaseInOut.interval(0.3, 0.7);
754 assert_eq!(seg.transform(0.0), 0.0);
756 assert_eq!(seg.transform(0.3), 0.0);
757 assert_eq!(seg.transform(0.1), 0.0);
758 assert_eq!(seg.transform(0.7), 1.0);
760 assert_eq!(seg.transform(0.9), 1.0);
761 assert_eq!(seg.transform(1.0), 1.0);
762 let local_mid = 0.3 + 0.5 * (0.7 - 0.3);
765 assert!((seg.transform(local_mid) - 0.5).abs() < 1e-4);
766 let local_quarter = 0.3 + 0.25 * (0.7 - 0.3);
768 assert!((seg.transform(local_quarter) - Curve::EaseInOut.transform(0.25)).abs() < 1e-9);
769 }
770
771 #[test]
772 fn interval_linear_matches_flutter_fade_through_shape() {
773 let out_seg = Curve::Linear.interval(0.0, 0.3);
775 let in_seg = Curve::Linear.interval(0.3, 1.0);
776 assert_eq!(out_seg.transform(0.0), 0.0);
777 assert!((out_seg.transform(0.15) - 0.5).abs() < 1e-9);
778 assert_eq!(out_seg.transform(0.3), 1.0);
779 assert_eq!(in_seg.transform(0.3), 0.0);
780 assert!((in_seg.transform(0.65) - 0.5).abs() < 1e-9);
781 assert_eq!(in_seg.transform(1.0), 1.0);
782 }
783
784 #[test]
785 fn interval_degenerate_start_equals_end_is_an_instant_step() {
786 let seg = Curve::Linear.interval(0.3, 0.3);
787 assert_eq!(seg.transform(0.0), 0.0);
788 assert_eq!(seg.transform(0.2), 0.0);
789 assert_eq!(seg.transform(0.3), 1.0);
791 assert_eq!(seg.transform(0.5), 1.0);
792 assert_eq!(seg.transform(1.0), 1.0);
793 }
794
795 #[test]
796 fn interval_degenerate_inverted_is_an_instant_step_at_start() {
797 let seg = Curve::Linear.interval(0.6, 0.2);
799 assert_eq!(seg.transform(0.0), 0.0);
800 assert_eq!(seg.transform(0.59), 0.0);
801 assert_eq!(seg.transform(0.6), 1.0);
802 assert_eq!(seg.transform(1.0), 1.0);
803 }
804
805 #[test]
806 fn interval_out_of_range_t_clamps_first() {
807 let seg = Curve::Linear.interval(0.3, 0.7);
808 assert_eq!(seg.transform(-1.0), 0.0);
809 assert_eq!(seg.transform(2.0), 1.0);
810 }
811
812 fn glyph_stagger() -> StaggerSpec {
815 StaggerSpec {
816 per_item_delay: 90.0,
817 item_duration: 150.0,
818 item_curve: Curve::Linear,
819 }
820 }
821
822 #[test]
823 fn stagger_total_duration_spans_last_items_window() {
824 let spec = glyph_stagger();
825 assert_eq!(spec.total_duration(5), 510.0);
827 assert_eq!(spec.total_duration(1), 150.0);
828 assert_eq!(spec.total_duration(0), 0.0);
829 }
830
831 #[test]
832 fn stagger_item_progress_hand_computed_table() {
833 let spec = glyph_stagger();
834 let n = 5;
835 let total = spec.total_duration(n); for i in 0..n {
840 assert_eq!(spec.item_progress(0.0, i, n), 0.0, "i={i} at overall=0.0");
841 }
842
843 for i in 0..n {
845 assert_eq!(spec.item_progress(1.0, i, n), 1.0, "i={i} at overall=1.0");
846 }
847
848 let overall_270 = 270.0 / total;
851 assert_eq!(spec.item_progress(overall_270, 0, n), 1.0);
852 assert_eq!(spec.item_progress(overall_270, 1, n), 1.0);
853 assert!((spec.item_progress(overall_270, 2, n) - 0.6).abs() < 1e-9);
854 assert_eq!(spec.item_progress(overall_270, 3, n), 0.0);
855 assert_eq!(spec.item_progress(overall_270, 4, n), 0.0);
856
857 let overall_45 = 45.0 / total;
859 assert!((spec.item_progress(overall_45, 0, n) - 0.3).abs() < 1e-9);
860 assert_eq!(spec.item_progress(overall_45, 1, n), 0.0);
861
862 let overall_135 = 135.0 / total;
865 assert!((spec.item_progress(overall_135, 0, n) - 0.9).abs() < 1e-9);
866 assert!((spec.item_progress(overall_135, 1, n) - 0.3).abs() < 1e-9);
867 }
868
869 #[test]
870 fn stagger_item_progress_out_of_range_index_or_empty_set_is_complete() {
871 let spec = glyph_stagger();
872 assert_eq!(spec.item_progress(0.5, 5, 5), 1.0);
874 assert_eq!(spec.item_progress(0.5, 0, 0), 1.0);
876 }
877
878 #[test]
879 fn stagger_zero_duration_item_is_an_instant_step() {
880 let spec = StaggerSpec {
881 per_item_delay: 100.0,
882 item_duration: 0.0,
883 item_curve: Curve::Linear,
884 };
885 let n = 3;
886 let total = spec.total_duration(n); assert_eq!(total, 200.0);
888 assert_eq!(spec.item_progress(99.0 / total, 1, n), 0.0);
890 assert_eq!(spec.item_progress(100.0 / total, 1, n), 1.0);
891 }
892
893 #[test]
894 fn tween_interpolates_value_types() {
895 assert!((Tween::new(0.0_f64, 10.0).lerp(0.25) - 2.5).abs() < 1e-12);
896 assert_eq!(
897 Tween::new(Point::new(0.0, 0.0), Point::new(4.0, 8.0)).lerp(0.5),
898 Point::new(2.0, 4.0)
899 );
900 assert_eq!(
901 Tween::new(Size::new(0.0, 0.0), Size::new(10.0, 20.0)).lerp(0.1),
902 Size::new(1.0, 2.0)
903 );
904 assert_eq!(
905 Tween::new(Rect::new(0.0, 0.0, 2.0, 2.0), Rect::new(2.0, 2.0, 6.0, 6.0)).lerp(0.5),
906 Rect::new(1.0, 1.0, 4.0, 4.0)
907 );
908 let c = Tween::new(Color::BLACK, Color::WHITE).lerp(0.5);
909 for ch in &c.components[..3] {
910 assert!((ch - 0.5).abs() < 1e-6);
911 }
912 }
913
914 fn integrate_spring(desc: SpringDesc, x0: f64, v0: f64, t_end: f64) -> f64 {
916 let m = desc.mass;
917 let k = desc.stiffness;
918 let c = 2.0 * desc.damping_ratio * (k * m).sqrt();
919 let accel = |x: f64, v: f64| -(k * x + c * v) / m;
920 let dt = 1e-5;
921 let steps = (t_end / dt).round() as usize;
922 let (mut x, mut v) = (x0, v0);
923 for _ in 0..steps {
924 let (k1x, k1v) = (v, accel(x, v));
925 let (k2x, k2v) = (
926 v + 0.5 * dt * k1v,
927 accel(x + 0.5 * dt * k1x, v + 0.5 * dt * k1v),
928 );
929 let (k3x, k3v) = (
930 v + 0.5 * dt * k2v,
931 accel(x + 0.5 * dt * k2x, v + 0.5 * dt * k2v),
932 );
933 let (k4x, k4v) = (v + dt * k3v, accel(x + dt * k3x, v + dt * k3v));
934 x += dt / 6.0 * (k1x + 2.0 * k2x + 2.0 * k3x + k4x);
935 v += dt / 6.0 * (k1v + 2.0 * k2v + 2.0 * k3v + k4v);
936 }
937 x
938 }
939
940 #[test]
941 fn spring_analytic_matches_numeric_under_critical_over() {
942 let cases = [
943 SpringDesc {
945 mass: 1.0,
946 stiffness: 700.0,
947 damping_ratio: 0.9,
948 },
949 SpringDesc {
950 mass: 1.0,
951 stiffness: 700.0,
952 damping_ratio: 1.0,
953 },
954 SpringDesc {
955 mass: 1.0,
956 stiffness: 700.0,
957 damping_ratio: 1.5,
958 },
959 ];
960 for desc in cases {
961 let spring = Spring::new(desc, 1.0, 0.0);
962 for &t in &[0.005, 0.01, 0.02, 0.03] {
963 let analytic = spring.position(t);
964 let numeric = integrate_spring(desc, 1.0, 0.0, t);
965 assert!(
966 (analytic - numeric).abs() < 1e-6,
967 "regime ζ={} at t={t}: analytic {analytic} vs numeric {numeric}",
968 desc.damping_ratio
969 );
970 }
971 }
972 }
973
974 #[test]
975 fn spring_velocity_matches_finite_difference() {
976 let desc = SpringDesc {
977 mass: 1.0,
978 stiffness: 700.0,
979 damping_ratio: 0.9,
980 };
981 let spring = Spring::new(desc, 1.0, 0.0);
982 let t = 0.01;
983 let h = 1e-7;
984 let fd = (spring.position(t + h) - spring.position(t - h)) / (2.0 * h);
985 assert!((spring.velocity(t) - fd).abs() < 1e-4);
986 }
987
988 #[test]
989 fn duration_forward_reaches_completed() {
990 let mut c = AnimationController::new(Duration::from_millis(100));
991 c.forward();
992 assert_eq!(c.status(), AnimationStatus::Forward);
993 assert!(c.advance(ft_secs(0.0)));
995 assert!(c.advance(ft_secs(0.05)));
996 assert!((c.value() - 0.5).abs() < 1e-6);
997 assert!(!c.advance(ft_secs(0.2)));
999 assert_eq!(c.value(), 1.0);
1000 assert_eq!(c.status(), AnimationStatus::Completed);
1001 }
1002
1003 #[test]
1004 fn reverse_reaches_dismissed() {
1005 let mut c = AnimationController::new(Duration::from_millis(100));
1006 c.forward();
1008 c.advance(ft_secs(0.0));
1009 c.advance(ft_secs(0.2));
1010 assert_eq!(c.value(), 1.0);
1011
1012 c.reverse();
1013 assert_eq!(c.status(), AnimationStatus::Reverse);
1014 c.advance(ft_secs(1.0));
1015 assert!(!c.advance(ft_secs(1.2)));
1016 assert_eq!(c.value(), 0.0);
1017 assert_eq!(c.status(), AnimationStatus::Dismissed);
1018 }
1019
1020 #[test]
1021 fn repeat_wraps_and_never_completes() {
1022 let mut c = AnimationController::new(Duration::from_secs(1));
1023 c.repeat();
1024 assert!(c.advance(ft_secs(0.0)));
1025 assert!(c.advance(ft_secs(0.5)));
1026 assert!((c.value() - 0.5).abs() < 1e-6);
1027 assert!(c.advance(ft_secs(1.5)));
1029 assert!((c.value() - 0.5).abs() < 1e-6);
1030 assert!(c.advance(ft_secs(2.0)));
1031 assert!(c.value() < 1e-6);
1032 assert!(c.advance(ft_secs(100.0)));
1034 }
1035
1036 #[test]
1037 fn animate_to_partial_target() {
1038 let mut c = AnimationController::new(Duration::from_millis(100));
1039 c.animate_to(0.3);
1040 assert_eq!(c.status(), AnimationStatus::Forward);
1041 c.advance(ft_secs(0.0));
1042 assert!(!c.advance(ft_secs(0.2)));
1043 assert!((c.value() - 0.3).abs() < 1e-6);
1044 assert_eq!(c.status(), AnimationStatus::Completed);
1045 }
1046
1047 #[test]
1048 fn fling_settles_at_target() {
1049 let mut c = AnimationController::new(Duration::from_millis(100));
1050 c.fling(
1051 2.0,
1052 SpringDesc {
1053 mass: 1.0,
1054 stiffness: 700.0,
1055 damping_ratio: 0.9,
1056 },
1057 );
1058 let mut t = 0.0;
1059 let mut running = true;
1060 for _ in 0..100_000 {
1062 running = c.advance(ft_secs(t));
1063 if !running {
1064 break;
1065 }
1066 t += 1.0 / 120.0;
1067 }
1068 assert!(!running, "fling failed to settle");
1069 assert!((c.value() - 1.0).abs() < 1e-6);
1070 assert_eq!(c.status(), AnimationStatus::Completed);
1071 }
1072
1073 #[test]
1074 fn fling_overshoots_past_target_for_underdamped_spring() {
1075 let desc = SpringDesc {
1079 mass: 1.0,
1080 stiffness: 700.0,
1081 damping_ratio: 0.9,
1082 };
1083 let mut c = AnimationController::new(Duration::from_millis(100));
1084 c.fling(0.0, desc);
1085
1086 let mut max_value = f64::MIN;
1090 let mut t = 0.0;
1091 let mut running = true;
1092 for _ in 0..100_000 {
1093 running = c.advance(ft_secs(t));
1094 if !running {
1095 break;
1096 }
1097 max_value = max_value.max(c.value());
1098 t += 1.0 / 120.0;
1099 }
1100 assert!(!running, "fling failed to settle");
1101 assert!(
1102 max_value > 1.0 + 1e-3,
1103 "expected a demonstrable overshoot past 1.0, got max {max_value}"
1104 );
1105 assert_eq!(c.value(), 1.0);
1106 assert_eq!(c.status(), AnimationStatus::Completed);
1107 }
1108
1109 #[test]
1110 fn fling_overshoot_values_match_analytic_spring() {
1111 let desc = SpringDesc {
1112 mass: 1.0,
1113 stiffness: 700.0,
1114 damping_ratio: 0.9,
1115 };
1116 let mut c = AnimationController::new(Duration::from_millis(100));
1117 c.fling(0.0, desc);
1118 let spring = Spring::new(desc, -1.0, 0.0);
1119
1120 assert!(c.advance(ft_secs(0.0)));
1123 for &t in &[0.01, 0.02, 0.03, 0.05, 0.08] {
1124 assert!(c.advance(ft_secs(t)));
1125 let expected = 1.0 + spring.position(t);
1126 assert!(
1127 (c.value() - expected).abs() < 1e-6,
1128 "at t={t}: controller {} vs analytic {expected}",
1129 c.value()
1130 );
1131 }
1132 }
1133
1134 #[test]
1135 fn effects_spring_never_exceeds_target() {
1136 let desc = SpringDesc {
1140 mass: 1.0,
1141 stiffness: 1600.0,
1142 damping_ratio: 1.0,
1143 };
1144 let mut c = AnimationController::new(Duration::from_millis(100));
1145 c.fling(0.0, desc);
1146
1147 let mut t = 0.0;
1148 let mut running = true;
1149 for _ in 0..100_000 {
1150 running = c.advance(ft_secs(t));
1151 assert!(
1152 c.value() <= 1.0 + 1e-9,
1153 "critically damped spring released from rest overshot: value {} at t={t}",
1154 c.value()
1155 );
1156 if !running {
1157 break;
1158 }
1159 t += 1.0 / 120.0;
1160 }
1161 assert!(!running, "fling failed to settle");
1162 assert_eq!(c.value(), 1.0);
1163 assert_eq!(c.status(), AnimationStatus::Completed);
1164 }
1165
1166 #[test]
1167 fn value_clamped_bounds_an_overshooting_fling() {
1168 let desc = SpringDesc {
1169 mass: 1.0,
1170 stiffness: 700.0,
1171 damping_ratio: 0.9,
1172 };
1173 let mut c = AnimationController::new(Duration::from_millis(100));
1174 c.fling(0.0, desc);
1175 c.advance(ft_secs(0.0));
1176 c.advance(ft_secs(0.02));
1177 assert!(c.value_clamped() >= 0.0 && c.value_clamped() <= 1.0);
1180 assert_eq!(c.value_clamped(), c.value().clamp(0.0, 1.0));
1181 }
1182
1183 #[test]
1184 fn advance_is_safe_on_equal_and_backward_timestamps() {
1185 let mut c = AnimationController::new(Duration::from_millis(100));
1186 c.forward();
1187 c.advance(ft_secs(0.05));
1188 let v_seed = c.value();
1189 c.advance(ft_secs(0.05));
1191 assert_eq!(c.value(), v_seed);
1192 assert!(c.value().is_finite());
1193 c.advance(ft_secs(0.01));
1195 assert_eq!(c.value(), v_seed);
1196 assert!(c.value().is_finite());
1197 }
1198
1199 #[test]
1200 fn stop_halts_progress() {
1201 let mut c = AnimationController::new(Duration::from_millis(100));
1202 c.forward();
1203 c.advance(ft_secs(0.0));
1204 c.advance(ft_secs(0.05));
1205 let v = c.value();
1206 c.stop();
1207 assert_eq!(c.status(), AnimationStatus::Idle);
1208 assert!(!c.advance(ft_secs(0.5)));
1209 assert_eq!(c.value(), v);
1210 }
1211}