1use super::{Simulation, SpringDescription, Tolerance};
20
21const DEFAULT_DISTANCE_TOLERANCE: f64 = 1e-3;
26
27fn signum_or_zero(value: f64) -> f64 {
30 if value == 0.0 { 0.0 } else { value.signum() }
31}
32
33#[derive(Debug, Clone)]
42pub struct FrictionSimulation {
43 drag: f64,
44 drag_log: f64,
45 position: f64,
46 velocity: f64,
47 constant_deceleration: f64,
50 final_time: f64,
53 tolerance: Tolerance,
54}
55
56impl FrictionSimulation {
57 pub fn new(
62 drag: f64,
63 position: f64,
64 velocity: f64,
65 tolerance: Tolerance,
66 constant_deceleration: f64,
67 ) -> Self {
68 let mut simulation = FrictionSimulation {
69 drag,
70 drag_log: drag.ln(),
71 position,
72 velocity,
73 constant_deceleration: constant_deceleration * signum_or_zero(velocity),
74 final_time: f64::INFINITY,
75 tolerance,
76 };
77 simulation.final_time = simulation.solve_final_time();
78 simulation
79 }
80
81 pub fn through(start_x: f64, end_x: f64, start_velocity: f64, end_velocity: f64) -> Self {
87 let drag = ((start_velocity - end_velocity) / (start_x - end_x)).exp();
88 FrictionSimulation::new(
89 drag,
90 start_x,
91 start_velocity,
92 Tolerance {
93 velocity: end_velocity.abs(),
94 distance: DEFAULT_DISTANCE_TOLERANCE,
95 },
96 0.0,
97 )
98 }
99
100 pub fn final_x(&self) -> f64 {
103 if self.constant_deceleration == 0.0 {
104 self.position - self.velocity / self.drag_log
105 } else {
106 self.position_at(self.final_time)
107 }
108 }
109
110 pub fn time_at_x(&self, x: f64) -> f64 {
118 if x == self.position {
119 return 0.0;
120 }
121 let final_x = self.final_x();
122 let out_of_reach = if self.velocity > 0.0 {
123 x < self.position || x > final_x
124 } else {
125 x > self.position || x < final_x
126 };
127 if self.velocity == 0.0 || out_of_reach {
128 return f64::INFINITY;
129 }
130 ((x - self.position + self.velocity / self.drag_log) * self.drag_log / self.velocity).ln()
131 / self.drag_log
132 }
133
134 fn position_at(&self, time: f64) -> f64 {
136 let drag_term = self.position + self.velocity * self.drag.powf(time) / self.drag_log
137 - self.velocity / self.drag_log;
138 if self.constant_deceleration == 0.0 {
139 drag_term
142 } else {
143 drag_term - self.constant_deceleration / 2.0 * time * time
144 }
145 }
146
147 fn velocity_at(&self, time: f64) -> f64 {
149 let drag_term = self.velocity * self.drag.powf(time);
150 if self.constant_deceleration == 0.0 {
151 drag_term
152 } else {
153 drag_term - self.constant_deceleration * time
154 }
155 }
156
157 fn solve_final_time(&self) -> f64 {
163 if self.constant_deceleration == 0.0 {
164 return f64::INFINITY;
165 }
166 let toward = signum_or_zero(self.velocity);
167 let mut lower = 0.0;
168 let mut upper = self.velocity.abs() / self.constant_deceleration.abs();
169 if self.velocity_at(upper) * toward > 0.0 {
170 return f64::INFINITY;
171 }
172 for _ in 0..64 {
173 let middle = 0.5 * (lower + upper);
174 if self.velocity_at(middle) * toward > 0.0 {
175 lower = middle;
176 } else {
177 upper = middle;
178 }
179 }
180 upper
181 }
182}
183
184impl Simulation for FrictionSimulation {
185 fn x(&self, time: f64) -> f64 {
186 self.position_at(time.min(self.final_time))
187 }
188
189 fn dx(&self, time: f64) -> f64 {
190 if time > self.final_time {
191 0.0
192 } else {
193 self.velocity_at(time)
194 }
195 }
196
197 fn is_done(&self, time: f64) -> bool {
202 time >= self.final_time || self.dx(time).abs() < self.tolerance.velocity
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub enum SpringType {
210 Critical,
213 Overdamped,
215 Underdamped,
218}
219
220#[derive(Debug, Clone, Copy)]
223enum SpringSolution {
224 Critical { r: f64, c1: f64, c2: f64 },
225 Overdamped { r1: f64, r2: f64, c1: f64, c2: f64 },
226 Underdamped { w: f64, r: f64, c1: f64, c2: f64 },
227}
228
229impl SpringSolution {
230 fn new(spring: SpringDescription, distance: f64, velocity: f64) -> Self {
231 let discriminant = spring.damping * spring.damping - 4.0 * spring.mass * spring.stiffness;
232 if discriminant == 0.0 {
233 let r = -spring.damping / (2.0 * spring.mass);
234 SpringSolution::Critical {
235 r,
236 c1: distance,
237 c2: velocity - r * distance,
238 }
239 } else if discriminant > 0.0 {
240 let root = discriminant.sqrt();
241 let r1 = (-spring.damping - root) / (2.0 * spring.mass);
242 let r2 = (-spring.damping + root) / (2.0 * spring.mass);
243 let c2 = (velocity - r1 * distance) / (r2 - r1);
244 SpringSolution::Overdamped {
245 r1,
246 r2,
247 c1: distance - c2,
248 c2,
249 }
250 } else {
251 let w = (4.0 * spring.mass * spring.stiffness - spring.damping * spring.damping).sqrt()
252 / (2.0 * spring.mass);
253 let r = -spring.damping / (2.0 * spring.mass);
255 SpringSolution::Underdamped {
256 w,
257 r,
258 c1: distance,
259 c2: (velocity - r * distance) / w,
260 }
261 }
262 }
263
264 fn spring_type(&self) -> SpringType {
265 match self {
266 SpringSolution::Critical { .. } => SpringType::Critical,
267 SpringSolution::Overdamped { .. } => SpringType::Overdamped,
268 SpringSolution::Underdamped { .. } => SpringType::Underdamped,
269 }
270 }
271
272 fn x(&self, time: f64) -> f64 {
274 match *self {
275 SpringSolution::Critical { r, c1, c2 } => (c1 + c2 * time) * (r * time).exp(),
276 SpringSolution::Overdamped { r1, r2, c1, c2 } => {
277 c1 * (r1 * time).exp() + c2 * (r2 * time).exp()
278 }
279 SpringSolution::Underdamped { w, r, c1, c2 } => {
280 (r * time).exp() * (c1 * (w * time).cos() + c2 * (w * time).sin())
281 }
282 }
283 }
284
285 fn dx(&self, time: f64) -> f64 {
286 match *self {
287 SpringSolution::Critical { r, c1, c2 } => {
288 let decay = (r * time).exp();
289 r * (c1 + c2 * time) * decay + c2 * decay
290 }
291 SpringSolution::Overdamped { r1, r2, c1, c2 } => {
292 c1 * r1 * (r1 * time).exp() + c2 * r2 * (r2 * time).exp()
293 }
294 SpringSolution::Underdamped { w, r, c1, c2 } => {
295 let decay = (r * time).exp();
296 let cosine = (w * time).cos();
297 let sine = (w * time).sin();
298 decay * (c2 * w * cosine - c1 * w * sine) + r * decay * (c2 * sine + c1 * cosine)
299 }
300 }
301 }
302}
303
304#[derive(Debug, Clone)]
307pub struct SpringSimulation {
308 end: f64,
309 solution: SpringSolution,
310 tolerance: Tolerance,
311}
312
313impl SpringSimulation {
314 pub fn new(
317 spring: SpringDescription,
318 start: f64,
319 end: f64,
320 velocity: f64,
321 tolerance: Tolerance,
322 ) -> Self {
323 SpringSimulation {
324 end,
325 solution: SpringSolution::new(spring, start - end, velocity),
326 tolerance,
327 }
328 }
329
330 pub fn end(&self) -> f64 {
332 self.end
333 }
334
335 pub fn spring_type(&self) -> SpringType {
337 self.solution.spring_type()
338 }
339}
340
341impl Simulation for SpringSimulation {
342 fn x(&self, time: f64) -> f64 {
343 self.end + self.solution.x(time)
344 }
345
346 fn dx(&self, time: f64) -> f64 {
347 self.solution.dx(time)
348 }
349
350 fn is_done(&self, time: f64) -> bool {
351 self.solution.x(time).abs() < self.tolerance.distance
352 && self.solution.dx(time).abs() < self.tolerance.velocity
353 }
354}
355
356#[derive(Debug, Clone)]
363pub struct ScrollSpringSimulation {
364 inner: SpringSimulation,
365}
366
367impl ScrollSpringSimulation {
368 pub fn new(
371 spring: SpringDescription,
372 start: f64,
373 end: f64,
374 velocity: f64,
375 tolerance: Tolerance,
376 ) -> Self {
377 ScrollSpringSimulation {
378 inner: SpringSimulation::new(spring, start, end, velocity, tolerance),
379 }
380 }
381
382 pub fn end(&self) -> f64 {
384 self.inner.end()
385 }
386
387 pub fn spring_type(&self) -> SpringType {
389 self.inner.spring_type()
390 }
391}
392
393impl Simulation for ScrollSpringSimulation {
394 fn x(&self, time: f64) -> f64 {
395 if self.inner.is_done(time) {
396 self.inner.end()
397 } else {
398 self.inner.x(time)
399 }
400 }
401
402 fn dx(&self, time: f64) -> f64 {
403 self.inner.dx(time)
404 }
405
406 fn is_done(&self, time: f64) -> bool {
407 self.inner.is_done(time)
408 }
409}
410
411#[derive(Debug, Clone)]
415pub struct ClampingScrollSimulation {
416 position: f64,
417 velocity: f64,
418 duration: f64,
420 distance: f64,
422 tolerance: Tolerance,
423}
424
425impl ClampingScrollSimulation {
426 pub const DEFAULT_FRICTION: f64 = 0.015;
428
429 pub const INFLEXION: f64 = 0.35;
433
434 pub const PHYSICAL_COEFF: f64 = 9.80665 * 39.37 * 160.0 * 0.84;
438
439 #[inline]
445 pub fn deceleration_rate() -> f64 {
446 0.78_f64.ln() / 0.9_f64.ln()
447 }
448
449 pub fn new(position: f64, velocity: f64, friction: f64, tolerance: Tolerance) -> Self {
452 let deceleration =
453 (Self::INFLEXION * velocity.abs() / (friction * Self::PHYSICAL_COEFF)).ln();
454 let duration = (deceleration / (Self::deceleration_rate() - 1.0)).exp();
455 ClampingScrollSimulation {
456 position,
457 velocity,
458 duration,
459 distance: velocity * duration / Self::deceleration_rate(),
460 tolerance,
461 }
462 }
463
464 pub fn duration(&self) -> f64 {
466 self.duration
467 }
468
469 pub fn final_x(&self) -> f64 {
471 self.position + self.distance
472 }
473
474 fn progress(&self, time: f64) -> f64 {
478 if self.duration > 0.0 {
479 (time / self.duration).clamp(0.0, 1.0)
480 } else {
481 1.0
482 }
483 }
484}
485
486impl Simulation for ClampingScrollSimulation {
487 fn x(&self, time: f64) -> f64 {
488 let remaining = 1.0 - self.progress(time);
489 self.position + self.distance * (1.0 - remaining.powf(Self::deceleration_rate()))
490 }
491
492 fn dx(&self, time: f64) -> f64 {
493 let remaining = 1.0 - self.progress(time);
494 self.velocity * remaining.powf(Self::deceleration_rate() - 1.0)
495 }
496
497 fn is_done(&self, time: f64) -> bool {
498 time >= self.duration || self.dx(time).abs() < self.tolerance.velocity
499 }
500}
501
502#[derive(Debug, Clone)]
505enum BouncingPhase {
506 Spring(ScrollSpringSimulation),
509 Friction(FrictionSimulation),
511 FrictionThenSpring {
514 friction: FrictionSimulation,
515 spring: ScrollSpringSimulation,
516 spring_time: f64,
517 },
518}
519
520#[derive(Debug, Clone)]
523pub struct BouncingScrollSimulation {
524 phase: BouncingPhase,
525}
526
527impl BouncingScrollSimulation {
528 pub const MAX_SPRING_TRANSFER_VELOCITY: f64 = 5000.0;
532
533 pub const FRICTION_DRAG: f64 = 0.135;
536
537 pub fn new(
541 position: f64,
542 velocity: f64,
543 leading_extent: f64,
544 trailing_extent: f64,
545 spring: SpringDescription,
546 tolerance: Tolerance,
547 constant_deceleration: f64,
548 ) -> Self {
549 debug_assert!(
550 leading_extent <= trailing_extent,
551 "leading extent {leading_extent} must not exceed trailing extent {trailing_extent}"
552 );
553 let phase = if position < leading_extent {
554 BouncingPhase::Spring(Self::edge_spring(
555 spring,
556 position,
557 leading_extent,
558 velocity,
559 tolerance,
560 ))
561 } else if position > trailing_extent {
562 BouncingPhase::Spring(Self::edge_spring(
563 spring,
564 position,
565 trailing_extent,
566 velocity,
567 tolerance,
568 ))
569 } else {
570 let friction = FrictionSimulation::new(
571 Self::FRICTION_DRAG,
572 position,
573 velocity,
574 tolerance,
575 constant_deceleration,
576 );
577 let heading_for = if velocity > 0.0 {
578 trailing_extent
579 } else if velocity < 0.0 {
580 leading_extent
581 } else {
582 return BouncingScrollSimulation {
584 phase: BouncingPhase::Friction(friction),
585 };
586 };
587 let spring_time = friction.time_at_x(heading_for);
589 if spring_time.is_finite() {
590 let transfer = friction.dx(spring_time);
591 BouncingPhase::FrictionThenSpring {
592 spring: Self::edge_spring(
593 spring,
594 heading_for,
595 heading_for,
596 transfer,
597 tolerance,
598 ),
599 friction,
600 spring_time,
601 }
602 } else {
603 BouncingPhase::Friction(friction)
604 }
605 };
606 BouncingScrollSimulation { phase }
607 }
608
609 fn edge_spring(
612 spring: SpringDescription,
613 start: f64,
614 extent: f64,
615 velocity: f64,
616 tolerance: Tolerance,
617 ) -> ScrollSpringSimulation {
618 ScrollSpringSimulation::new(
619 spring,
620 start,
621 extent,
622 velocity.clamp(
623 -Self::MAX_SPRING_TRANSFER_VELOCITY,
624 Self::MAX_SPRING_TRANSFER_VELOCITY,
625 ),
626 tolerance,
627 )
628 }
629
630 fn active(&self, time: f64) -> (&dyn Simulation, f64) {
634 match &self.phase {
635 BouncingPhase::Spring(spring) => (spring, 0.0),
636 BouncingPhase::Friction(friction) => (friction, 0.0),
637 BouncingPhase::FrictionThenSpring {
638 friction,
639 spring,
640 spring_time,
641 } => {
642 if time >= *spring_time {
643 (spring, *spring_time)
644 } else {
645 (friction, 0.0)
646 }
647 }
648 }
649 }
650}
651
652impl Simulation for BouncingScrollSimulation {
653 fn x(&self, time: f64) -> f64 {
654 let (simulation, offset) = self.active(time);
655 simulation.x(time - offset)
656 }
657
658 fn dx(&self, time: f64) -> f64 {
659 let (simulation, offset) = self.active(time);
660 simulation.dx(time - offset)
661 }
662
663 fn is_done(&self, time: f64) -> bool {
664 let (simulation, offset) = self.active(time);
665 simulation.is_done(time - offset)
666 }
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 fn tol() -> Tolerance {
675 Tolerance::for_device_pixel_ratio(1.0)
676 }
677
678 fn assert_close(actual: f64, expected: f64, epsilon: f64, what: &str) {
679 assert!(
680 (actual - expected).abs() < epsilon,
681 "{what}: {actual} is not within {epsilon} of {expected}"
682 );
683 }
684
685 #[test]
686 fn friction_final_x_matches_closed_form() {
687 let simulation = FrictionSimulation::new(0.135, 20.0, 1000.0, tol(), 0.0);
688 let expected = 20.0 - 1000.0 / 0.135_f64.ln();
689 assert_close(simulation.final_x(), expected, 1e-9, "final_x");
690 assert_close(simulation.x(f64::INFINITY), expected, 1e-9, "x at infinity");
691 assert_close(simulation.x(20.0), expected, 1e-6, "x long after release");
692 assert!(!simulation.is_done(0.0), "a live fling reported settled");
693 assert!(simulation.is_done(20.0), "a decayed fling never settled");
694 }
695
696 #[test]
697 fn friction_through_passes_endpoints() {
698 let simulation = FrictionSimulation::through(0.0, 100.0, 500.0, 100.0);
699 let arrival = simulation.time_at_x(100.0);
700 assert!(arrival.is_finite(), "end position unreachable at {arrival}");
701 assert_close(simulation.x(0.0), 0.0, 1e-9, "start position");
702 assert_close(simulation.dx(0.0), 500.0, 1e-9, "start velocity");
703 assert_close(simulation.x(arrival), 100.0, 1e-9, "end position");
704 assert_close(simulation.dx(arrival), 100.0, 1e-9, "end velocity");
705
706 let asymptotic = FrictionSimulation::through(0.0, 100.0, 500.0, 0.0);
709 assert_close(asymptotic.final_x(), 100.0, 1e-9, "asymptotic final_x");
710 assert!(
711 asymptotic.time_at_x(200.0).is_infinite(),
712 "a position past final_x must be unreachable"
713 );
714 assert!(
715 asymptotic.time_at_x(-10.0).is_infinite(),
716 "a position behind the start must be unreachable"
717 );
718 }
719
720 #[test]
721 fn friction_velocity_decays_exponentially() {
722 let simulation = FrictionSimulation::new(0.135, 0.0, 800.0, tol(), 0.0);
723 let ratio = simulation.dx(0.5) / simulation.dx(0.0);
724 assert_close(ratio, 0.135_f64.powf(0.5), 1e-9, "half-second decay ratio");
725 }
726
727 #[test]
728 fn friction_constant_deceleration_ends_the_motion() {
729 let plain = FrictionSimulation::new(0.135, 0.0, 1000.0, tol(), 0.0);
730 let decelerated = FrictionSimulation::new(0.135, 0.0, 1000.0, tol(), 3000.0);
731 assert!(
732 decelerated.final_x() < plain.final_x(),
733 "constant deceleration travelled {}, no further than pure drag's {}",
734 decelerated.final_x(),
735 plain.final_x()
736 );
737 assert!(decelerated.is_done(2.0), "the motion never ended");
738 assert_eq!(decelerated.dx(5.0), 0.0, "velocity past the end");
739 assert_close(
740 decelerated.x(5.0),
741 decelerated.final_x(),
742 1e-9,
743 "position past the end",
744 );
745 }
746
747 #[test]
748 fn spring_critical_over_under_damped_all_converge_to_end() {
749 let cases = [
752 (20.0, SpringType::Critical),
753 (30.0, SpringType::Overdamped),
754 (5.0, SpringType::Underdamped),
755 ];
756 for (damping, expected_type) in cases {
757 let spring = SpringDescription {
758 mass: 1.0,
759 stiffness: 100.0,
760 damping,
761 };
762 let simulation = SpringSimulation::new(spring, 100.0, 0.0, 0.0, tol());
763 assert_eq!(simulation.spring_type(), expected_type);
764 assert_close(simulation.x(0.0), 100.0, 1e-9, "start position");
765 assert_close(simulation.dx(0.0), 0.0, 1e-9, "start velocity");
766 assert_close(simulation.x(10.0), 0.0, 1e-6, "converged position");
767 assert!(
768 !simulation.is_done(0.0),
769 "{expected_type:?} spring reported settled at release"
770 );
771 assert!(
772 simulation.is_done(10.0),
773 "{expected_type:?} spring never settled"
774 );
775 }
776 }
777
778 #[test]
779 fn spring_with_damping_ratio_1_1_is_overdamped_no_oscillation() {
780 let simulation = SpringSimulation::new(
781 SpringDescription::default_scroll_spring(),
782 100.0,
783 0.0,
784 0.0,
785 tol(),
786 );
787 assert_eq!(simulation.spring_type(), SpringType::Overdamped);
788 for step in 0..=2000 {
789 let time = f64::from(step) / 1000.0;
790 let displacement = simulation.x(time) - simulation.end();
791 assert!(
792 displacement >= 0.0,
793 "crossed the end position at t={time}s (displacement {displacement})"
794 );
795 }
796 }
797
798 #[test]
799 fn scroll_spring_snaps_to_end_once_settled() {
800 let spring = SpringDescription::default_scroll_spring();
801 let plain = SpringSimulation::new(spring, -50.0, 0.0, 0.0, tol());
802 let snapping = ScrollSpringSimulation::new(spring, -50.0, 0.0, 0.0, tol());
803
804 assert_eq!(snapping.x(0.05), plain.x(0.05));
806 assert_eq!(snapping.dx(0.05), plain.dx(0.05));
807
808 assert!(snapping.is_done(3.0));
811 assert_ne!(plain.x(3.0), 0.0);
812 assert_eq!(snapping.x(3.0), snapping.end());
813 }
814
815 #[test]
816 fn clamping_deceleration_rate_value() {
817 assert_close(
818 ClampingScrollSimulation::deceleration_rate(),
819 2.358_202,
820 1e-5,
821 "DECELERATION_RATE",
822 );
823 assert_close(
824 ClampingScrollSimulation::PHYSICAL_COEFF,
825 51_890.2,
826 0.1,
827 "PHYSICAL_COEFF",
828 );
829 assert_eq!(ClampingScrollSimulation::INFLEXION, 0.35);
830 assert_eq!(ClampingScrollSimulation::DEFAULT_FRICTION, 0.015);
831 }
832
833 #[test]
834 fn clamping_dx_at_zero_equals_initial_velocity() {
835 for velocity in [2000.0, -2000.0, 350.0] {
836 let simulation = ClampingScrollSimulation::new(
837 0.0,
838 velocity,
839 ClampingScrollSimulation::DEFAULT_FRICTION,
840 tol(),
841 );
842 assert_close(simulation.dx(0.0), velocity, 1e-9, "dx at release");
843 }
844 }
845
846 #[test]
847 fn clamping_stops_at_duration() {
848 let simulation = ClampingScrollSimulation::new(
849 10.0,
850 2000.0,
851 ClampingScrollSimulation::DEFAULT_FRICTION,
852 tol(),
853 );
854 let duration = simulation.duration();
855 let target = simulation.final_x();
856 assert!(
857 duration > 0.0 && duration.is_finite(),
858 "implausible duration {duration}"
859 );
860 assert!(target > 10.0, "a positive fling must travel forward");
861 assert!(!simulation.is_done(0.0), "a live fling reported settled");
862 assert!(simulation.is_done(duration), "the fling never stopped");
863 assert_close(simulation.x(duration), target, 1e-9, "position at duration");
864 assert_close(
865 simulation.x(duration * 2.0),
866 target,
867 1e-9,
868 "position past duration",
869 );
870 assert_close(simulation.dx(duration), 0.0, 1e-9, "velocity at duration");
871
872 let mut previous = simulation.x(0.0);
873 for step in 1..=100 {
874 let position = simulation.x(duration * f64::from(step) / 100.0);
875 assert!(position >= previous, "backtracked to {position}");
876 assert!(position <= target + 1e-9, "overshot the target: {position}");
877 previous = position;
878 }
879 }
880
881 #[test]
882 fn bouncing_switches_friction_to_spring_at_extent() {
883 let trailing = 100.0;
884 let simulation = BouncingScrollSimulation::new(
885 0.0,
886 2000.0,
887 0.0,
888 trailing,
889 SpringDescription::default_scroll_spring(),
890 tol(),
891 0.0,
892 );
893 let friction = FrictionSimulation::new(
894 BouncingScrollSimulation::FRICTION_DRAG,
895 0.0,
896 2000.0,
897 tol(),
898 0.0,
899 );
900 let handover = friction.time_at_x(trailing);
901 assert!(handover.is_finite(), "the fling must reach the extent");
902
903 let early = handover / 2.0;
905 assert_eq!(simulation.x(early), friction.x(early));
906 assert_eq!(simulation.dx(early), friction.dx(early));
907
908 assert_close(
911 simulation.x(handover),
912 trailing,
913 1e-9,
914 "position at handover",
915 );
916 assert_close(
917 simulation.dx(handover),
918 friction.dx(handover),
919 1e-9,
920 "velocity at handover",
921 );
922
923 let mut furthest = f64::MIN;
925 for step in 0..=500 {
926 furthest = furthest.max(simulation.x(handover + f64::from(step) / 100.0));
927 }
928 assert!(
929 furthest > trailing,
930 "the bounce never passed the extent (peaked at {furthest})"
931 );
932 assert!(
933 furthest < trailing + 60.0,
934 "the bounce ran away past the extent (peaked at {furthest})"
935 );
936 assert_close(
937 simulation.x(handover + 5.0),
938 trailing,
939 1e-9,
940 "settled position",
941 );
942 assert!(
943 simulation.is_done(handover + 5.0),
944 "the bounce never settled"
945 );
946 }
947
948 #[test]
949 fn bouncing_stays_friction_when_no_extent_is_reached() {
950 let simulation = BouncingScrollSimulation::new(
951 0.0,
952 100.0,
953 0.0,
954 10_000.0,
955 SpringDescription::default_scroll_spring(),
956 tol(),
957 0.0,
958 );
959 let friction = FrictionSimulation::new(
960 BouncingScrollSimulation::FRICTION_DRAG,
961 0.0,
962 100.0,
963 tol(),
964 0.0,
965 );
966 assert!(
967 friction.time_at_x(10_000.0).is_infinite(),
968 "this fling must stop short of the extent for the test to mean anything"
969 );
970 for step in 0..=100 {
971 let time = f64::from(step) / 10.0;
972 assert_eq!(simulation.x(time), friction.x(time));
973 assert_eq!(simulation.dx(time), friction.dx(time));
974 }
975 }
976
977 #[test]
978 fn bouncing_underscroll_starts_as_spring() {
979 let leading = 0.0;
980 let simulation = BouncingScrollSimulation::new(
981 -50.0,
982 0.0,
983 leading,
984 100.0,
985 SpringDescription::default_scroll_spring(),
986 tol(),
987 0.0,
988 );
989 assert_close(simulation.x(0.0), -50.0, 1e-9, "starts where released");
990 assert!(
991 simulation.x(0.1) > simulation.x(0.0),
992 "must travel back toward the leading extent"
993 );
994 assert!(!simulation.is_done(0.0), "an overscrolled rest is not done");
995 assert_close(simulation.x(3.0), leading, 1e-9, "settled position");
996 assert!(simulation.is_done(3.0), "the bounce-back never settled");
997 }
998
999 #[test]
1000 fn bouncing_spring_transfer_velocity_capped() {
1001 let spring = SpringDescription::default_scroll_spring();
1002 let cap = BouncingScrollSimulation::MAX_SPRING_TRANSFER_VELOCITY;
1003
1004 let underscroll =
1006 BouncingScrollSimulation::new(-50.0, -20_000.0, 0.0, 100.0, spring, tol(), 0.0);
1007 assert_close(underscroll.dx(0.0), -cap, 1e-9, "clamped underscroll seed");
1008
1009 let fling = BouncingScrollSimulation::new(0.0, 50_000.0, 0.0, 100.0, spring, tol(), 0.0);
1011 let friction = FrictionSimulation::new(
1012 BouncingScrollSimulation::FRICTION_DRAG,
1013 0.0,
1014 50_000.0,
1015 tol(),
1016 0.0,
1017 );
1018 let handover = friction.time_at_x(100.0);
1019 assert!(
1020 friction.dx(handover) > cap,
1021 "the fling must arrive above the cap for the test to mean anything"
1022 );
1023 assert_close(fling.dx(handover), cap, 1e-9, "clamped handover seed");
1024 }
1025}