use std::time::Instant;
const SAMPLE_CAPACITY: usize = 6;
const DEFAULT_FRICTION: f32 = 4.8;
const MIN_VELOCITY_CUTOFF: f32 = 1.0;
const MAX_FLING_WINDOW_SECS: f32 = 0.12;
#[derive(Clone, Debug)]
pub struct KineticTracker {
friction: f32,
velocity: (f32, f32),
samples: [(f32, f32, Instant); SAMPLE_CAPACITY],
sample_count: usize,
sample_idx: usize,
}
impl Default for KineticTracker {
fn default() -> Self {
Self::new(DEFAULT_FRICTION)
}
}
impl KineticTracker {
pub fn new(friction: f32) -> Self {
let now = Instant::now();
Self {
friction: friction.max(0.1),
velocity: (0.0, 0.0),
samples: [(0.0, 0.0, now); SAMPLE_CAPACITY],
sample_count: 0,
sample_idx: 0,
}
}
pub fn on_press(&mut self, x: f32, y: f32) {
let now = Instant::now();
self.velocity = (0.0, 0.0);
self.samples[0] = (x, y, now);
self.sample_count = 1;
self.sample_idx = 1;
}
pub fn on_move(&mut self, x: f32, y: f32) {
let now = Instant::now();
self.samples[self.sample_idx] = (x, y, now);
self.sample_idx = (self.sample_idx + 1) % SAMPLE_CAPACITY;
if self.sample_count < SAMPLE_CAPACITY {
self.sample_count += 1;
}
}
pub fn on_release(&mut self) -> (f32, f32) {
if self.sample_count < 2 {
self.sample_count = 0;
self.velocity = (0.0, 0.0);
return (0.0, 0.0);
}
let now = Instant::now();
let newest_idx = (self.sample_idx + SAMPLE_CAPACITY - 1) % SAMPLE_CAPACITY;
let newest = self.samples[newest_idx];
if now.duration_since(newest.2).as_secs_f32() > MAX_FLING_WINDOW_SECS {
self.sample_count = 0;
self.velocity = (0.0, 0.0);
return (0.0, 0.0);
}
let mut oldest = newest;
for i in 1..self.sample_count {
let idx = (self.sample_idx + SAMPLE_CAPACITY - 1 - i) % SAMPLE_CAPACITY;
let sample = self.samples[idx];
if newest.2.duration_since(sample.2).as_secs_f32() <= MAX_FLING_WINDOW_SECS {
oldest = sample;
} else {
break;
}
}
let dt = newest.2.duration_since(oldest.2).as_secs_f32();
if dt > 0.005 {
let vx = (newest.0 - oldest.0) / dt;
let vy = (newest.1 - oldest.1) / dt;
self.velocity = (vx, vy);
} else {
self.velocity = (0.0, 0.0);
}
self.sample_count = 0;
self.velocity
}
pub fn set_velocity(&mut self, vx: f32, vy: f32) {
self.velocity = (vx, vy);
}
pub fn velocity(&self) -> (f32, f32) {
self.velocity
}
pub fn set_friction(&mut self, friction: f32) {
self.friction = friction.max(0.1);
}
pub fn friction(&self) -> f32 {
self.friction
}
pub fn update(&mut self, dt: f32) -> Option<(f32, f32)> {
if !self.is_active() {
self.velocity = (0.0, 0.0);
return None;
}
let dt_clamped = dt.clamp(0.001, 0.05);
let decay = (-self.friction * dt_clamped).exp();
let dx = self.velocity.0 * dt_clamped;
let dy = self.velocity.1 * dt_clamped;
self.velocity.0 *= decay;
self.velocity.1 *= decay;
if self.velocity.0.abs() <= MIN_VELOCITY_CUTOFF {
self.velocity.0 = 0.0;
}
if self.velocity.1.abs() <= MIN_VELOCITY_CUTOFF {
self.velocity.1 = 0.0;
}
Some((dx, dy))
}
pub fn is_active(&self) -> bool {
self.velocity.0.abs() > MIN_VELOCITY_CUTOFF || self.velocity.1.abs() > MIN_VELOCITY_CUTOFF
}
pub fn stop(&mut self) {
self.velocity = (0.0, 0.0);
self.sample_count = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::sleep;
use std::time::Duration;
#[test]
fn test_kinetic_velocity_and_decay() {
let mut tracker = KineticTracker::new(4.8);
assert!(!tracker.is_active());
tracker.on_press(0.0, 0.0);
sleep(Duration::from_millis(10));
tracker.on_move(50.0, 0.0);
sleep(Duration::from_millis(10));
tracker.on_move(100.0, 0.0);
let (vx, vy) = tracker.on_release();
assert!(vx > 1000.0, "vx should be > 1000 px/sec, got {}", vx);
assert_eq!(vy, 0.0);
assert!(tracker.is_active());
let delta1 = tracker.update(0.016);
assert!(delta1.is_some());
let (dx1, dy1) = delta1.unwrap();
assert!(dx1 > 0.0);
assert_eq!(dy1, 0.0);
let mut total_x = dx1;
while let Some((dx, _)) = tracker.update(0.016) {
total_x += dx;
}
assert!(!tracker.is_active());
assert_eq!(tracker.velocity(), (0.0, 0.0));
assert!(total_x > 0.0);
}
#[test]
fn test_pause_cancels_fling() {
let mut tracker = KineticTracker::new(4.8);
tracker.on_press(0.0, 0.0);
tracker.on_move(100.0, 0.0);
sleep(Duration::from_millis(150));
let (vx, vy) = tracker.on_release();
assert_eq!(vx, 0.0);
assert_eq!(vy, 0.0);
assert!(!tracker.is_active());
}
}