use glam::{Vec2, Vec3, Vec3A};
use std::{
fmt::Debug,
iter::Sum,
ops::{Add, Mul, Sub},
};
pub trait Point:
Mul<f32, Output = Self>
+ Add<Self, Output = Self>
+ Sub<Self, Output = Self>
+ Add<f32, Output = Self>
+ Sum
+ Default
+ Debug
+ Clone
+ PartialEq
+ Copy
{
}
impl Point for Vec3 {}
impl Point for Vec3A {}
impl Point for Vec2 {}
impl Point for f32 {}
pub struct Bezier<P: Point> {
control_points: Vec<[P; 4]>,
}
impl<P: Point> Bezier<P> {
pub fn new(control_points: impl Into<Vec<[P; 4]>>) -> Self {
Self {
control_points: control_points.into(),
}
}
}
impl<P: Point> CubicGenerator<P> for Bezier<P> {
#[inline]
fn to_curve(&self) -> CubicCurve<P> {
let char_matrix = [
[1., 0., 0., 0.],
[-3., 3., 0., 0.],
[3., -6., 3., 0.],
[-1., 3., -3., 1.],
];
let segments = self
.control_points
.iter()
.map(|p| CubicCurve::coefficients(*p, 1.0, char_matrix))
.collect();
CubicCurve { segments }
}
}
pub struct Hermite<P: Point> {
control_points: Vec<(P, P)>,
}
impl<P: Point> Hermite<P> {
pub fn new(
control_points: impl IntoIterator<Item = P>,
tangents: impl IntoIterator<Item = P>,
) -> Self {
Self {
control_points: control_points
.into_iter()
.zip(tangents.into_iter())
.collect(),
}
}
}
impl<P: Point> CubicGenerator<P> for Hermite<P> {
#[inline]
fn to_curve(&self) -> CubicCurve<P> {
let char_matrix = [
[1., 0., 0., 0.],
[0., 1., 0., 0.],
[-3., -2., 3., -1.],
[2., 1., -2., 1.],
];
let segments = self
.control_points
.windows(2)
.map(|p| {
let (p0, v0, p1, v1) = (p[0].0, p[0].1, p[1].0, p[1].1);
CubicCurve::coefficients([p0, v0, p1, v1], 1.0, char_matrix)
})
.collect();
CubicCurve { segments }
}
}
pub struct CardinalSpline<P: Point> {
tension: f32,
control_points: Vec<P>,
}
impl<P: Point> CardinalSpline<P> {
pub fn new(tension: f32, control_points: impl Into<Vec<P>>) -> Self {
Self {
tension,
control_points: control_points.into(),
}
}
pub fn new_catmull_rom(control_points: impl Into<Vec<P>>) -> Self {
Self {
tension: 0.5,
control_points: control_points.into(),
}
}
}
impl<P: Point> CubicGenerator<P> for CardinalSpline<P> {
#[inline]
fn to_curve(&self) -> CubicCurve<P> {
let s = self.tension;
let char_matrix = [
[0., 1., 0., 0.],
[-s, 0., s, 0.],
[2. * s, s - 3., 3. - 2. * s, -s],
[-s, 2. - s, s - 2., s],
];
let segments = self
.control_points
.windows(4)
.map(|p| CubicCurve::coefficients([p[0], p[1], p[2], p[3]], 1.0, char_matrix))
.collect();
CubicCurve { segments }
}
}
pub struct BSpline<P: Point> {
control_points: Vec<P>,
}
impl<P: Point> BSpline<P> {
pub fn new(control_points: impl Into<Vec<P>>) -> Self {
Self {
control_points: control_points.into(),
}
}
}
impl<P: Point> CubicGenerator<P> for BSpline<P> {
#[inline]
fn to_curve(&self) -> CubicCurve<P> {
let char_matrix = [
[1., 4., 1., 0.],
[-3., 0., 3., 0.],
[3., -6., 3., 0.],
[-1., 3., -3., 1.],
];
let segments = self
.control_points
.windows(4)
.map(|p| CubicCurve::coefficients([p[0], p[1], p[2], p[3]], 1.0 / 6.0, char_matrix))
.collect();
CubicCurve { segments }
}
}
pub trait CubicGenerator<P: Point> {
fn to_curve(&self) -> CubicCurve<P>;
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CubicSegment<P: Point> {
coeff: [P; 4],
}
impl<P: Point> CubicSegment<P> {
#[inline]
pub fn position(&self, t: f32) -> P {
let [a, b, c, d] = self.coeff;
a + b * t + c * t.powi(2) + d * t.powi(3)
}
#[inline]
pub fn velocity(&self, t: f32) -> P {
let [_, b, c, d] = self.coeff;
b + c * 2.0 * t + d * 3.0 * t.powi(2)
}
#[inline]
pub fn acceleration(&self, t: f32) -> P {
let [_, _, c, d] = self.coeff;
c * 2.0 + d * 6.0 * t
}
}
impl CubicSegment<Vec2> {
pub fn new_bezier(p1: impl Into<Vec2>, p2: impl Into<Vec2>) -> Self {
let (p0, p3) = (Vec2::ZERO, Vec2::ONE);
let bezier = Bezier::new([[p0, p1.into(), p2.into(), p3]]).to_curve();
bezier.segments[0].clone()
}
const MAX_ERROR: f32 = 1e-5;
const MAX_ITERS: u8 = 8;
#[inline]
pub fn ease(&self, time: f32) -> f32 {
let x = time.clamp(0.0, 1.0);
self.find_y_given_x(x)
}
#[inline]
fn find_y_given_x(&self, x: f32) -> f32 {
let mut t_guess = x;
let mut pos_guess = Vec2::ZERO;
for _ in 0..Self::MAX_ITERS {
pos_guess = self.position(t_guess);
let error = pos_guess.x - x;
if error.abs() <= Self::MAX_ERROR {
break;
}
let slope = self.velocity(t_guess).x; t_guess -= error / slope;
}
pos_guess.y
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CubicCurve<P: Point> {
segments: Vec<CubicSegment<P>>,
}
impl<P: Point> CubicCurve<P> {
#[inline]
pub fn position(&self, t: f32) -> P {
let (segment, t) = self.segment(t);
segment.position(t)
}
#[inline]
pub fn velocity(&self, t: f32) -> P {
let (segment, t) = self.segment(t);
segment.velocity(t)
}
#[inline]
pub fn acceleration(&self, t: f32) -> P {
let (segment, t) = self.segment(t);
segment.acceleration(t)
}
#[inline]
pub fn iter_samples(
&self,
subdivisions: usize,
sample_function: fn(&Self, f32) -> P,
) -> impl Iterator<Item = P> + '_ {
(0..=subdivisions).map(move |i| {
let segments = self.segments.len() as f32;
let t = i as f32 / subdivisions as f32 * segments;
sample_function(self, t)
})
}
pub fn iter_positions(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
self.iter_samples(subdivisions, Self::position)
}
pub fn iter_velocities(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
self.iter_samples(subdivisions, Self::velocity)
}
pub fn iter_accelerations(&self, subdivisions: usize) -> impl Iterator<Item = P> + '_ {
self.iter_samples(subdivisions, Self::acceleration)
}
#[inline]
fn segment(&self, t: f32) -> (&CubicSegment<P>, f32) {
if self.segments.len() == 1 {
(&self.segments[0], t)
} else {
let i = (t.floor() as usize).clamp(0, self.segments.len() - 1);
(&self.segments[i], t - i as f32)
}
}
#[inline]
fn coefficients(p: [P; 4], multiplier: f32, char_matrix: [[f32; 4]; 4]) -> CubicSegment<P> {
let [c0, c1, c2, c3] = char_matrix;
let mut coeff = [
p[0] * c0[0] + p[1] * c0[1] + p[2] * c0[2] + p[3] * c0[3],
p[0] * c1[0] + p[1] * c1[1] + p[2] * c1[2] + p[3] * c1[3],
p[0] * c2[0] + p[1] * c2[1] + p[2] * c2[2] + p[3] * c2[3],
p[0] * c3[0] + p[1] * c3[1] + p[2] * c3[2] + p[3] * c3[3],
];
coeff.iter_mut().for_each(|c| *c = *c * multiplier);
CubicSegment { coeff }
}
}
#[cfg(test)]
mod tests {
use glam::{vec2, Vec2};
use crate::cubic_splines::{Bezier, CubicGenerator, CubicSegment};
const FLOAT_EQ: f32 = 1e-5;
#[test]
fn cubic() {
const N_SAMPLES: usize = 1000;
let points = [[
vec2(-1.0, -20.0),
vec2(3.0, 2.0),
vec2(5.0, 3.0),
vec2(9.0, 8.0),
]];
let bezier = Bezier::new(points).to_curve();
for i in 0..=N_SAMPLES {
let t = i as f32 / N_SAMPLES as f32; assert!(bezier.position(t).distance(cubic_manual(t, points[0])) <= FLOAT_EQ);
}
}
fn cubic_manual(t: f32, points: [Vec2; 4]) -> Vec2 {
let p = points;
p[0] * (1.0 - t).powi(3)
+ 3.0 * p[1] * t * (1.0 - t).powi(2)
+ 3.0 * p[2] * t.powi(2) * (1.0 - t)
+ p[3] * t.powi(3)
}
#[test]
fn easing_simple() {
let bezier = CubicSegment::new_bezier([1.0, 0.0], [0.0, 1.0]);
assert_eq!(bezier.ease(0.0), 0.0);
assert!(bezier.ease(0.2) < 0.2); assert_eq!(bezier.ease(0.5), 0.5); assert!(bezier.ease(0.8) > 0.8); assert_eq!(bezier.ease(1.0), 1.0);
}
#[test]
fn easing_overshoot() {
let bezier = CubicSegment::new_bezier([0.0, 2.0], [1.0, 2.0]);
assert_eq!(bezier.ease(0.0), 0.0);
assert!(bezier.ease(0.5) > 1.5);
assert_eq!(bezier.ease(1.0), 1.0);
}
#[test]
fn easing_undershoot() {
let bezier = CubicSegment::new_bezier([0.0, -2.0], [1.0, -2.0]);
assert_eq!(bezier.ease(0.0), 0.0);
assert!(bezier.ease(0.5) < -0.5);
assert_eq!(bezier.ease(1.0), 1.0);
}
}