Files
feresaria/crates/ferese-animation/src/gesture.rs
T

154 lines
5.4 KiB
Rust

use std::collections::VecDeque;
use std::time::Duration;
#[derive(Default, Debug)]
pub struct VelocityTracker {
samples: VecDeque<(Duration, f64)>,
}
impl VelocityTracker {
pub fn push(&mut self, time: Duration, position: f64) {
if !position.is_finite() {
return;
}
if let Some(&(last, _)) = self.samples.back() {
if time < last {
return;
}
if time == last {
self.samples.pop_back();
}
}
self.samples.push_back((time, position));
while self
.samples
.front()
.is_some_and(|(t, _)| time.saturating_sub(*t) > Duration::from_millis(100))
|| self.samples.len() > 32
{
self.samples.pop_front();
}
}
#[must_use]
pub fn velocity(&self, now: Duration) -> f64 {
let Some(&(last, origin)) = self.samples.back() else {
return 0.0;
};
if now.saturating_sub(last) > Duration::from_millis(40) || self.samples.len() < 2 {
return 0.0;
}
// Fit positions relative to the newest sample, with time normalized to
// the history horizon to avoid ill-conditioned powers of tiny seconds.
let degree = if self.samples.len() >= 3 { 2 } else { 1 };
let mut matrix = [[0.0; 4]; 3];
for &(time, position) in &self.samples {
let t = -last.saturating_sub(time).as_secs_f64() / 0.1;
let powers = [1.0, t, t * t, t * t * t, t * t * t * t];
for row in 0..=degree {
for col in 0..=degree {
matrix[row][col] += powers[row + col];
}
matrix[row][degree + 1] += powers[row] * (position - origin);
}
}
for col in 0..=degree {
let pivot = (col..=degree)
.max_by(|&a, &b| matrix[a][col].abs().total_cmp(&matrix[b][col].abs()))
.expect("the inclusive pivot range contains at least the current column");
matrix.swap(col, pivot);
let divisor = matrix[col][col];
if divisor.abs() < 1e-12 {
let (first, position) = self.samples[0];
let dt = last.saturating_sub(first).as_secs_f64();
return if dt > 0.0 {
(origin - position) / dt
} else {
0.0
};
}
for entry in &mut matrix[col][col..=degree + 1] {
*entry /= divisor;
}
let pivot_row = matrix[col];
for (row, values) in matrix.iter_mut().enumerate().take(degree + 1) {
if row == col {
continue;
}
let factor = values[col];
for (entry, pivot) in values[col..=degree + 1]
.iter_mut()
.zip(&pivot_row[col..=degree + 1])
{
*entry -= factor * pivot;
}
}
}
matrix[1][degree + 1] / 0.1
}
}
#[must_use]
pub fn project(position: f64, velocity: f64, rate: f64) -> f64 {
if !velocity.is_finite() || !(0.0..1.0).contains(&rate) || rate == 0.0 {
return position;
}
position - velocity / (1000.0 * rate.ln())
}
/// Nonlinear resistance, bounded by `extent`; the derivative transfers the
/// velocity of the displayed (resisted) position into the release spring.
#[must_use]
pub fn rubber_band(distance: f64, extent: f64, resistance: f64) -> (f64, f64) {
if !distance.is_finite()
|| !extent.is_finite()
|| extent <= 0.0
|| !resistance.is_finite()
|| resistance <= 0.0
{
return (0.0, 0.0);
}
let denominator = 1.0 + resistance * distance.abs() / extent;
(
distance.signum() * extent * (1.0 - denominator.recip()),
resistance / denominator.powi(2),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rubber_band_is_bounded_symmetric_and_has_matching_velocity() {
for x in [0.0, 1.0, 100.0, 10000.0] {
let (position, derivative) = rubber_band(x, 64.0, 0.55);
assert!((0.0..64.0).contains(&position));
assert_eq!(rubber_band(-x, 64.0, 0.55).0, -position);
let measured = (rubber_band(x + 0.0001, 64.0, 0.55).0
- rubber_band(x - 0.0001, 64.0, 0.55).0)
/ 0.0002;
assert!((measured - derivative).abs() < 1e-6);
}
}
#[test]
fn quadratic_velocity_and_pause() {
let mut tracker = VelocityTracker::default();
for ms in [0, 7, 19, 40, 60, 90] {
let t = ms as f64 / 1000.0;
tracker.push(Duration::from_millis(ms), 100.0 * t + 200.0 * t * t);
}
assert!((tracker.velocity(Duration::from_millis(90)) - 136.0).abs() < 1e-8);
assert_eq!(tracker.velocity(Duration::from_millis(140)), 0.0);
assert!((project(0.0, 1.0, 0.997) - 0.332833082957).abs() < 1e-10);
}
#[test]
fn duplicate_and_out_of_order_samples() {
let mut tracker = VelocityTracker::default();
tracker.push(Duration::ZERO, 0.0);
tracker.push(Duration::from_millis(10), 1.0);
tracker.push(Duration::from_millis(10), 2.0);
tracker.push(Duration::from_millis(5), 99.0);
assert!((tracker.velocity(Duration::from_millis(10)) - 200.0).abs() < 1e-10);
}
}