use glam::{Vec2, Vec3, Vec4, Quat};
pub trait Lerp<T = Self> {
fn lerp(&self, other: T, t: f32) -> Self;
}
pub trait Slerp<T = Self> {
fn slerp(&self, other: T, t: f32) -> Self;
}
pub trait Interpolate<T = Self> {
fn interpolate_linear(&self, other: T, t: f32) -> Self;
fn interpolate_smooth(&self, other: T, t: f32) -> Self;
fn interpolate_eased(&self, other: T, t: f32, ease_fn: fn(f32) -> f32) -> Self;
}
impl Lerp for f32 {
fn lerp(&self, other: f32, t: f32) -> f32 {
self + (other - self) * t
}
}
impl Lerp for f64 {
fn lerp(&self, other: f64, t: f32) -> f64 {
self + (other - self) * t as f64
}
}
impl Lerp for Vec2 {
fn lerp(&self, other: Vec2, t: f32) -> Vec2 {
*self + (other - *self) * t
}
}
impl Lerp for Vec3 {
fn lerp(&self, other: Vec3, t: f32) -> Vec3 {
*self + (other - *self) * t
}
}
impl Lerp for Vec4 {
fn lerp(&self, other: Vec4, t: f32) -> Vec4 {
*self + (other - *self) * t
}
}
impl Slerp for Quat {
fn slerp(&self, other: Quat, t: f32) -> Quat {
Quat::slerp(*self, other, t)
}
}
impl Lerp for Quat {
fn lerp(&self, other: Quat, t: f32) -> Quat {
Quat::lerp(*self, other, t).normalize()
}
}
impl<T: Lerp + Copy> Interpolate for T {
fn interpolate_linear(&self, other: T, t: f32) -> T {
self.lerp(other, t)
}
fn interpolate_smooth(&self, other: T, t: f32) -> T {
let smooth_t = smoothstep(t);
self.lerp(other, smooth_t)
}
fn interpolate_eased(&self, other: T, t: f32, ease_fn: fn(f32) -> f32) -> T {
let eased_t = ease_fn(t);
self.lerp(other, eased_t)
}
}
pub fn smoothstep(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
t * t * (3.0 - 2.0 * t)
}
pub fn smootherstep(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
}
pub fn remap(value: f32, from_min: f32, from_max: f32, to_min: f32, to_max: f32) -> f32 {
let range = from_max - from_min;
if range.abs() < f32::EPSILON {
return (to_min + to_max) * 0.5;
}
let t = (value - from_min) / range;
to_min + t * (to_max - to_min)
}
pub fn ease_in_quad(t: f32) -> f32 {
t * t
}
pub fn ease_out_quad(t: f32) -> f32 {
1.0 - (1.0 - t) * (1.0 - t)
}
pub fn ease_in_out_quad(t: f32) -> f32 {
if t < 0.5 {
2.0 * t * t
} else {
1.0 - 2.0 * (1.0 - t) * (1.0 - t)
}
}
pub fn ease_in_cubic(t: f32) -> f32 {
t * t * t
}
pub fn ease_out_cubic(t: f32) -> f32 {
let t = 1.0 - t;
1.0 - t * t * t
}
pub fn ease_in_out_cubic(t: f32) -> f32 {
if t < 0.5 {
4.0 * t * t * t
} else {
let t = 1.0 - t;
1.0 - 4.0 * t * t * t
}
}
pub fn ease_in_quart(t: f32) -> f32 {
t * t * t * t
}
pub fn ease_out_quart(t: f32) -> f32 {
let t = 1.0 - t;
1.0 - t * t * t * t
}
pub fn ease_in_out_quart(t: f32) -> f32 {
if t < 0.5 {
8.0 * t * t * t * t
} else {
let t = 1.0 - t;
1.0 - 8.0 * t * t * t * t
}
}
pub fn ease_out_elastic(t: f32) -> f32 {
if t == 0.0 {
0.0
} else if t == 1.0 {
1.0
} else {
let p = 0.3;
let s = p / 4.0;
2.0_f32.powf(-10.0 * t) * ((t - s) * (2.0 * std::f32::consts::PI) / p).sin() + 1.0
}
}
pub fn ease_out_bounce(t: f32) -> f32 {
if t < 1.0 / 2.75 {
7.5625 * t * t
} else if t < 2.0 / 2.75 {
let t = t - 1.5 / 2.75;
7.5625 * t * t + 0.75
} else if t < 2.5 / 2.75 {
let t = t - 2.25 / 2.75;
7.5625 * t * t + 0.9375
} else {
let t = t - 2.625 / 2.75;
7.5625 * t * t + 0.984375
}
}
#[cfg(test)]
mod tests {
use super::*;
fn vec3_approx_eq(a: Vec3, b: Vec3, epsilon: f32) -> bool {
(a - b).length() < epsilon
}
#[allow(dead_code)]
fn quat_approx_eq(a: glam::Quat, b: glam::Quat, epsilon: f32) -> bool {
let dot = a.dot(b).abs();
(dot - 1.0).abs() < epsilon
}
#[test]
fn test_f32_lerp() {
assert_eq!(0.0.lerp(10.0, 0.0), 0.0);
assert_eq!(0.0.lerp(10.0, 1.0), 10.0);
assert_eq!(0.0.lerp(10.0, 0.5), 5.0);
}
#[test]
fn test_vec3_lerp() {
let start = Vec3::ZERO;
let end = Vec3::new(10.0, 20.0, 30.0);
let mid = start.lerp(end, 0.5);
assert!(vec3_approx_eq(mid, Vec3::new(5.0, 10.0, 15.0), 1e-6));
}
#[test]
fn test_quat_slerp() {
let start = Quat::IDENTITY;
let end = Quat::from_rotation_y(std::f32::consts::PI);
let mid = start.slerp(end, 0.5);
assert!(mid.is_finite());
assert!((mid.length() - 1.0).abs() < 1e-6, "Quaternion should be normalized");
let angle = 2.0 * mid.w.abs().acos();
let expected_angle = std::f32::consts::PI * 0.5;
assert!((angle - expected_angle).abs() < 1e-3,
"Expected angle {}, got {}", expected_angle, angle);
}
#[test]
fn test_smoothstep() {
assert_eq!(smoothstep(0.0), 0.0);
assert_eq!(smoothstep(1.0), 1.0);
assert!((smoothstep(0.5) - 0.5).abs() < 1e-6);
assert!(smoothstep(0.25) < 0.25);
assert!(smoothstep(0.75) > 0.75);
}
#[test]
fn test_remap() {
assert_eq!(remap(50.0, 0.0, 100.0, 0.0, 1.0), 0.5);
assert_eq!(remap(0.0, 0.0, 100.0, -1.0, 1.0), -1.0);
assert_eq!(remap(100.0, 0.0, 100.0, -1.0, 1.0), 1.0);
}
#[test]
fn test_ease_functions() {
assert_eq!(ease_in_quad(0.0), 0.0);
assert_eq!(ease_in_quad(1.0), 1.0);
assert_eq!(ease_out_quad(0.0), 0.0);
assert_eq!(ease_out_quad(1.0), 1.0);
let t = 0.3;
let ease_in = ease_in_out_cubic(t);
let ease_out = ease_in_out_cubic(1.0 - t);
assert!((ease_in - (1.0 - ease_out)).abs() < 1e-6);
}
#[test]
fn test_interpolate_trait() {
let start = Vec3::ZERO;
let end = Vec3::new(10.0, 20.0, 30.0);
let linear = start.interpolate_linear(end, 0.5);
let _smooth = start.interpolate_smooth(end, 0.5);
let _eased = start.interpolate_eased(end, 0.5, ease_in_out_cubic);
assert!(vec3_approx_eq(linear, Vec3::new(5.0, 10.0, 15.0), 1e-6));
let linear_quarter = start.interpolate_linear(end, 0.25);
let smooth_quarter = start.interpolate_smooth(end, 0.25);
let eased_quarter = start.interpolate_eased(end, 0.25, ease_in_out_cubic);
assert!(!vec3_approx_eq(smooth_quarter, linear_quarter, 1e-6)); assert!(!vec3_approx_eq(eased_quarter, linear_quarter, 1e-6)); }
#[test]
fn test_elastic_and_bounce() {
assert_eq!(ease_out_elastic(0.0), 0.0);
assert!((ease_out_elastic(1.0) - 1.0).abs() < 1e-6);
assert_eq!(ease_out_bounce(0.0), 0.0);
assert!((ease_out_bounce(1.0) - 1.0).abs() < 1e-6);
let elastic_mid = ease_out_elastic(0.5);
assert!(elastic_mid > 1.0 || elastic_mid < 0.0);
}
#[test]
fn test_extrapolation() {
let start = 0.0;
let end = 10.0;
assert_eq!(start.lerp(end, -0.5), -5.0); assert_eq!(start.lerp(end, 1.5), 15.0); }
#[test]
fn test_lerp_same_values() {
assert_eq!(5.0_f32.lerp(5.0, 0.5), 5.0);
let v = Vec3::ONE;
assert!(vec3_approx_eq(v.lerp(v, 0.7), Vec3::ONE, 1e-6));
}
#[test]
fn test_f64_lerp() {
assert_eq!(0.0_f64.lerp(100.0, 0.25), 25.0);
assert_eq!(0.0_f64.lerp(100.0, 0.0), 0.0);
assert_eq!(0.0_f64.lerp(100.0, 1.0), 100.0);
}
#[test]
fn test_vec2_lerp() {
let start = Vec2::ZERO;
let end = Vec2::new(10.0, 20.0);
let mid = start.lerp(end, 0.5);
assert!((mid.x - 5.0).abs() < 1e-6);
assert!((mid.y - 10.0).abs() < 1e-6);
}
#[test]
fn test_vec4_lerp() {
let start = Vec4::ZERO;
let end = Vec4::new(4.0, 8.0, 12.0, 16.0);
let mid = start.lerp(end, 0.25);
assert!((mid.x - 1.0).abs() < 1e-6);
assert!((mid.y - 2.0).abs() < 1e-6);
assert!((mid.z - 3.0).abs() < 1e-6);
assert!((mid.w - 4.0).abs() < 1e-6);
}
#[test]
fn test_quat_lerp() {
let start = Quat::IDENTITY;
let end = Quat::from_rotation_y(std::f32::consts::PI);
let mid = Lerp::lerp(&start, end, 0.0);
assert!(quat_approx_eq(mid, start, 1e-5));
}
#[test]
fn test_smootherstep() {
assert_eq!(smootherstep(0.0), 0.0);
assert_eq!(smootherstep(1.0), 1.0);
assert!((smootherstep(0.5) - 0.5).abs() < 1e-6);
assert!(smootherstep(0.1) < smoothstep(0.1));
}
#[test]
fn test_smoothstep_clamping() {
assert_eq!(smoothstep(-0.5), 0.0);
assert_eq!(smoothstep(1.5), 1.0);
}
#[test]
fn test_remap_inverse_range() {
let result = remap(0.5, 0.0, 1.0, 10.0, 0.0);
assert!((result - 5.0).abs() < 1e-6);
}
#[test]
fn test_ease_quart_boundary() {
assert_eq!(ease_in_quart(0.0), 0.0);
assert_eq!(ease_in_quart(1.0), 1.0);
assert_eq!(ease_out_quart(0.0), 0.0);
assert!((ease_out_quart(1.0) - 1.0).abs() < 1e-6);
assert_eq!(ease_in_out_quart(0.0), 0.0);
assert!((ease_in_out_quart(1.0) - 1.0).abs() < 1e-6);
}
#[test]
fn test_ease_monotonicity() {
let points = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0];
for window in points.windows(2) {
assert!(ease_in_quad(window[1]) >= ease_in_quad(window[0]));
assert!(ease_out_quad(window[1]) >= ease_out_quad(window[0]));
assert!(ease_in_out_quad(window[1]) >= ease_in_out_quad(window[0]));
assert!(ease_in_cubic(window[1]) >= ease_in_cubic(window[0]));
assert!(ease_out_cubic(window[1]) >= ease_out_cubic(window[0]));
}
}
#[test]
fn test_interpolate_smooth_at_boundaries() {
let start = 0.0_f32;
let end = 10.0_f32;
assert!((start.interpolate_smooth(end, 0.0) - 0.0).abs() < 1e-6);
assert!((start.interpolate_smooth(end, 1.0) - 10.0).abs() < 1e-6);
}
}