use std::collections::VecDeque;
use std::fmt::Debug;
pub fn hermite_scalar(p0: f32, v0: f32, p1: f32, v1: f32, t: f32, seconds: f32) -> f32 {
let t2 = t * t;
let t3 = t2 * t;
let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
let h10 = t3 - 2.0 * t2 + t;
let h01 = -2.0 * t3 + 3.0 * t2;
let h11 = t3 - t2;
h00 * p0 + h10 * seconds * v0 + h01 * p1 + h11 * seconds * v1
}
pub trait HermiteInterpolatable<Velocity>: Clone {
fn hermite(&self, other: &Self, velocity_a: &Velocity, velocity_b: &Velocity, t: f32, seconds: f32) -> Self;
}
impl HermiteInterpolatable<f32> for f32 {
fn hermite(&self, other: &Self, va: &f32, vb: &f32, t: f32, seconds: f32) -> Self {
hermite_scalar(*self, *va, *other, *vb, t, seconds)
}
}
impl HermiteInterpolatable<crate::math::Vec2> for crate::math::Vec2 {
fn hermite(&self, other: &Self, va: &Self, vb: &Self, t: f32, seconds: f32) -> Self {
Self {
x: hermite_scalar(self.x, va.x, other.x, vb.x, t, seconds),
y: hermite_scalar(self.y, va.y, other.y, vb.y, t, seconds),
}
}
}
impl HermiteInterpolatable<crate::math::Vec3> for crate::math::Vec3 {
fn hermite(&self, other: &Self, va: &Self, vb: &Self, t: f32, seconds: f32) -> Self {
Self {
x: hermite_scalar(self.x, va.x, other.x, vb.x, t, seconds),
y: hermite_scalar(self.y, va.y, other.y, vb.y, t, seconds),
z: hermite_scalar(self.z, va.z, other.z, vb.z, t, seconds),
}
}
}
#[derive(Debug, Clone)]
pub struct HermiteView<State, Velocity> {
samples: VecDeque<(u64, State, Velocity)>,
capacity: usize,
}
impl<State, Velocity> HermiteView<State, Velocity>
where
State: HermiteInterpolatable<Velocity> + Clone + Debug,
Velocity: Clone + Debug,
{
pub fn new(capacity: usize) -> Self {
assert!(capacity >= 2, "a HermiteView needs at least two samples");
Self {
samples: VecDeque::with_capacity(capacity),
capacity,
}
}
pub fn push(&mut self, time_ms: u64, state: State, velocity: Velocity) {
let at = self.samples.iter().position(|(t, _, _)| *t > time_ms);
match at {
Some(index) => self.samples.insert(index, (time_ms, state, velocity)),
None => self.samples.push_back((time_ms, state, velocity)),
}
while self.samples.len() > self.capacity {
self.samples.pop_front();
}
}
pub fn render(&self, target_ms: u64) -> Option<State> {
if self.samples.len() == 1 {
return self.samples.front().map(|(_, s, _)| s.clone());
}
let mut bracket = None;
for pair in 0..self.samples.len().saturating_sub(1) {
let (t0, ..) = self.samples[pair];
let (t1, ..) = self.samples[pair + 1];
if target_ms >= t0 && target_ms <= t1 {
bracket = Some(pair);
break;
}
}
let Some(pair) = bracket else {
let (first, _, _) = self.samples.front()?;
return if target_ms < *first {
self.samples.front().map(|(_, s, _)| s.clone())
} else {
self.samples.back().map(|(_, s, _)| s.clone())
};
};
let (t0, ref s0, ref v0) = self.samples[pair];
let (t1, ref s1, ref v1) = self.samples[pair + 1];
let span = t1.saturating_sub(t0);
if span == 0 {
return Some(s1.clone());
}
let t = (target_ms - t0) as f32 / span as f32;
Some(s0.hermite(s1, v0, v1, t, span as f32 / 1000.0))
}
pub fn latest(&self) -> Option<&State> {
self.samples.back().map(|(_, s, _)| s)
}
pub fn oldest_time(&self) -> Option<u64> {
self.samples.front().map(|(t, _, _)| *t)
}
pub fn latest_time(&self) -> Option<u64> {
self.samples.back().map(|(t, _, _)| *t)
}
pub fn len(&self) -> usize {
self.samples.len()
}
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
pub fn clear(&mut self) {
self.samples.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::Vec2;
#[test]
fn the_spline_passes_through_both_samples() {
let mut view: HermiteView<f32, f32> = HermiteView::new(4);
view.push(0, 3.0, -2.0);
view.push(100, 9.0, 5.0);
assert!((view.render(0).unwrap() - 3.0).abs() < 1e-5);
assert!((view.render(100).unwrap() - 9.0).abs() < 1e-5);
}
#[test]
fn it_leaves_along_the_velocity_it_was_given() {
let mut view: HermiteView<f32, f32> = HermiteView::new(4);
view.push(0, 0.0, 0.0);
view.push(1000, 1.0, 0.0);
let just_after = view.render(20).unwrap();
assert!(just_after.abs() < 0.01, "left flat, not at a slope: {just_after}");
}
#[test]
fn on_a_curve_at_a_low_send_rate_it_beats_a_straight_line() {
let radius = 10.0f32;
let omega = std::f32::consts::TAU * 0.5; let position = |ms: u64| {
let s = ms as f32 / 1000.0;
Vec2::new(radius * (omega * s).cos(), radius * (omega * s).sin())
};
let velocity = |ms: u64| {
let s = ms as f32 / 1000.0;
Vec2::new(-radius * omega * (omega * s).sin(), radius * omega * (omega * s).cos())
};
let mut spline: HermiteView<Vec2, Vec2> = HermiteView::new(32);
for tick in 0..=10u64 {
let ms = tick * 100;
spline.push(ms, position(ms), velocity(ms));
}
let (mut worst_hermite, mut worst_linear) = (0.0f32, 0.0f32);
for ms in 0..=1000u64 {
let truth = position(ms);
let drawn = spline.render(ms).unwrap();
worst_hermite = worst_hermite.max((drawn - truth).length());
let lo = (ms / 100) * 100;
let hi = (lo + 100).min(1000);
let t = if hi == lo { 0.0 } else { (ms - lo) as f32 / (hi - lo) as f32 };
let (a, b) = (position(lo), position(hi));
let lerped = Vec2::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
worst_linear = worst_linear.max((lerped - truth).length());
}
println!(
"10Hz samples on a 10-unit circle, worst error over a second: hermite {worst_hermite:.4}, linear {worst_linear:.4} ({:.1}x)",
worst_linear / worst_hermite
);
assert!(
worst_hermite < worst_linear / 5.0,
"hermite {worst_hermite:.4} should be far under linear {worst_linear:.4}"
);
}
#[test]
fn outside_the_samples_it_holds_rather_than_guessing() {
let mut view: HermiteView<f32, f32> = HermiteView::new(4);
view.push(100, 1.0, 50.0);
view.push(200, 2.0, 50.0);
assert_eq!(view.render(50).unwrap(), 1.0);
assert_eq!(view.render(9999).unwrap(), 2.0);
}
#[test]
fn a_straggler_lands_in_time_order() {
let mut view: HermiteView<f32, f32> = HermiteView::new(8);
view.push(0, 0.0, 0.0);
view.push(200, 2.0, 0.0);
view.push(100, 1.0, 0.0);
assert_eq!(view.render(100).unwrap(), 1.0, "the late sample is used, not appended");
assert_eq!(view.latest_time(), Some(200));
}
#[test]
fn the_ring_forgets_the_oldest() {
let mut view: HermiteView<f32, f32> = HermiteView::new(2);
for tick in 0..5u64 {
view.push(tick * 10, tick as f32, 0.0);
}
assert_eq!(view.len(), 2);
assert_eq!(view.oldest_time(), Some(30));
}
#[test]
fn one_sample_renders_as_itself() {
let mut view: HermiteView<f32, f32> = HermiteView::new(4);
assert!(view.render(0).is_none());
view.push(10, 7.0, 1.0);
assert_eq!(view.render(9999).unwrap(), 7.0);
}
}