1#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct Tolerance {
16 pub distance: f32,
18 pub velocity: f32,
20}
21
22impl Default for Tolerance {
23 fn default() -> Self {
24 Self {
25 distance: 1e-3,
26 velocity: 1e-3,
27 }
28 }
29}
30
31impl Tolerance {
32 pub const PIXELS: Tolerance = Tolerance {
35 distance: 0.5,
36 velocity: 2.0,
37 };
38}
39
40pub trait Simulation {
44 fn x(&self, time: f32) -> f32;
46 fn dx(&self, time: f32) -> f32;
48 fn is_done(&self, time: f32) -> bool;
50 fn tolerance(&self) -> Tolerance {
52 Tolerance::default()
53 }
54}
55
56#[derive(Clone, Copy, Debug, PartialEq)]
58pub struct SpringDescription {
59 pub mass: f32,
61 pub stiffness: f32,
63 pub damping: f32,
65}
66
67impl SpringDescription {
68 pub fn new(mass: f32, stiffness: f32, damping: f32) -> Self {
70 Self {
71 mass,
72 stiffness,
73 damping,
74 }
75 }
76
77 pub fn with_damping_ratio(mass: f32, stiffness: f32, ratio: f32) -> Self {
81 Self {
82 mass,
83 stiffness,
84 damping: ratio * 2.0 * (mass * stiffness).sqrt(),
85 }
86 }
87}
88
89#[derive(Clone, Copy, Debug)]
91enum Solution {
92 Critical { r: f32, c1: f32, c2: f32 },
95 Overdamped { r1: f32, r2: f32, c1: f32, c2: f32 },
97 Underdamped { w: f32, r: f32, c1: f32, c2: f32 },
99}
100
101impl Solution {
102 fn x(&self, t: f32) -> f32 {
103 match *self {
104 Solution::Critical { r, c1, c2 } => (c1 + c2 * t) * (r * t).exp(),
105 Solution::Overdamped { r1, r2, c1, c2 } => c1 * (r1 * t).exp() + c2 * (r2 * t).exp(),
106 Solution::Underdamped { w, r, c1, c2 } => {
107 (r * t).exp() * (c1 * (w * t).cos() + c2 * (w * t).sin())
108 }
109 }
110 }
111
112 fn dx(&self, t: f32) -> f32 {
113 match *self {
114 Solution::Critical { r, c1, c2 } => {
115 let power = (r * t).exp();
116 r * (c1 + c2 * t) * power + c2 * power
117 }
118 Solution::Overdamped { r1, r2, c1, c2 } => {
119 c1 * r1 * (r1 * t).exp() + c2 * r2 * (r2 * t).exp()
120 }
121 Solution::Underdamped { w, r, c1, c2 } => {
122 let power = (r * t).exp();
123 let cosine = (w * t).cos();
124 let sine = (w * t).sin();
125 power * (c2 * w * cosine - c1 * w * sine) + r * power * (c2 * sine + c1 * cosine)
126 }
127 }
128 }
129}
130
131#[derive(Clone, Copy, Debug)]
134pub struct SpringSimulation {
135 end: f32,
136 solution: Solution,
137 tolerance: Tolerance,
138}
139
140impl SpringSimulation {
141 pub fn new(
144 spring: SpringDescription,
145 start: f32,
146 end: f32,
147 velocity: f32,
148 tolerance: Tolerance,
149 ) -> Self {
150 let m = spring.mass;
151 let k = spring.stiffness;
152 let c = spring.damping;
153 let x0 = start - end;
154 let v0 = velocity;
155 let cmk = c * c - 4.0 * m * k;
156
157 let solution = if cmk.abs() <= 1e-4 * (4.0 * m * k).max(1.0) {
161 let r = -c / (2.0 * m);
162 Solution::Critical {
163 r,
164 c1: x0,
165 c2: v0 - r * x0,
166 }
167 } else if cmk > 0.0 {
168 let root = cmk.sqrt();
169 let r1 = (-c - root) / (2.0 * m);
170 let r2 = (-c + root) / (2.0 * m);
171 let c2 = (v0 - r1 * x0) / (r2 - r1);
172 Solution::Overdamped {
173 r1,
174 r2,
175 c1: x0 - c2,
176 c2,
177 }
178 } else {
179 let w = (4.0 * m * k - c * c).sqrt() / (2.0 * m);
180 let r = -c / (2.0 * m);
181 Solution::Underdamped {
182 w,
183 r,
184 c1: x0,
185 c2: (v0 - r * x0) / w,
186 }
187 };
188
189 Self {
190 end,
191 solution,
192 tolerance,
193 }
194 }
195}
196
197impl Simulation for SpringSimulation {
198 fn x(&self, time: f32) -> f32 {
199 self.end + self.solution.x(time)
200 }
201
202 fn dx(&self, time: f32) -> f32 {
203 self.solution.dx(time)
204 }
205
206 fn is_done(&self, time: f32) -> bool {
207 self.solution.x(time).abs() < self.tolerance.distance
208 && self.solution.dx(time).abs() < self.tolerance.velocity
209 }
210
211 fn tolerance(&self) -> Tolerance {
212 self.tolerance
213 }
214}
215
216fn sign_of(v: f32) -> f32 {
220 if v < 0.0 {
221 -1.0
222 } else {
223 1.0
224 }
225}
226
227fn newtons_method(guess: f32, target: f32, f: impl Fn(f32) -> f32, df: impl Fn(f32) -> f32) -> f32 {
232 let mut t = guess;
233 for _ in 0..10 {
234 let slope = df(t);
235 if slope == 0.0 {
236 break;
237 }
238 t -= (f(t) - target) / slope;
239 }
240 t
241}
242
243#[derive(Clone, Copy, Debug)]
247pub struct FrictionSimulation {
248 drag: f32,
249 drag_log: f32,
250 x0: f32,
251 v0: f32,
252 constant_deceleration: f32,
260 final_time: f32,
263 tolerance: Tolerance,
264}
265
266impl FrictionSimulation {
267 pub fn new(drag: f32, position: f32, velocity: f32, tolerance: Tolerance) -> Self {
269 Self::with_constant_deceleration(drag, position, velocity, tolerance, 0.0)
270 }
271
272 pub fn with_constant_deceleration(
284 drag: f32,
285 position: f32,
286 velocity: f32,
287 tolerance: Tolerance,
288 constant_deceleration: f32,
289 ) -> Self {
290 let drag_log = drag.ln();
291 let constant_deceleration = constant_deceleration * sign_of(velocity);
292 let mut sim = Self {
293 drag,
294 drag_log,
295 x0: position,
296 v0: velocity,
297 constant_deceleration,
298 final_time: f32::INFINITY,
301 tolerance,
302 };
303 if constant_deceleration != 0.0 {
304 sim.final_time = newtons_method(
305 0.0,
306 0.0,
307 |t| sim.dx(t),
308 |t| sim.v0 * sim.drag.powf(t) * sim.drag_log - sim.constant_deceleration,
309 );
310 }
311 sim
312 }
313
314 pub fn through(
318 start: f32,
319 end: f32,
320 start_velocity: f32,
321 end_velocity: f32,
322 tolerance: Tolerance,
323 ) -> Self {
324 let drag = ((start_velocity - end_velocity) / (start - end)).exp();
325 Self::new(drag, start, start_velocity, tolerance)
326 }
327
328 pub fn final_x(&self) -> f32 {
331 if self.constant_deceleration == 0.0 {
332 self.x0 - self.v0 / self.drag_log
333 } else {
334 self.x(self.final_time)
335 }
336 }
337
338 pub fn time_at_x(&self, x: f32) -> f32 {
343 if x == self.x0 {
344 return 0.0;
345 }
346 let unreachable = if self.v0 > 0.0 {
347 x < self.x0 || x > self.final_x()
348 } else {
349 x > self.x0 || x < self.final_x()
350 };
351 if self.v0 == 0.0 || unreachable {
352 return f32::INFINITY;
353 }
354 if self.constant_deceleration == 0.0 {
355 return (self.drag_log * (x - self.x0) / self.v0 + 1.0).ln() / self.drag_log;
356 }
357 newtons_method(0.0, x, |t| self.x(t), |t| self.dx(t))
358 }
359}
360
361impl Simulation for FrictionSimulation {
362 fn x(&self, time: f32) -> f32 {
363 if time > self.final_time {
364 return self.final_x();
365 }
366 self.x0 + (self.v0 / self.drag_log) * (self.drag.powf(time) - 1.0)
367 - 0.5 * self.constant_deceleration * time * time
368 }
369
370 fn dx(&self, time: f32) -> f32 {
371 if time > self.final_time {
372 return 0.0;
373 }
374 self.v0 * self.drag.powf(time) - self.constant_deceleration * time
375 }
376
377 fn is_done(&self, time: f32) -> bool {
378 self.dx(time).abs() < self.tolerance.velocity
379 }
380
381 fn tolerance(&self) -> Tolerance {
382 self.tolerance
383 }
384}
385
386#[derive(Clone, Copy, Debug)]
398pub struct ClampingScrollSimulation {
399 position: f32,
400 velocity: f32,
401 duration: f32,
403 distance: f32,
405 tolerance: Tolerance,
406}
407
408const DECELERATION_RATE: f32 = 2.358_201_8;
410const INFLEXION: f32 = 0.35;
412const PHYSICAL_COEFF: f32 = 9.806_65 * 39.37 * 160.0 * 0.84;
415pub const CLAMPING_FRICTION: f32 = 0.015;
418
419impl ClampingScrollSimulation {
420 pub fn new(position: f32, velocity: f32, tolerance: Tolerance) -> Self {
422 Self::with_friction(position, velocity, CLAMPING_FRICTION, tolerance)
423 }
424
425 pub fn with_friction(
428 position: f32,
429 velocity: f32,
430 friction: f32,
431 tolerance: Tolerance,
432 ) -> Self {
433 let duration = if velocity.abs() < tolerance.velocity.max(f32::EPSILON) {
436 0.0
437 } else {
438 let reference_velocity = friction * PHYSICAL_COEFF / INFLEXION;
439 let android_duration =
440 (velocity.abs() / reference_velocity).powf(1.0 / (DECELERATION_RATE - 1.0));
441 DECELERATION_RATE * INFLEXION * android_duration
444 };
445 Self {
446 position,
447 velocity,
448 duration,
449 distance: velocity * duration / DECELERATION_RATE,
450 tolerance,
451 }
452 }
453
454 pub fn duration(&self) -> f32 {
456 self.duration
457 }
458
459 pub fn final_x(&self) -> f32 {
461 self.position + self.distance
462 }
463
464 fn progress(&self, time: f32) -> f32 {
466 if self.duration <= 0.0 {
467 1.0
468 } else {
469 (time / self.duration).clamp(0.0, 1.0)
470 }
471 }
472}
473
474impl Simulation for ClampingScrollSimulation {
475 fn x(&self, time: f32) -> f32 {
476 let t = self.progress(time);
477 self.position + self.distance * (1.0 - (1.0 - t).powf(DECELERATION_RATE))
478 }
479
480 fn dx(&self, time: f32) -> f32 {
481 if self.duration <= 0.0 {
482 return 0.0;
483 }
484 let t = self.progress(time);
485 self.velocity * (1.0 - t).powf(DECELERATION_RATE - 1.0)
486 }
487
488 fn is_done(&self, time: f32) -> bool {
489 time >= self.duration
490 }
491
492 fn tolerance(&self) -> Tolerance {
493 self.tolerance
494 }
495}
496
497#[derive(Clone, Copy, Debug)]
508pub struct BouncingScrollSimulation {
509 friction: FrictionSimulation,
510 spring: SpringSimulation,
511 spring_time: f32,
514 tolerance: Tolerance,
515}
516
517pub const BOUNCING_DRAG: f32 = 0.135;
520
521pub const MAX_SPRING_TRANSFER_VELOCITY: f32 = 5000.0;
524
525impl BouncingScrollSimulation {
526 pub fn new(
529 position: f32,
530 velocity: f32,
531 leading: f32,
532 trailing: f32,
533 spring: SpringDescription,
534 tolerance: Tolerance,
535 ) -> Self {
536 Self::with_constant_deceleration(
537 position, velocity, leading, trailing, spring, tolerance, 0.0,
538 )
539 }
540
541 #[allow(clippy::too_many_arguments)]
548 pub fn with_constant_deceleration(
549 position: f32,
550 velocity: f32,
551 leading: f32,
552 trailing: f32,
553 spring: SpringDescription,
554 tolerance: Tolerance,
555 constant_deceleration: f32,
556 ) -> Self {
557 debug_assert!(leading <= trailing);
558 let friction = FrictionSimulation::with_constant_deceleration(
559 BOUNCING_DRAG,
560 position,
561 velocity,
562 tolerance,
563 constant_deceleration,
564 );
565 let to =
566 |edge: f32, from: f32, v: f32| SpringSimulation::new(spring, from, edge, v, tolerance);
567
568 if position < leading {
571 return Self {
572 friction,
573 spring: to(leading, position, velocity),
574 spring_time: f32::NEG_INFINITY,
575 tolerance,
576 };
577 }
578 if position > trailing {
579 return Self {
580 friction,
581 spring: to(trailing, position, velocity),
582 spring_time: f32::NEG_INFINITY,
583 tolerance,
584 };
585 }
586
587 let final_x = friction.final_x();
589 let (spring_time, spring) = if velocity > 0.0 && final_x > trailing {
590 let t = friction.time_at_x(trailing);
591 let carried = friction.dx(t).min(MAX_SPRING_TRANSFER_VELOCITY);
592 (t, to(trailing, trailing, carried))
593 } else if velocity < 0.0 && final_x < leading {
594 let t = friction.time_at_x(leading);
595 let carried = friction.dx(t).min(MAX_SPRING_TRANSFER_VELOCITY);
596 (t, to(leading, leading, carried))
597 } else {
598 (f32::INFINITY, to(position, position, 0.0))
601 };
602 Self {
603 friction,
604 spring,
605 spring_time,
606 tolerance,
607 }
608 }
609
610 fn phase(&self, time: f32) -> (&dyn Simulation, f32) {
613 if time > self.spring_time {
614 let offset = if self.spring_time.is_finite() {
615 self.spring_time
616 } else {
617 0.0
618 };
619 (&self.spring, offset)
620 } else {
621 (&self.friction, 0.0)
622 }
623 }
624}
625
626impl Simulation for BouncingScrollSimulation {
627 fn x(&self, time: f32) -> f32 {
628 let (sim, offset) = self.phase(time);
629 sim.x(time - offset)
630 }
631
632 fn dx(&self, time: f32) -> f32 {
633 let (sim, offset) = self.phase(time);
634 sim.dx(time - offset)
635 }
636
637 fn is_done(&self, time: f32) -> bool {
638 let (sim, offset) = self.phase(time);
639 sim.is_done(time - offset)
640 }
641
642 fn tolerance(&self) -> Tolerance {
643 self.tolerance
644 }
645}
646
647pub struct ClampedSimulation<S: Simulation> {
651 inner: S,
652 min: f32,
653 max: f32,
654}
655
656impl<S: Simulation> ClampedSimulation<S> {
657 pub fn new(inner: S, min: f32, max: f32) -> Self {
659 Self { inner, min, max }
660 }
661}
662
663impl<S: Simulation> Simulation for ClampedSimulation<S> {
664 fn x(&self, time: f32) -> f32 {
665 self.inner.x(time).clamp(self.min, self.max)
666 }
667
668 fn dx(&self, time: f32) -> f32 {
669 self.inner.dx(time)
670 }
671
672 fn is_done(&self, time: f32) -> bool {
673 self.inner.is_done(time)
674 }
675
676 fn tolerance(&self) -> Tolerance {
677 self.inner.tolerance()
678 }
679}
680
681#[cfg(test)]
682mod tests {
683 use super::*;
684
685 fn dx_matches_x<S: Simulation>(sim: &S, t: f32) {
688 let h = 1e-4;
689 let numeric = (sim.x(t + h) - sim.x(t - h)) / (2.0 * h);
690 let analytic = sim.dx(t);
691 let scale = analytic.abs().max(1.0);
694 assert!(
695 (numeric - analytic).abs() < 1e-2 * scale,
696 "inconsistent dx at t={t}: numeric={numeric}, analytic={analytic}"
697 );
698 }
699
700 #[test]
701 fn critical_spring_settles_without_overshoot() {
702 let spring = SpringDescription::with_damping_ratio(1.0, 500.0, 1.0);
703 let sim = SpringSimulation::new(spring, 0.0, 1.0, 0.0, Tolerance::default());
704 assert!((sim.x(0.0) - 0.0).abs() < 1e-5, "starts at start");
705 let mut prev = sim.x(0.0);
707 let mut t = 0.0;
708 while t < 2.0 {
709 let v = sim.x(t);
710 assert!(v <= 1.0 + 1e-4, "overshoot at t={t}: {v}");
711 assert!(v >= prev - 1e-4, "not monotonic at t={t}");
712 prev = v;
713 t += 0.01;
714 }
715 assert!((sim.x(2.0) - 1.0).abs() < 1e-2, "arrives at end");
716 assert!(sim.is_done(3.0), "at rest after 3 s");
717 dx_matches_x(&sim, 0.2);
718 }
719
720 #[test]
721 fn underdamped_spring_overshoots_then_returns() {
722 let spring = SpringDescription::with_damping_ratio(1.0, 500.0, 0.3);
723 let sim = SpringSimulation::new(spring, 0.0, 1.0, 0.0, Tolerance::default());
724 let mut max = f32::MIN;
726 let mut t = 0.0;
727 while t < 2.0 {
728 max = max.max(sim.x(t));
729 t += 0.005;
730 }
731 assert!(max > 1.05, "should overshoot the target, max={max}");
732 assert!((sim.x(3.0) - 1.0).abs() < 1e-2);
734 dx_matches_x(&sim, 0.1);
735 }
736
737 #[test]
738 fn overdamped_spring_is_slow_and_monotonic() {
739 let spring = SpringDescription::with_damping_ratio(1.0, 100.0, 2.5);
740 let sim = SpringSimulation::new(spring, 0.0, 1.0, 0.0, Tolerance::default());
741 let mut prev = sim.x(0.0);
742 let mut t = 0.0;
743 while t < 3.0 {
744 let v = sim.x(t);
745 assert!(v <= 1.0 + 1e-4, "no overshoot when overdamped, at t={t}");
746 assert!(v >= prev - 1e-4, "monotonic at t={t}");
747 prev = v;
748 t += 0.01;
749 }
750 dx_matches_x(&sim, 0.3);
751 }
752
753 #[test]
754 fn spring_honours_initial_velocity() {
755 let spring = SpringDescription::with_damping_ratio(1.0, 500.0, 1.0);
756 let sim = SpringSimulation::new(spring, 0.0, 1.0, 50.0, Tolerance::default());
757 assert!((sim.dx(0.0) - 50.0).abs() < 1e-2, "dx(0) = {}", sim.dx(0.0));
759 }
760
761 #[test]
762 fn friction_decelerates_to_finite_limit() {
763 let sim = FrictionSimulation::new(0.135, 0.0, 1000.0, Tolerance::PIXELS);
764 let limit = sim.final_x();
765 assert!(sim.x(0.0).abs() < 1e-4);
767 assert!(sim.x(10.0) <= limit + 1e-3);
768 assert!(
769 (sim.x(10.0) - limit).abs() < 1.0,
770 "close to the limit after 10 s"
771 );
772 assert!(sim.dx(0.0) > sim.dx(1.0));
774 assert!(sim.dx(5.0).abs() < sim.dx(0.0).abs());
775 dx_matches_x(&sim, 0.5);
776 }
777
778 #[test]
779 fn friction_through_hits_endpoints() {
780 let sim = FrictionSimulation::through(0.0, 200.0, 1000.0, 0.0, Tolerance::PIXELS);
782 assert!((sim.x(0.0)).abs() < 1e-3);
783 assert!(
784 (sim.final_x() - 200.0).abs() < 1e-1,
785 "final = {}",
786 sim.final_x()
787 );
788 }
789
790 #[test]
791 fn friction_time_at_x_inverts_x() {
792 let sim = FrictionSimulation::new(0.135, 0.0, 1000.0, Tolerance::PIXELS);
793 let t = sim.time_at_x(200.0);
794 assert!(t.is_finite(), "200 px is within reach of a 1000 px/s fling");
795 assert!(
796 (sim.x(t) - 200.0).abs() < 1e-2,
797 "x(time_at_x(200)) = {}",
798 sim.x(t)
799 );
800 assert!(sim.time_at_x(-10.0).is_infinite());
802 assert!(sim.time_at_x(sim.final_x() + 10.0).is_infinite());
803 assert_eq!(sim.time_at_x(0.0), 0.0);
804 }
805
806 #[test]
807 fn clamping_fling_matches_the_platform_distance() {
808 let sim = ClampingScrollSimulation::new(0.0, 1000.0, Tolerance::PIXELS);
809 assert!(
812 (sim.duration() - 0.458).abs() < 0.01,
813 "duration = {}",
814 sim.duration()
815 );
816 assert!(
817 (sim.final_x() - 194.3).abs() < 1.0,
818 "distance = {}",
819 sim.final_x()
820 );
821 let mut prev = sim.x(0.0);
823 let mut t = 0.0;
824 while t < sim.duration() {
825 let v = sim.x(t);
826 assert!(v >= prev - 1e-4, "not monotonic at t={t}");
827 prev = v;
828 t += 0.01;
829 }
830 assert!(
831 (sim.dx(0.0) - 1000.0).abs() < 1e-1,
832 "starts at the release velocity"
833 );
834 assert!(sim.dx(sim.duration()).abs() < 1e-3, "ends at rest");
835 assert!(!sim.is_done(sim.duration() * 0.5));
836 assert!(sim.is_done(sim.duration()));
837 dx_matches_x(&sim, 0.2);
838 }
839
840 #[test]
841 fn clamping_fling_is_ballistic() {
842 let sim = ClampingScrollSimulation::new(0.0, 1200.0, Tolerance::PIXELS);
846 let t = 0.15;
847 let restarted = ClampingScrollSimulation::new(sim.x(t), sim.dx(t), Tolerance::PIXELS);
848 assert!(
849 (restarted.final_x() - sim.final_x()).abs() < 1.0,
850 "restarted lands at {} instead of {}",
851 restarted.final_x(),
852 sim.final_x()
853 );
854 }
855
856 #[test]
857 fn clamping_fling_below_tolerance_does_not_move() {
858 let sim = ClampingScrollSimulation::new(42.0, 0.0, Tolerance::PIXELS);
859 assert_eq!(sim.duration(), 0.0);
860 assert_eq!(sim.x(0.0), 42.0);
861 assert_eq!(sim.dx(0.0), 0.0);
862 assert!(sim.is_done(0.0));
863 }
864
865 fn scroll_spring() -> SpringDescription {
867 SpringDescription::with_damping_ratio(0.5, 100.0, 1.1)
868 }
869
870 #[test]
871 fn bouncing_fling_springs_back_when_it_starts_outside() {
872 let sim = BouncingScrollSimulation::new(
874 130.0,
875 0.0,
876 0.0,
877 100.0,
878 scroll_spring(),
879 Tolerance::PIXELS,
880 );
881 assert!(
882 (sim.x(0.0) - 130.0).abs() < 1e-3,
883 "starts where it was let go"
884 );
885 assert!(sim.x(0.3) < 130.0, "comes back towards the edge");
886 assert!(
887 (sim.x(2.0) - 100.0).abs() < 0.5,
888 "settles on the edge, at {}",
889 sim.x(2.0)
890 );
891 assert!(sim.is_done(2.0));
892 }
893
894 #[test]
895 fn bouncing_fling_hands_friction_over_to_the_spring_at_the_edge() {
896 let sim = BouncingScrollSimulation::new(
898 0.0,
899 2000.0,
900 0.0,
901 100.0,
902 scroll_spring(),
903 Tolerance::PIXELS,
904 );
905 let friction = FrictionSimulation::new(0.135, 0.0, 2000.0, Tolerance::PIXELS);
906 let handover = friction.time_at_x(100.0);
907 assert!(handover.is_finite() && handover > 0.0);
908 let before = sim.x(handover - 1e-5);
912 let after = sim.x(handover + 1e-5);
913 assert!((before - 100.0).abs() < 0.1, "before = {before}");
914 assert!((after - 100.0).abs() < 0.1, "after = {after}");
915 assert!((after - before).abs() < 0.1, "jump at the hand-over");
916 let mut peak: f32 = 0.0;
918 let mut t = 0.0;
919 while t < 1.5 {
920 peak = peak.max(sim.x(t));
921 t += 0.005;
922 }
923 assert!(peak > 105.0, "the bounce should overshoot, peak = {peak}");
924 assert!(
925 (sim.x(3.0) - 100.0).abs() < 0.5,
926 "settles on the edge, at {}",
927 sim.x(3.0)
928 );
929 assert!(sim.is_done(3.0));
930 }
931
932 #[test]
933 fn bouncing_fling_that_dies_inside_never_reaches_the_spring() {
934 let sim = BouncingScrollSimulation::new(
936 0.0,
937 300.0,
938 0.0,
939 1000.0,
940 scroll_spring(),
941 Tolerance::PIXELS,
942 );
943 let expected = FrictionSimulation::new(0.135, 0.0, 300.0, Tolerance::PIXELS);
944 assert!(
945 (sim.x(0.5) - expected.x(0.5)).abs() < 1e-3,
946 "pure friction while inside the content"
947 );
948 assert!(sim.x(5.0) < 1000.0);
949 assert!(sim.is_done(5.0));
950 }
951
952 #[test]
953 fn clamped_pins_position_but_reports_velocity() {
954 let inner = FrictionSimulation::new(0.135, 0.0, 1000.0, Tolerance::PIXELS);
955 let uncapped = inner.x(10.0);
956 assert!(uncapped > 100.0);
957 let clamped = ClampedSimulation::new(inner, 0.0, 100.0);
958 assert_eq!(clamped.x(10.0), 100.0, "position pinned to the maximum");
959 assert!(clamped.dx(0.1) > 0.0);
961 }
962
963 #[test]
968 fn no_constant_deceleration_is_the_old_motion_exactly() {
969 let plain = FrictionSimulation::new(0.135, 0.0, 2000.0, Tolerance::PIXELS);
970 let with_none = FrictionSimulation::with_constant_deceleration(
971 0.135,
972 0.0,
973 2000.0,
974 Tolerance::PIXELS,
975 0.0,
976 );
977 for t in [0.0, 0.05, 0.2, 0.5, 1.0, 3.0] {
978 assert_eq!(plain.x(t), with_none.x(t), "position at {t}");
979 assert_eq!(plain.dx(t), with_none.dx(t), "velocity at {t}");
980 }
981 assert_eq!(plain.final_x(), with_none.final_x());
982 }
983
984 #[test]
988 fn a_constant_deceleration_brings_the_motion_to_an_actual_stop() {
989 let coasting = FrictionSimulation::new(0.135, 0.0, 2000.0, Tolerance::PIXELS);
990 let stopping = FrictionSimulation::with_constant_deceleration(
991 0.135,
992 0.0,
993 2000.0,
994 Tolerance::PIXELS,
995 1400.0,
996 );
997 assert!(
999 coasting.dx(5.0) > 0.0,
1000 "drag alone only reaches nought at infinity"
1001 );
1002 assert_eq!(stopping.dx(5.0), 0.0);
1004 let rest = stopping.final_x();
1005 assert_eq!(stopping.x(5.0), rest, "and it stays where it stopped");
1006 assert_eq!(stopping.x(50.0), rest);
1007 assert!(
1008 rest < coasting.final_x(),
1009 "and it stops short of where coasting would have carried it: {rest} vs {}",
1010 coasting.final_x()
1011 );
1012 }
1013
1014 #[test]
1018 fn the_stopping_time_is_where_the_velocity_runs_out() {
1019 let sim = FrictionSimulation::with_constant_deceleration(
1020 0.135,
1021 0.0,
1022 2000.0,
1023 Tolerance::PIXELS,
1024 1400.0,
1025 );
1026 let rest = sim.final_x();
1027 let mut t = 0.0;
1030 while sim.dx(t) > 0.0 && t < 5.0 {
1031 t += 0.001;
1032 }
1033 assert!(t > 0.0 && t < 5.0, "it stops, and not immediately: {t}");
1034 assert!(sim.dx(t - 0.002) > 0.0, "still moving a step before");
1035 assert!((sim.x(t) - rest).abs() < 1.0, "and it is at rest there");
1036 }
1037
1038 #[test]
1043 fn the_crossing_instant_survives_the_constant_term() {
1044 let sim = FrictionSimulation::with_constant_deceleration(
1045 0.135,
1046 0.0,
1047 2000.0,
1048 Tolerance::PIXELS,
1049 1400.0,
1050 );
1051 let target = sim.final_x() * 0.5;
1052 let t = sim.time_at_x(target);
1053 assert!(t.is_finite() && t > 0.0, "reached, at {t}");
1054 assert!(
1055 (sim.x(t) - target).abs() < 0.5,
1056 "and x({t}) is {target}: got {}",
1057 sim.x(t)
1058 );
1059 }
1060
1061 #[test]
1064 fn a_backward_fling_decelerates_backwards() {
1065 let sim = FrictionSimulation::with_constant_deceleration(
1066 0.135,
1067 0.0,
1068 -2000.0,
1069 Tolerance::PIXELS,
1070 1400.0,
1071 );
1072 assert!(sim.final_x() < 0.0, "it travelled backwards");
1073 assert_eq!(sim.dx(5.0), 0.0, "and it stopped");
1074 let forward = FrictionSimulation::with_constant_deceleration(
1075 0.135,
1076 0.0,
1077 2000.0,
1078 Tolerance::PIXELS,
1079 1400.0,
1080 );
1081 assert!(
1082 (sim.final_x() + forward.final_x()).abs() < 0.5,
1083 "the same distance either way: {} vs {}",
1084 sim.final_x(),
1085 forward.final_x()
1086 );
1087 }
1088}