#[derive(Debug, Clone, Copy)]
pub struct Decay {
pub velocity: f64,
pub friction: f64,
pub rest_threshold: f64,
pub min_bound: Option<f64>,
pub max_bound: Option<f64>,
pub bounce_stiffness: f64,
pub bounce_damping: f64,
}
impl Decay {
pub fn new(velocity: f64) -> Self {
Self {
velocity,
friction: 0.998,
rest_threshold: 0.01,
min_bound: None,
max_bound: None,
bounce_stiffness: 400.0,
bounce_damping: 30.0,
}
}
pub fn friction(mut self, f: f64) -> Self {
self.friction = f.clamp(0.0, 1.0);
self
}
pub fn rest_threshold(mut self, t: f64) -> Self {
self.rest_threshold = t.abs();
self
}
pub fn bounds(mut self, min: f64, max: f64) -> Self {
self.min_bound = Some(min);
self.max_bound = Some(max);
self
}
pub fn bounce_stiffness(mut self, s: f64) -> Self {
self.bounce_stiffness = s;
self
}
pub fn bounce_damping(mut self, d: f64) -> Self {
self.bounce_damping = d;
self
}
pub fn evaluate(&self, t: f64) -> (f64, f64) {
if self.friction >= 1.0 || t < 0.0 {
return (0.0, self.velocity);
}
let frames = t * 60.0;
let velocity = self.velocity * self.friction.powf(frames);
let ln_friction = self.friction.ln();
let position = if ln_friction.abs() < 1e-10 {
self.velocity * t
} else {
self.velocity * (self.friction.powf(frames) - 1.0) / (60.0 * ln_friction)
};
if self.min_bound.is_some() || self.max_bound.is_some() {
self.apply_bounds(position, velocity, t)
} else {
(position, velocity)
}
}
fn apply_bounds(&self, position: f64, velocity: f64, _t: f64) -> (f64, f64) {
let min = self.min_bound.unwrap_or(f64::NEG_INFINITY);
let max = self.max_bound.unwrap_or(f64::INFINITY);
if position < min {
let overshoot = min - position;
let resistance = self.calculate_spring_resistance(overshoot, velocity);
let bounded_pos = min - overshoot * resistance;
let damped_vel = velocity * resistance;
(bounded_pos, damped_vel)
} else if position > max {
let overshoot = position - max;
let resistance = self.calculate_spring_resistance(overshoot, velocity);
let bounded_pos = max + overshoot * resistance;
let damped_vel = velocity * resistance;
(bounded_pos, damped_vel)
} else {
(position, velocity)
}
}
fn calculate_spring_resistance(&self, overshoot: f64, _velocity: f64) -> f64 {
let spring_factor = self.bounce_stiffness / 1000.0; (-overshoot * spring_factor).exp().clamp(0.0, 1.0)
}
pub fn is_at_rest(&self, t: f64) -> bool {
let (_, velocity) = self.evaluate(t);
velocity.abs() < self.rest_threshold
}
pub fn estimated_duration(&self) -> f64 {
if self.velocity.abs() < self.rest_threshold {
return 0.0;
}
if self.friction >= 1.0 || self.friction <= 0.0 {
return f64::INFINITY;
}
let ln_friction = self.friction.ln();
if ln_friction.abs() < 1e-10 {
return f64::INFINITY;
}
let frames = (self.rest_threshold / self.velocity.abs()).ln() / ln_friction;
(frames / 60.0).max(0.0)
}
pub fn final_position(&self) -> f64 {
if self.friction >= 1.0 || self.friction <= 0.0 {
return f64::INFINITY * self.velocity.signum();
}
let ln_friction = self.friction.ln();
if ln_friction.abs() < 1e-10 {
return f64::INFINITY * self.velocity.signum();
}
-self.velocity / (60.0 * ln_friction)
}
pub fn ios_scroll(velocity: f64) -> Self {
Self::new(velocity).friction(0.998)
}
pub fn heavy(velocity: f64) -> Self {
Self::new(velocity).friction(0.99)
}
pub fn light(velocity: f64) -> Self {
Self::new(velocity).friction(0.999)
}
}
impl Default for Decay {
fn default() -> Self {
Self::new(0.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_velocity_immediate_rest() {
let decay = Decay::new(0.0);
assert!(decay.is_at_rest(0.0));
assert!(decay.is_at_rest(1.0));
let (pos, vel) = decay.evaluate(0.0);
assert_eq!(pos, 0.0);
assert_eq!(vel, 0.0);
}
#[test]
fn positive_velocity_decelerates() {
let decay = Decay::new(100.0).friction(0.95);
let (pos1, vel1) = decay.evaluate(0.0);
let (pos2, vel2) = decay.evaluate(0.5);
let (pos3, vel3) = decay.evaluate(1.0);
assert!(vel1 > vel2);
assert!(vel2 > vel3);
assert!(pos1 < pos2);
assert!(pos2 < pos3);
assert!((vel1 - 100.0).abs() < 0.1);
}
#[test]
fn final_position_is_finite() {
let decay = Decay::new(500.0).friction(0.998);
let final_pos = decay.final_position();
assert!(final_pos.is_finite());
assert!(final_pos > 0.0);
let duration = decay.estimated_duration();
assert!(duration.is_finite());
assert!(duration > 0.0);
}
#[test]
fn higher_friction_shorter_distance() {
let light = Decay::new(500.0).friction(0.999);
let heavy = Decay::new(500.0).friction(0.99);
let light_final = light.final_position();
let heavy_final = heavy.final_position();
assert!(heavy_final < light_final);
let light_duration = light.estimated_duration();
let heavy_duration = heavy.estimated_duration();
assert!(heavy_duration < light_duration);
}
#[test]
fn bounds_contain_position() {
let decay = Decay::new(500.0) .friction(0.998)
.bounds(0.0, 100.0);
for i in 0..20 {
let t = i as f64 * 0.1;
let (pos, _vel) = decay.evaluate(t);
assert!(
pos >= -10.0 && pos <= 110.0,
"Position {} out of range at t={}",
pos, t
);
}
let final_pos = decay.final_position();
if final_pos.is_finite() {
assert!(final_pos > 100.0); }
}
#[test]
fn estimated_duration_positive() {
let decay = Decay::new(100.0).friction(0.998);
let duration = decay.estimated_duration();
assert!(duration > 0.0);
assert!(duration.is_finite());
let (_, vel) = decay.evaluate(duration);
assert!(vel.abs() < decay.rest_threshold * 2.0);
}
#[test]
fn rest_detection_works() {
let decay = Decay::new(100.0)
.friction(0.95)
.rest_threshold(0.1);
assert!(!decay.is_at_rest(0.0));
let duration = decay.estimated_duration();
assert!(decay.is_at_rest(duration * 1.5));
assert!(decay.is_at_rest(duration * 2.0));
}
#[test]
fn presets_have_different_behaviors() {
let velocity = 500.0;
let heavy = Decay::heavy(velocity);
let ios = Decay::ios_scroll(velocity);
let light = Decay::light(velocity);
let heavy_dist = heavy.final_position();
let ios_dist = ios.final_position();
let light_dist = light.final_position();
assert!(heavy_dist < ios_dist);
assert!(ios_dist < light_dist);
}
#[test]
fn negative_velocity_decelerates_backward() {
let decay = Decay::new(-200.0).friction(0.98);
let (pos1, vel1) = decay.evaluate(0.5);
let (pos2, vel2) = decay.evaluate(1.0);
assert!(pos1 < 0.0);
assert!(pos2 < pos1);
assert!(vel1 < 0.0);
assert!(vel2 < 0.0);
assert!(vel1 < vel2); }
#[test]
fn friction_near_one_behaves_reasonably() {
let decay = Decay::new(100.0).friction(0.9999);
let (pos, vel) = decay.evaluate(0.1);
assert!(pos.is_finite());
assert!(vel.is_finite());
assert!(pos > 0.0);
assert!(vel > 0.0);
}
}