use bevy::{
math::curve::{Curve, Interval},
prelude::*,
};
use crate::sequence::Key;
pub(crate) fn eased_segment(a: &Key, b: &Key, t: f32) -> (Vec3, Option<Quat>) {
let w = eased_fraction(a, b, t);
let pos = a.pos.lerp(b.pos, w);
let rot = match (a.rot, b.rot) {
(Some(qa), Some(qb)) => Some(qa.slerp(qb, w)),
_ => None,
};
(pos, rot)
}
pub(crate) fn eased_fraction(a: &Key, b: &Key, t: f32) -> f32 {
let span = (b.time - a.time).max(f32::EPSILON);
a.ease.sample_clamped(((t - a.time) / span).clamp(0.0, 1.0))
}
pub struct ArcLengthLut {
lengths: Vec<f32>,
domain: Interval,
}
impl ArcLengthLut {
pub fn new(curve: &impl Curve<Vec3>, samples: usize) -> Self {
let samples = samples.max(2);
let domain = curve.domain();
let mut lengths = Vec::with_capacity(samples);
lengths.push(0.0);
let mut prev = curve.sample_clamped(domain.start());
let mut total = 0.0;
for i in 1..samples {
let t = domain.start() + domain.length() * i as f32 / (samples - 1) as f32;
let p = curve.sample_clamped(t);
total += p.distance(prev);
lengths.push(total);
prev = p;
}
Self { lengths, domain }
}
pub fn total_length(&self) -> f32 {
*self.lengths.last().unwrap_or(&0.0)
}
pub fn t_at_length(&self, s: f32) -> f32 {
let total = self.total_length();
if total <= 0.0 {
return self.domain.start();
}
let s = s.clamp(0.0, total);
let i = self.lengths.partition_point(|&l| l < s);
let steps = (self.lengths.len() - 1) as f32;
let frac = if i == 0 {
0.0
} else {
let (l0, l1) = (self.lengths[i - 1], self.lengths[i]);
let inside = if l1 > l0 { (s - l0) / (l1 - l0) } else { 0.0 };
(i as f32 - 1.0 + inside) / steps
};
self.domain.start() + self.domain.length() * frac
}
pub fn t_at_fraction(&self, fraction: f32) -> f32 {
self.t_at_length(self.total_length() * fraction.clamp(0.0, 1.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::math::curve::FunctionCurve;
use std::f32::consts::TAU;
#[test]
fn eased_segment_matches_easefunction_samples() {
let a = Key {
time: 1.0,
pos: Vec3::ZERO,
rot: Some(Quat::IDENTITY),
ease: EaseFunction::SmoothStep,
};
let b = Key {
time: 3.0,
pos: Vec3::new(10.0, 0.0, 0.0),
rot: Some(Quat::from_rotation_y(1.0)),
ease: EaseFunction::Linear,
};
let (pos, rot) = eased_segment(&a, &b, 2.0);
assert!((pos.x - 5.0).abs() < 1e-5);
let (pos, _) = eased_segment(&a, &b, 1.5);
assert!((pos.x - 1.5625).abs() < 1e-4);
let (pos, _) = eased_segment(&a, &b, 0.0);
assert!(pos.x.abs() < 1e-6);
let (pos, rot_end) = eased_segment(&a, &b, 9.0);
assert!((pos.x - 10.0).abs() < 1e-5);
assert!(rot.is_some() && rot_end.is_some());
}
#[test]
fn missing_rotation_on_either_side_yields_none() {
let a = Key {
time: 0.0,
pos: Vec3::ZERO,
rot: None,
ease: EaseFunction::Linear,
};
let b = Key {
time: 1.0,
pos: Vec3::X,
rot: Some(Quat::IDENTITY),
ease: EaseFunction::Linear,
};
assert!(eased_segment(&a, &b, 0.5).1.is_none());
}
#[test]
fn arclength_lut_matches_analytic_circle() {
let radius = 3.0;
let circle = FunctionCurve::new(Interval::UNIT, move |t: f32| {
Vec3::new((t * TAU).cos(), (t * TAU).sin(), 0.0) * radius
});
let lut = ArcLengthLut::new(&circle, 512);
let circumference = TAU * radius;
assert!(
(lut.total_length() - circumference).abs() / circumference < 1e-3,
"lut total {} vs analytic {}",
lut.total_length(),
circumference
);
let t_half = lut.t_at_length(circumference / 2.0);
assert!((t_half - 0.5).abs() < 1e-3);
}
#[test]
fn constant_speed_fractions_are_equidistant_on_uneven_curves() {
let line = FunctionCurve::new(Interval::UNIT, |t: f32| {
Vec3::new(t * t * t * 10.0, 0.0, 0.0)
});
let lut = ArcLengthLut::new(&line, 512);
let points: Vec<Vec3> = (0..=10)
.map(|i| line.sample_clamped(lut.t_at_fraction(i as f32 / 10.0)))
.collect();
let gaps: Vec<f32> = points.windows(2).map(|p| p[0].distance(p[1])).collect();
let mean = gaps.iter().sum::<f32>() / gaps.len() as f32;
for gap in gaps {
assert!(
(gap - mean).abs() / mean < 0.02,
"gap {gap} strays from mean {mean}"
);
}
}
}