#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Tolerance {
pub distance: f32,
pub velocity: f32,
}
impl Default for Tolerance {
fn default() -> Self {
Self {
distance: 1e-3,
velocity: 1e-3,
}
}
}
impl Tolerance {
pub const PIXELS: Tolerance = Tolerance {
distance: 0.5,
velocity: 2.0,
};
}
pub trait Simulation {
fn x(&self, time: f32) -> f32;
fn dx(&self, time: f32) -> f32;
fn is_done(&self, time: f32) -> bool;
fn tolerance(&self) -> Tolerance {
Tolerance::default()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SpringDescription {
pub mass: f32,
pub stiffness: f32,
pub damping: f32,
}
impl SpringDescription {
pub fn new(mass: f32, stiffness: f32, damping: f32) -> Self {
Self {
mass,
stiffness,
damping,
}
}
pub fn with_damping_ratio(mass: f32, stiffness: f32, ratio: f32) -> Self {
Self {
mass,
stiffness,
damping: ratio * 2.0 * (mass * stiffness).sqrt(),
}
}
}
#[derive(Clone, Copy, Debug)]
enum Solution {
Critical { r: f32, c1: f32, c2: f32 },
Overdamped { r1: f32, r2: f32, c1: f32, c2: f32 },
Underdamped { w: f32, r: f32, c1: f32, c2: f32 },
}
impl Solution {
fn x(&self, t: f32) -> f32 {
match *self {
Solution::Critical { r, c1, c2 } => (c1 + c2 * t) * (r * t).exp(),
Solution::Overdamped { r1, r2, c1, c2 } => c1 * (r1 * t).exp() + c2 * (r2 * t).exp(),
Solution::Underdamped { w, r, c1, c2 } => {
(r * t).exp() * (c1 * (w * t).cos() + c2 * (w * t).sin())
}
}
}
fn dx(&self, t: f32) -> f32 {
match *self {
Solution::Critical { r, c1, c2 } => {
let power = (r * t).exp();
r * (c1 + c2 * t) * power + c2 * power
}
Solution::Overdamped { r1, r2, c1, c2 } => {
c1 * r1 * (r1 * t).exp() + c2 * r2 * (r2 * t).exp()
}
Solution::Underdamped { w, r, c1, c2 } => {
let power = (r * t).exp();
let cosine = (w * t).cos();
let sine = (w * t).sin();
power * (c2 * w * cosine - c1 * w * sine) + r * power * (c2 * sine + c1 * cosine)
}
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct SpringSimulation {
end: f32,
solution: Solution,
tolerance: Tolerance,
}
impl SpringSimulation {
pub fn new(
spring: SpringDescription,
start: f32,
end: f32,
velocity: f32,
tolerance: Tolerance,
) -> Self {
let m = spring.mass;
let k = spring.stiffness;
let c = spring.damping;
let x0 = start - end;
let v0 = velocity;
let cmk = c * c - 4.0 * m * k;
let solution = if cmk.abs() <= 1e-4 * (4.0 * m * k).max(1.0) {
let r = -c / (2.0 * m);
Solution::Critical {
r,
c1: x0,
c2: v0 - r * x0,
}
} else if cmk > 0.0 {
let root = cmk.sqrt();
let r1 = (-c - root) / (2.0 * m);
let r2 = (-c + root) / (2.0 * m);
let c2 = (v0 - r1 * x0) / (r2 - r1);
Solution::Overdamped {
r1,
r2,
c1: x0 - c2,
c2,
}
} else {
let w = (4.0 * m * k - c * c).sqrt() / (2.0 * m);
let r = -c / (2.0 * m);
Solution::Underdamped {
w,
r,
c1: x0,
c2: (v0 - r * x0) / w,
}
};
Self {
end,
solution,
tolerance,
}
}
}
impl Simulation for SpringSimulation {
fn x(&self, time: f32) -> f32 {
self.end + self.solution.x(time)
}
fn dx(&self, time: f32) -> f32 {
self.solution.dx(time)
}
fn is_done(&self, time: f32) -> bool {
self.solution.x(time).abs() < self.tolerance.distance
&& self.solution.dx(time).abs() < self.tolerance.velocity
}
fn tolerance(&self) -> Tolerance {
self.tolerance
}
}
fn sign_of(v: f32) -> f32 {
if v < 0.0 {
-1.0
} else {
1.0
}
}
fn newtons_method(guess: f32, target: f32, f: impl Fn(f32) -> f32, df: impl Fn(f32) -> f32) -> f32 {
let mut t = guess;
for _ in 0..10 {
let slope = df(t);
if slope == 0.0 {
break;
}
t -= (f(t) - target) / slope;
}
t
}
#[derive(Clone, Copy, Debug)]
pub struct FrictionSimulation {
drag: f32,
drag_log: f32,
x0: f32,
v0: f32,
constant_deceleration: f32,
final_time: f32,
tolerance: Tolerance,
}
impl FrictionSimulation {
pub fn new(drag: f32, position: f32, velocity: f32, tolerance: Tolerance) -> Self {
Self::with_constant_deceleration(drag, position, velocity, tolerance, 0.0)
}
pub fn with_constant_deceleration(
drag: f32,
position: f32,
velocity: f32,
tolerance: Tolerance,
constant_deceleration: f32,
) -> Self {
let drag_log = drag.ln();
let constant_deceleration = constant_deceleration * sign_of(velocity);
let mut sim = Self {
drag,
drag_log,
x0: position,
v0: velocity,
constant_deceleration,
final_time: f32::INFINITY,
tolerance,
};
if constant_deceleration != 0.0 {
sim.final_time = newtons_method(
0.0,
0.0,
|t| sim.dx(t),
|t| sim.v0 * sim.drag.powf(t) * sim.drag_log - sim.constant_deceleration,
);
}
sim
}
pub fn through(
start: f32,
end: f32,
start_velocity: f32,
end_velocity: f32,
tolerance: Tolerance,
) -> Self {
let drag = ((start_velocity - end_velocity) / (start - end)).exp();
Self::new(drag, start, start_velocity, tolerance)
}
pub fn final_x(&self) -> f32 {
if self.constant_deceleration == 0.0 {
self.x0 - self.v0 / self.drag_log
} else {
self.x(self.final_time)
}
}
pub fn time_at_x(&self, x: f32) -> f32 {
if x == self.x0 {
return 0.0;
}
let unreachable = if self.v0 > 0.0 {
x < self.x0 || x > self.final_x()
} else {
x > self.x0 || x < self.final_x()
};
if self.v0 == 0.0 || unreachable {
return f32::INFINITY;
}
if self.constant_deceleration == 0.0 {
return (self.drag_log * (x - self.x0) / self.v0 + 1.0).ln() / self.drag_log;
}
newtons_method(0.0, x, |t| self.x(t), |t| self.dx(t))
}
}
impl Simulation for FrictionSimulation {
fn x(&self, time: f32) -> f32 {
if time > self.final_time {
return self.final_x();
}
self.x0 + (self.v0 / self.drag_log) * (self.drag.powf(time) - 1.0)
- 0.5 * self.constant_deceleration * time * time
}
fn dx(&self, time: f32) -> f32 {
if time > self.final_time {
return 0.0;
}
self.v0 * self.drag.powf(time) - self.constant_deceleration * time
}
fn is_done(&self, time: f32) -> bool {
self.dx(time).abs() < self.tolerance.velocity
}
fn tolerance(&self) -> Tolerance {
self.tolerance
}
}
#[derive(Clone, Copy, Debug)]
pub struct ClampingScrollSimulation {
position: f32,
velocity: f32,
duration: f32,
distance: f32,
tolerance: Tolerance,
}
const DECELERATION_RATE: f32 = 2.358_201_8;
const INFLEXION: f32 = 0.35;
const PHYSICAL_COEFF: f32 = 9.806_65 * 39.37 * 160.0 * 0.84;
pub const CLAMPING_FRICTION: f32 = 0.015;
impl ClampingScrollSimulation {
pub fn new(position: f32, velocity: f32, tolerance: Tolerance) -> Self {
Self::with_friction(position, velocity, CLAMPING_FRICTION, tolerance)
}
pub fn with_friction(
position: f32,
velocity: f32,
friction: f32,
tolerance: Tolerance,
) -> Self {
let duration = if velocity.abs() < tolerance.velocity.max(f32::EPSILON) {
0.0
} else {
let reference_velocity = friction * PHYSICAL_COEFF / INFLEXION;
let android_duration =
(velocity.abs() / reference_velocity).powf(1.0 / (DECELERATION_RATE - 1.0));
DECELERATION_RATE * INFLEXION * android_duration
};
Self {
position,
velocity,
duration,
distance: velocity * duration / DECELERATION_RATE,
tolerance,
}
}
pub fn duration(&self) -> f32 {
self.duration
}
pub fn final_x(&self) -> f32 {
self.position + self.distance
}
fn progress(&self, time: f32) -> f32 {
if self.duration <= 0.0 {
1.0
} else {
(time / self.duration).clamp(0.0, 1.0)
}
}
}
impl Simulation for ClampingScrollSimulation {
fn x(&self, time: f32) -> f32 {
let t = self.progress(time);
self.position + self.distance * (1.0 - (1.0 - t).powf(DECELERATION_RATE))
}
fn dx(&self, time: f32) -> f32 {
if self.duration <= 0.0 {
return 0.0;
}
let t = self.progress(time);
self.velocity * (1.0 - t).powf(DECELERATION_RATE - 1.0)
}
fn is_done(&self, time: f32) -> bool {
time >= self.duration
}
fn tolerance(&self) -> Tolerance {
self.tolerance
}
}
#[derive(Clone, Copy, Debug)]
pub struct BouncingScrollSimulation {
friction: FrictionSimulation,
spring: SpringSimulation,
spring_time: f32,
tolerance: Tolerance,
}
pub const BOUNCING_DRAG: f32 = 0.135;
pub const MAX_SPRING_TRANSFER_VELOCITY: f32 = 5000.0;
impl BouncingScrollSimulation {
pub fn new(
position: f32,
velocity: f32,
leading: f32,
trailing: f32,
spring: SpringDescription,
tolerance: Tolerance,
) -> Self {
Self::with_constant_deceleration(
position, velocity, leading, trailing, spring, tolerance, 0.0,
)
}
#[allow(clippy::too_many_arguments)]
pub fn with_constant_deceleration(
position: f32,
velocity: f32,
leading: f32,
trailing: f32,
spring: SpringDescription,
tolerance: Tolerance,
constant_deceleration: f32,
) -> Self {
debug_assert!(leading <= trailing);
let friction = FrictionSimulation::with_constant_deceleration(
BOUNCING_DRAG,
position,
velocity,
tolerance,
constant_deceleration,
);
let to =
|edge: f32, from: f32, v: f32| SpringSimulation::new(spring, from, edge, v, tolerance);
if position < leading {
return Self {
friction,
spring: to(leading, position, velocity),
spring_time: f32::NEG_INFINITY,
tolerance,
};
}
if position > trailing {
return Self {
friction,
spring: to(trailing, position, velocity),
spring_time: f32::NEG_INFINITY,
tolerance,
};
}
let final_x = friction.final_x();
let (spring_time, spring) = if velocity > 0.0 && final_x > trailing {
let t = friction.time_at_x(trailing);
let carried = friction.dx(t).min(MAX_SPRING_TRANSFER_VELOCITY);
(t, to(trailing, trailing, carried))
} else if velocity < 0.0 && final_x < leading {
let t = friction.time_at_x(leading);
let carried = friction.dx(t).min(MAX_SPRING_TRANSFER_VELOCITY);
(t, to(leading, leading, carried))
} else {
(f32::INFINITY, to(position, position, 0.0))
};
Self {
friction,
spring,
spring_time,
tolerance,
}
}
fn phase(&self, time: f32) -> (&dyn Simulation, f32) {
if time > self.spring_time {
let offset = if self.spring_time.is_finite() {
self.spring_time
} else {
0.0
};
(&self.spring, offset)
} else {
(&self.friction, 0.0)
}
}
}
impl Simulation for BouncingScrollSimulation {
fn x(&self, time: f32) -> f32 {
let (sim, offset) = self.phase(time);
sim.x(time - offset)
}
fn dx(&self, time: f32) -> f32 {
let (sim, offset) = self.phase(time);
sim.dx(time - offset)
}
fn is_done(&self, time: f32) -> bool {
let (sim, offset) = self.phase(time);
sim.is_done(time - offset)
}
fn tolerance(&self) -> Tolerance {
self.tolerance
}
}
pub struct ClampedSimulation<S: Simulation> {
inner: S,
min: f32,
max: f32,
}
impl<S: Simulation> ClampedSimulation<S> {
pub fn new(inner: S, min: f32, max: f32) -> Self {
Self { inner, min, max }
}
}
impl<S: Simulation> Simulation for ClampedSimulation<S> {
fn x(&self, time: f32) -> f32 {
self.inner.x(time).clamp(self.min, self.max)
}
fn dx(&self, time: f32) -> f32 {
self.inner.dx(time)
}
fn is_done(&self, time: f32) -> bool {
self.inner.is_done(time)
}
fn tolerance(&self) -> Tolerance {
self.inner.tolerance()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dx_matches_x<S: Simulation>(sim: &S, t: f32) {
let h = 1e-4;
let numeric = (sim.x(t + h) - sim.x(t - h)) / (2.0 * h);
let analytic = sim.dx(t);
let scale = analytic.abs().max(1.0);
assert!(
(numeric - analytic).abs() < 1e-2 * scale,
"inconsistent dx at t={t}: numeric={numeric}, analytic={analytic}"
);
}
#[test]
fn critical_spring_settles_without_overshoot() {
let spring = SpringDescription::with_damping_ratio(1.0, 500.0, 1.0);
let sim = SpringSimulation::new(spring, 0.0, 1.0, 0.0, Tolerance::default());
assert!((sim.x(0.0) - 0.0).abs() < 1e-5, "starts at start");
let mut prev = sim.x(0.0);
let mut t = 0.0;
while t < 2.0 {
let v = sim.x(t);
assert!(v <= 1.0 + 1e-4, "overshoot at t={t}: {v}");
assert!(v >= prev - 1e-4, "not monotonic at t={t}");
prev = v;
t += 0.01;
}
assert!((sim.x(2.0) - 1.0).abs() < 1e-2, "arrives at end");
assert!(sim.is_done(3.0), "at rest after 3 s");
dx_matches_x(&sim, 0.2);
}
#[test]
fn underdamped_spring_overshoots_then_returns() {
let spring = SpringDescription::with_damping_ratio(1.0, 500.0, 0.3);
let sim = SpringSimulation::new(spring, 0.0, 1.0, 0.0, Tolerance::default());
let mut max = f32::MIN;
let mut t = 0.0;
while t < 2.0 {
max = max.max(sim.x(t));
t += 0.005;
}
assert!(max > 1.05, "should overshoot the target, max={max}");
assert!((sim.x(3.0) - 1.0).abs() < 1e-2);
dx_matches_x(&sim, 0.1);
}
#[test]
fn overdamped_spring_is_slow_and_monotonic() {
let spring = SpringDescription::with_damping_ratio(1.0, 100.0, 2.5);
let sim = SpringSimulation::new(spring, 0.0, 1.0, 0.0, Tolerance::default());
let mut prev = sim.x(0.0);
let mut t = 0.0;
while t < 3.0 {
let v = sim.x(t);
assert!(v <= 1.0 + 1e-4, "no overshoot when overdamped, at t={t}");
assert!(v >= prev - 1e-4, "monotonic at t={t}");
prev = v;
t += 0.01;
}
dx_matches_x(&sim, 0.3);
}
#[test]
fn spring_honours_initial_velocity() {
let spring = SpringDescription::with_damping_ratio(1.0, 500.0, 1.0);
let sim = SpringSimulation::new(spring, 0.0, 1.0, 50.0, Tolerance::default());
assert!((sim.dx(0.0) - 50.0).abs() < 1e-2, "dx(0) = {}", sim.dx(0.0));
}
#[test]
fn friction_decelerates_to_finite_limit() {
let sim = FrictionSimulation::new(0.135, 0.0, 1000.0, Tolerance::PIXELS);
let limit = sim.final_x();
assert!(sim.x(0.0).abs() < 1e-4);
assert!(sim.x(10.0) <= limit + 1e-3);
assert!(
(sim.x(10.0) - limit).abs() < 1.0,
"close to the limit after 10 s"
);
assert!(sim.dx(0.0) > sim.dx(1.0));
assert!(sim.dx(5.0).abs() < sim.dx(0.0).abs());
dx_matches_x(&sim, 0.5);
}
#[test]
fn friction_through_hits_endpoints() {
let sim = FrictionSimulation::through(0.0, 200.0, 1000.0, 0.0, Tolerance::PIXELS);
assert!((sim.x(0.0)).abs() < 1e-3);
assert!(
(sim.final_x() - 200.0).abs() < 1e-1,
"final = {}",
sim.final_x()
);
}
#[test]
fn friction_time_at_x_inverts_x() {
let sim = FrictionSimulation::new(0.135, 0.0, 1000.0, Tolerance::PIXELS);
let t = sim.time_at_x(200.0);
assert!(t.is_finite(), "200 px is within reach of a 1000 px/s fling");
assert!(
(sim.x(t) - 200.0).abs() < 1e-2,
"x(time_at_x(200)) = {}",
sim.x(t)
);
assert!(sim.time_at_x(-10.0).is_infinite());
assert!(sim.time_at_x(sim.final_x() + 10.0).is_infinite());
assert_eq!(sim.time_at_x(0.0), 0.0);
}
#[test]
fn clamping_fling_matches_the_platform_distance() {
let sim = ClampingScrollSimulation::new(0.0, 1000.0, Tolerance::PIXELS);
assert!(
(sim.duration() - 0.458).abs() < 0.01,
"duration = {}",
sim.duration()
);
assert!(
(sim.final_x() - 194.3).abs() < 1.0,
"distance = {}",
sim.final_x()
);
let mut prev = sim.x(0.0);
let mut t = 0.0;
while t < sim.duration() {
let v = sim.x(t);
assert!(v >= prev - 1e-4, "not monotonic at t={t}");
prev = v;
t += 0.01;
}
assert!(
(sim.dx(0.0) - 1000.0).abs() < 1e-1,
"starts at the release velocity"
);
assert!(sim.dx(sim.duration()).abs() < 1e-3, "ends at rest");
assert!(!sim.is_done(sim.duration() * 0.5));
assert!(sim.is_done(sim.duration()));
dx_matches_x(&sim, 0.2);
}
#[test]
fn clamping_fling_is_ballistic() {
let sim = ClampingScrollSimulation::new(0.0, 1200.0, Tolerance::PIXELS);
let t = 0.15;
let restarted = ClampingScrollSimulation::new(sim.x(t), sim.dx(t), Tolerance::PIXELS);
assert!(
(restarted.final_x() - sim.final_x()).abs() < 1.0,
"restarted lands at {} instead of {}",
restarted.final_x(),
sim.final_x()
);
}
#[test]
fn clamping_fling_below_tolerance_does_not_move() {
let sim = ClampingScrollSimulation::new(42.0, 0.0, Tolerance::PIXELS);
assert_eq!(sim.duration(), 0.0);
assert_eq!(sim.x(0.0), 42.0);
assert_eq!(sim.dx(0.0), 0.0);
assert!(sim.is_done(0.0));
}
fn scroll_spring() -> SpringDescription {
SpringDescription::with_damping_ratio(0.5, 100.0, 1.1)
}
#[test]
fn bouncing_fling_springs_back_when_it_starts_outside() {
let sim = BouncingScrollSimulation::new(
130.0,
0.0,
0.0,
100.0,
scroll_spring(),
Tolerance::PIXELS,
);
assert!(
(sim.x(0.0) - 130.0).abs() < 1e-3,
"starts where it was let go"
);
assert!(sim.x(0.3) < 130.0, "comes back towards the edge");
assert!(
(sim.x(2.0) - 100.0).abs() < 0.5,
"settles on the edge, at {}",
sim.x(2.0)
);
assert!(sim.is_done(2.0));
}
#[test]
fn bouncing_fling_hands_friction_over_to_the_spring_at_the_edge() {
let sim = BouncingScrollSimulation::new(
0.0,
2000.0,
0.0,
100.0,
scroll_spring(),
Tolerance::PIXELS,
);
let friction = FrictionSimulation::new(0.135, 0.0, 2000.0, Tolerance::PIXELS);
let handover = friction.time_at_x(100.0);
assert!(handover.is_finite() && handover > 0.0);
let before = sim.x(handover - 1e-5);
let after = sim.x(handover + 1e-5);
assert!((before - 100.0).abs() < 0.1, "before = {before}");
assert!((after - 100.0).abs() < 0.1, "after = {after}");
assert!((after - before).abs() < 0.1, "jump at the hand-over");
let mut peak: f32 = 0.0;
let mut t = 0.0;
while t < 1.5 {
peak = peak.max(sim.x(t));
t += 0.005;
}
assert!(peak > 105.0, "the bounce should overshoot, peak = {peak}");
assert!(
(sim.x(3.0) - 100.0).abs() < 0.5,
"settles on the edge, at {}",
sim.x(3.0)
);
assert!(sim.is_done(3.0));
}
#[test]
fn bouncing_fling_that_dies_inside_never_reaches_the_spring() {
let sim = BouncingScrollSimulation::new(
0.0,
300.0,
0.0,
1000.0,
scroll_spring(),
Tolerance::PIXELS,
);
let expected = FrictionSimulation::new(0.135, 0.0, 300.0, Tolerance::PIXELS);
assert!(
(sim.x(0.5) - expected.x(0.5)).abs() < 1e-3,
"pure friction while inside the content"
);
assert!(sim.x(5.0) < 1000.0);
assert!(sim.is_done(5.0));
}
#[test]
fn clamped_pins_position_but_reports_velocity() {
let inner = FrictionSimulation::new(0.135, 0.0, 1000.0, Tolerance::PIXELS);
let uncapped = inner.x(10.0);
assert!(uncapped > 100.0);
let clamped = ClampedSimulation::new(inner, 0.0, 100.0);
assert_eq!(clamped.x(10.0), 100.0, "position pinned to the maximum");
assert!(clamped.dx(0.1) > 0.0);
}
#[test]
fn no_constant_deceleration_is_the_old_motion_exactly() {
let plain = FrictionSimulation::new(0.135, 0.0, 2000.0, Tolerance::PIXELS);
let with_none = FrictionSimulation::with_constant_deceleration(
0.135,
0.0,
2000.0,
Tolerance::PIXELS,
0.0,
);
for t in [0.0, 0.05, 0.2, 0.5, 1.0, 3.0] {
assert_eq!(plain.x(t), with_none.x(t), "position at {t}");
assert_eq!(plain.dx(t), with_none.dx(t), "velocity at {t}");
}
assert_eq!(plain.final_x(), with_none.final_x());
}
#[test]
fn a_constant_deceleration_brings_the_motion_to_an_actual_stop() {
let coasting = FrictionSimulation::new(0.135, 0.0, 2000.0, Tolerance::PIXELS);
let stopping = FrictionSimulation::with_constant_deceleration(
0.135,
0.0,
2000.0,
Tolerance::PIXELS,
1400.0,
);
assert!(
coasting.dx(5.0) > 0.0,
"drag alone only reaches nought at infinity"
);
assert_eq!(stopping.dx(5.0), 0.0);
let rest = stopping.final_x();
assert_eq!(stopping.x(5.0), rest, "and it stays where it stopped");
assert_eq!(stopping.x(50.0), rest);
assert!(
rest < coasting.final_x(),
"and it stops short of where coasting would have carried it: {rest} vs {}",
coasting.final_x()
);
}
#[test]
fn the_stopping_time_is_where_the_velocity_runs_out() {
let sim = FrictionSimulation::with_constant_deceleration(
0.135,
0.0,
2000.0,
Tolerance::PIXELS,
1400.0,
);
let rest = sim.final_x();
let mut t = 0.0;
while sim.dx(t) > 0.0 && t < 5.0 {
t += 0.001;
}
assert!(t > 0.0 && t < 5.0, "it stops, and not immediately: {t}");
assert!(sim.dx(t - 0.002) > 0.0, "still moving a step before");
assert!((sim.x(t) - rest).abs() < 1.0, "and it is at rest there");
}
#[test]
fn the_crossing_instant_survives_the_constant_term() {
let sim = FrictionSimulation::with_constant_deceleration(
0.135,
0.0,
2000.0,
Tolerance::PIXELS,
1400.0,
);
let target = sim.final_x() * 0.5;
let t = sim.time_at_x(target);
assert!(t.is_finite() && t > 0.0, "reached, at {t}");
assert!(
(sim.x(t) - target).abs() < 0.5,
"and x({t}) is {target}: got {}",
sim.x(t)
);
}
#[test]
fn a_backward_fling_decelerates_backwards() {
let sim = FrictionSimulation::with_constant_deceleration(
0.135,
0.0,
-2000.0,
Tolerance::PIXELS,
1400.0,
);
assert!(sim.final_x() < 0.0, "it travelled backwards");
assert_eq!(sim.dx(5.0), 0.0, "and it stopped");
let forward = FrictionSimulation::with_constant_deceleration(
0.135,
0.0,
2000.0,
Tolerance::PIXELS,
1400.0,
);
assert!(
(sim.final_x() + forward.final_x()).abs() < 0.5,
"the same distance either way: {} vs {}",
sim.final_x(),
forward.final_x()
);
}
}