use core::f32::consts::PI;
use crate::curve::{Curve, KeyframeInterp};
use crate::CurveError;
pub fn spring(damping: f32, stiffness: f32, mass: f32) -> Result<Curve, CurveError> {
let mass = mass.max(1e-3);
let stiffness = stiffness.max(1e-3);
let zeta = damping.clamp(0.0, 1.0);
let omega = (stiffness / mass).sqrt();
let omega_d = omega * (1.0 - zeta * zeta).max(0.0).sqrt();
let scale = 3.0 * PI;
Curve::from_curve_fn(
move |t| {
let x = t * scale;
1.0 - (-zeta * omega * x).exp() * (omega_d * x).cos()
},
128,
KeyframeInterp::Linear,
)
}
pub fn bounce(bounces: u32, decay: f32) -> Result<Curve, CurveError> {
let b = bounces.max(1) as f32;
let decay = decay.clamp(0.0, 1.0).max(1e-2);
let k = 0.5 + 2.0 * decay;
Curve::from_curve_fn(
move |t| {
let envelope = (1.0 - t).max(0.0).powf(k);
1.0 - (b * PI * t).cos().abs() * envelope
},
128,
KeyframeInterp::Linear,
)
}
pub fn elastic(period: f32, amplitude: f32) -> Result<Curve, CurveError> {
let p = period.max(0.05);
let a = amplitude.max(1.0);
let s = p / (2.0 * PI) * (1.0f32 / a).asin();
Curve::from_curve_fn(
move |t| {
if t <= 0.0 {
return 0.0;
}
if t >= 1.0 {
return 1.0;
}
a * (2.0f32).powf(-10.0 * t) * ((t - s) * 2.0 * PI / p).sin() + 1.0
},
128,
KeyframeInterp::Linear,
)
}
pub fn overshoot(strength: f32) -> Result<Curve, CurveError> {
let s = strength.max(0.0);
let c1 = s + 1.0;
Curve::from_curve_fn(
move |t| {
let u = t - 1.0;
u * u * (c1 * u + s) + 1.0
},
96,
KeyframeInterp::Linear,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AnimationCurve;
const EPS: f32 = 1e-3;
fn endpoints_reach_zero_and_one(curve: &Curve) {
assert!(curve.eval(0.0).abs() < EPS, "start != 0: {}", curve.eval(0.0));
assert!(
(curve.eval(1.0) - 1.0).abs() < EPS,
"end != 1: {}",
curve.eval(1.0)
);
}
#[test]
fn spring_endpoints_and_bounded() {
let c = spring(0.5, 180.0, 1.0).unwrap();
endpoints_reach_zero_and_one(&c);
for i in 0..=50 {
let t = i as f32 / 50.0;
let v = c.eval(t);
assert!(v.abs() < 5.0, "spring blew up at t={t}: {v}");
}
}
#[test]
fn spring_critical_damping_no_overshoot() {
let c = spring(1.0, 180.0, 1.0).unwrap();
for i in 0..=50 {
let t = i as f32 / 50.0;
let v = c.eval(t);
assert!(v <= 1.01, "critical spring overshot at t={t}: {v}");
}
}
#[test]
fn bounce_endpoints() {
let c = bounce(3, 0.5).unwrap();
endpoints_reach_zero_and_one(&c);
}
#[test]
fn elastic_endpoints() {
let c = elastic(0.3, 1.0).unwrap();
endpoints_reach_zero_and_one(&c);
}
#[test]
fn overshoot_zero_strength_is_smooth_ease_out_cubic() {
let c = overshoot(0.0).unwrap();
for i in 0..=10 {
let t = i as f32 / 10.0;
let expected = {
let u = 1.0 - t;
1.0 - u * u * u
};
assert!(
(c.eval(t) - expected).abs() < 5e-3,
"at t={t}: got {}, want {expected}",
c.eval(t)
);
}
}
#[test]
fn overshoot_positive_strength_overshoots_one() {
let c = overshoot(1.7).unwrap();
let peak = (0..=100)
.map(|i| c.eval(i as f32 / 100.0))
.fold(f32::MIN, f32::max);
assert!(peak > 1.05, "no overshoot detected, peak = {peak}");
}
}