Files
feresaria/crates/ferese-animation/src/gesture.rs
T
Samuel Onoja a6f24e8188 Harden desktop recovery and pre-release stability checks
Bound IPC transport deadlines while preserving portal leases, supervise desktop helper and agent recovery, and reserve final floating window geometry during placement.

Stabilize CLI output and isolated tests, expand workspace CI, resolve formatting and lint failures, and bundle the MIT license with installer compatibility.
2026-10-07 09:08:23 +01:00

137 lines
5.2 KiB
Rust

//! Input estimation and destination selection; all post-release motion uses springs.
use std::{collections::VecDeque, 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();
}
}
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()))
.unwrap();
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
}
}
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.
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);
}
}