use incular_core::time::Instant;
use std::collections::VecDeque;
use incular_core::Offset;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub(crate) struct VelocityEstimate {
pub(crate) pixels_per_second: Offset,
pub(crate) offset: Offset,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum VelocityTrackerKind {
#[default]
LeastSquares,
IosScrollViewFling,
MacOsScrollViewFling,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct VelocityTracker {
samples: VecDeque<(Instant, Offset)>,
kind: VelocityTrackerKind,
}
impl VelocityTracker {
const HISTORY_SIZE: usize = 20;
const HORIZON_MILLISECONDS: f64 = 100.0;
const ASSUME_POINTER_MOVE_STOPPED_MILLISECONDS: f64 = 40.0;
const MIN_SAMPLE_SIZE: usize = 3;
pub(crate) fn new(kind: VelocityTrackerKind) -> Self {
Self {
samples: VecDeque::new(),
kind,
}
}
pub(crate) const fn kind(&self) -> VelocityTrackerKind {
self.kind
}
pub(crate) fn add_position(&mut self, time: Instant, position: Offset) {
if self.samples.len() == Self::HISTORY_SIZE {
self.samples.pop_front();
}
self.samples.push_back((time, position));
}
pub(crate) fn estimate_at(&self, now: Instant) -> VelocityEstimate {
let Some(&(newest_time, newest_point)) = self.samples.back() else {
return VelocityEstimate::default();
};
if now.saturating_duration_since(newest_time).as_millis() as f64
> Self::ASSUME_POINTER_MOVE_STOPPED_MILLISECONDS
{
return VelocityEstimate::default();
}
let weights = match self.kind {
VelocityTrackerKind::LeastSquares => None,
VelocityTrackerKind::IosScrollViewFling => Some([0.6, 0.35, 0.05]),
VelocityTrackerKind::MacOsScrollViewFling => Some([0.15, 0.65, 0.2]),
};
if let Some([earliest, previous, latest]) = weights {
let oldest_point = self.samples.front().map_or(newest_point, |sample| sample.1);
let [a, b, c] = [
self.previous_velocity_at(2),
self.previous_velocity_at(1),
self.previous_velocity_at(0),
];
return VelocityEstimate {
pixels_per_second: Offset::new(
a.x * earliest + b.x * previous + c.x * latest,
a.y * earliest + b.y * previous + c.y * latest,
),
offset: Offset::new(
newest_point.x - oldest_point.x,
newest_point.y - oldest_point.y,
),
};
}
let mut x = Vec::new();
let mut y = Vec::new();
let mut time = Vec::new();
let mut oldest_point = newest_point;
let mut previous_time = newest_time;
for &(sample_time, point) in self.samples.iter().rev() {
let age = micros_between(sample_time, newest_time) / 1000.0;
let delta = micros_between(previous_time, sample_time).abs() / 1000.0;
previous_time = sample_time;
if age > Self::HORIZON_MILLISECONDS
|| delta > Self::ASSUME_POINTER_MOVE_STOPPED_MILLISECONDS
{
break;
}
oldest_point = point;
x.push(f64::from(point.x));
y.push(f64::from(point.y));
time.push(-age);
}
let offset = Offset::new(
newest_point.x - oldest_point.x,
newest_point.y - oldest_point.y,
);
if time.len() >= Self::MIN_SAMPLE_SIZE
&& let (Some(slope_x), Some(slope_y)) =
(solve_linear_slope(&time, &x), solve_linear_slope(&time, &y))
{
return VelocityEstimate {
pixels_per_second: Offset::new(
(slope_x * 1000.0) as f32,
(slope_y * 1000.0) as f32,
),
offset,
};
}
VelocityEstimate {
pixels_per_second: Offset::ZERO,
offset,
}
}
}
impl VelocityTracker {
fn previous_velocity_at(&self, back: usize) -> Offset {
let len = self.samples.len();
let (Some(end), Some(start)) = (
len.checked_sub(1 + back).map(|index| self.samples[index]),
len.checked_sub(2 + back).map(|index| self.samples[index]),
) else {
return Offset::ZERO;
};
let dt = micros_between(start.0, end.0);
if dt <= 0.0 {
return Offset::ZERO;
}
let scale = (1000.0 / (dt / 1000.0)) as f32;
Offset::new((end.1.x - start.1.x) * scale, (end.1.y - start.1.y) * scale)
}
}
fn micros_between(from: Instant, to: Instant) -> f64 {
if to >= from {
to.duration_since(from).as_micros() as f64
} else {
-(from.duration_since(to).as_micros() as f64)
}
}
fn solve_linear_slope(x: &[f64], y: &[f64]) -> Option<f64> {
const PRECISION_ERROR_TOLERANCE: f64 = 1e-10;
let m = x.len();
let a = [
vec![1.0; m],
x.to_vec(),
x.iter().map(|value| value * value).collect::<Vec<f64>>(),
];
let mut q = [vec![0.0; m], vec![0.0; m], vec![0.0; m]];
let mut r = [[0.0_f64; 3]; 3];
for j in 0..3 {
q[j].copy_from_slice(&a[j]);
for i in 0..j {
let previous = q[i].clone();
let projection = dot(&q[j], &previous);
for (value, basis) in q[j].iter_mut().zip(&previous) {
*value -= projection * basis;
}
}
let norm = dot(&q[j], &q[j]).sqrt();
if norm < PRECISION_ERROR_TOLERANCE {
return None;
}
let inverse_norm = 1.0 / norm;
for value in q[j].iter_mut() {
*value *= inverse_norm;
}
for (i, column) in a.iter().enumerate() {
r[j][i] = if i < j { 0.0 } else { dot(&q[j], column) };
}
}
let mut coefficients = [0.0_f64; 3];
for i in (0..3).rev() {
coefficients[i] = dot(&q[i], y);
for j in ((i + 1)..3).rev() {
coefficients[i] -= r[i][j] * coefficients[j];
}
coefficients[i] /= r[i][i];
}
Some(coefficients[1])
}
fn dot(left: &[f64], right: &[f64]) -> f64 {
left.iter().zip(right).fold(0.0, |sum, (a, b)| sum + a * b)
}