use std::collections::VecDeque;
pub const TOUCH_SLOP: f64 = 18.0;
pub const MOUSE_SLOP: f64 = 3.0;
pub const WHEEL_LINE_PX: f64 = 40.0;
pub const FLING_DECAY: f64 = 0.998;
pub const FLING_STOP: f64 = 30.0;
pub const VELOCITY_WINDOW_MS: f64 = 100.0;
pub fn fling_decay(v0: f64, elapsed_ms: f64) -> f64 {
v0 * FLING_DECAY.powf(elapsed_ms)
}
pub fn fling_displacement(v0: f64, elapsed_ms: f64) -> f64 {
(v0 / 1000.0) * (FLING_DECAY.powf(elapsed_ms) - 1.0) / FLING_DECAY.ln()
}
#[derive(Clone, Debug, Default)]
pub struct VelocityTracker {
samples: VecDeque<(f64, f64)>,
}
impl VelocityTracker {
pub fn new() -> Self {
Self {
samples: VecDeque::new(),
}
}
pub fn clear(&mut self) {
self.samples.clear();
}
pub fn record(&mut self, time_ms: f64, position: f64) {
self.samples.push_back((time_ms, position));
let cutoff = time_ms - VELOCITY_WINDOW_MS;
while let Some(&(t, _)) = self.samples.front() {
if t < cutoff {
self.samples.pop_front();
} else {
break;
}
}
}
pub fn velocity(&self) -> f64 {
if self.samples.len() < 2 {
return 0.0;
}
let (t0, p0) = *self.samples.front().unwrap();
let (t1, p1) = *self.samples.back().unwrap();
let dt = t1 - t0;
if dt <= 0.0 {
return 0.0;
}
(p1 - p0) / dt * 1000.0
}
pub fn len(&self) -> usize {
self.samples.len()
}
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fling_decay_halves_in_about_347ms() {
let v = fling_decay(1000.0, 347.0);
assert!((v - 500.0).abs() < 1.0, "expected ~500, got {v}");
assert_eq!(fling_decay(1000.0, 0.0), 1000.0);
}
#[test]
fn fling_displacement_matches_numeric_integration() {
let v0 = 800.0;
let total_ms = 500.0;
let mut numeric = 0.0;
let step = 1.0;
let mut t = 0.0;
while t < total_ms {
numeric += fling_decay(v0, t) / 1000.0 * step;
t += step;
}
let closed = fling_displacement(v0, total_ms);
let rel_err = (closed - numeric).abs() / numeric.abs();
assert!(rel_err < 0.01, "closed {closed} vs numeric {numeric}");
assert!(closed > 0.0);
}
#[test]
fn fling_displacement_sign_follows_velocity() {
assert!(fling_displacement(500.0, 100.0) > 0.0);
assert!(fling_displacement(-500.0, 100.0) < 0.0);
assert_eq!(fling_displacement(0.0, 100.0), 0.0);
}
#[test]
fn velocity_is_delta_over_window() {
let mut vt = VelocityTracker::new();
vt.record(0.0, 100.0);
vt.record(50.0, 50.0); assert!((vt.velocity() - (-1000.0)).abs() < 1e-9);
}
#[test]
fn velocity_empty_or_single_sample_is_zero() {
let mut vt = VelocityTracker::new();
assert_eq!(vt.velocity(), 0.0);
vt.record(0.0, 10.0);
assert_eq!(vt.velocity(), 0.0);
assert!(!vt.is_empty());
}
#[test]
fn old_samples_are_pruned_from_the_window() {
let mut vt = VelocityTracker::new();
vt.record(0.0, 0.0);
vt.record(50.0, 10.0);
vt.record(200.0, 40.0);
assert_eq!(vt.len(), 1);
assert_eq!(vt.velocity(), 0.0); }
#[test]
fn clear_resets_the_tracker() {
let mut vt = VelocityTracker::new();
vt.record(0.0, 1.0);
vt.record(10.0, 2.0);
vt.clear();
assert!(vt.is_empty());
assert_eq!(vt.velocity(), 0.0);
}
}