use super::spring::SpringConfig;
use std::sync::Arc;
#[derive(Clone)]
pub enum TimingFunction {
Linear,
EaseIn,
EaseOut,
EaseInOut,
CubicBezier(f32, f32, f32, f32),
Spring(SpringConfig),
Custom(CustomCurve),
}
#[derive(Clone)]
pub struct CustomCurve {
f: Arc<dyn Fn(f32) -> f32 + Send + Sync>,
excursion: f32,
}
impl CustomCurve {
fn evaluate(&self, t: f32) -> f32 {
(self.f)(t)
}
}
impl TimingFunction {
pub fn evaluate(&self, t: f32) -> f32 {
match self {
TimingFunction::Linear => t,
TimingFunction::EaseIn => ease_in(t),
TimingFunction::EaseOut => ease_out(t),
TimingFunction::EaseInOut => ease_in_out(t),
TimingFunction::CubicBezier(x1, y1, x2, y2) => cubic_bezier(t, *x1, *y1, *x2, *y2),
TimingFunction::Spring(_) => t, TimingFunction::Custom(curve) => curve.evaluate(t),
}
}
pub fn peak_overshoot(&self) -> f32 {
match self {
TimingFunction::Linear
| TimingFunction::EaseIn
| TimingFunction::EaseOut
| TimingFunction::EaseInOut => 0.0,
TimingFunction::CubicBezier(_, y1, _, y2) => {
let above = (y1.max(*y2) - 1.0).max(0.0);
let below = (-y1.min(*y2)).max(0.0);
above.max(below)
}
TimingFunction::Spring(config) => config.peak_overshoot(),
TimingFunction::Custom(curve) => curve.excursion,
}
}
const CUSTOM_SAMPLES: usize = 65;
pub fn custom<F>(f: F) -> Self
where
F: Fn(f32) -> f32 + Send + Sync + 'static,
{
let mut excursion = 0.0f32;
for i in 0..Self::CUSTOM_SAMPLES {
let v = f(i as f32 / (Self::CUSTOM_SAMPLES - 1) as f32);
excursion = excursion.max(v - 1.0).max(-v);
}
TimingFunction::Custom(CustomCurve {
f: Arc::new(f),
excursion: excursion.max(0.0),
})
}
}
impl std::fmt::Debug for TimingFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TimingFunction::Linear => write!(f, "Linear"),
TimingFunction::EaseIn => write!(f, "EaseIn"),
TimingFunction::EaseOut => write!(f, "EaseOut"),
TimingFunction::EaseInOut => write!(f, "EaseInOut"),
TimingFunction::CubicBezier(x1, y1, x2, y2) => {
write!(f, "CubicBezier({}, {}, {}, {})", x1, y1, x2, y2)
}
TimingFunction::Spring(config) => write!(f, "Spring({:?})", config),
TimingFunction::Custom(curve) => write!(f, "Custom(±{})", curve.excursion),
}
}
}
fn ease_in(t: f32) -> f32 {
t * t
}
fn ease_out(t: f32) -> f32 {
t * (2.0 - t)
}
fn ease_in_out(t: f32) -> f32 {
if t < 0.5 {
2.0 * t * t
} else {
-1.0 + (4.0 - 2.0 * t) * t
}
}
fn cubic_bezier(t: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
let mut current_t = t;
for _ in 0..8 {
let current_x = cubic_bezier_x(current_t, x1, x2);
let current_slope = cubic_bezier_slope(current_t, x1, x2);
if current_slope.abs() < 1e-6 {
break;
}
current_t -= (current_x - t) / current_slope;
}
cubic_bezier_y(current_t, y1, y2)
}
fn cubic_bezier_x(t: f32, x1: f32, x2: f32) -> f32 {
let t2 = t * t;
let t3 = t2 * t;
let mt = 1.0 - t;
let mt2 = mt * mt;
3.0 * mt2 * t * x1 + 3.0 * mt * t2 * x2 + t3
}
fn cubic_bezier_y(t: f32, y1: f32, y2: f32) -> f32 {
let t2 = t * t;
let t3 = t2 * t;
let mt = 1.0 - t;
let mt2 = mt * mt;
3.0 * mt2 * t * y1 + 3.0 * mt * t2 * y2 + t3
}
fn cubic_bezier_slope(t: f32, x1: f32, x2: f32) -> f32 {
let mt = 1.0 - t;
3.0 * mt * mt * x1 + 6.0 * mt * t * (x2 - x1) + 3.0 * t * t * (1.0 - x2)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_linear() {
assert_eq!(TimingFunction::Linear.evaluate(0.0), 0.0);
assert_eq!(TimingFunction::Linear.evaluate(0.5), 0.5);
assert_eq!(TimingFunction::Linear.evaluate(1.0), 1.0);
}
#[test]
fn test_ease_in() {
let result = TimingFunction::EaseIn.evaluate(0.5);
assert!(result < 0.5); }
#[test]
fn test_ease_out() {
let result = TimingFunction::EaseOut.evaluate(0.5);
assert!(result > 0.5); }
}
#[cfg(test)]
mod overshoot_bound_tests {
use super::*;
#[test]
fn a_custom_curve_is_measured_rather_than_clamped() {
let curve = TimingFunction::custom(|t| t * 2.0 - 0.5);
assert_eq!(curve.evaluate(1.0), 1.5, "the curve is left alone");
assert_eq!(curve.evaluate(0.0), -0.5);
assert!((curve.peak_overshoot() - 0.5).abs() < 1e-5);
for step in 0..=100 {
let t = step as f32 / 100.0;
let v = curve.evaluate(t);
let bound = curve.peak_overshoot();
assert!(
v <= 1.0 + bound + 1e-4 && v >= -bound - 1e-4,
"t = {t} evaluated to {v}, outside the bound it reports"
);
}
}
#[test]
fn a_custom_curve_that_only_eases_reports_no_excursion() {
let eased = TimingFunction::custom(|t| t * t);
assert_eq!(eased.peak_overshoot(), 0.0);
}
#[test]
fn the_ordinary_curves_report_what_they_do() {
assert_eq!(TimingFunction::Linear.peak_overshoot(), 0.0);
assert_eq!(TimingFunction::EaseInOut.peak_overshoot(), 0.0);
assert!(
(TimingFunction::CubicBezier(0.34, 1.56, 0.64, 1.0).peak_overshoot() - 0.56).abs()
< 1e-5
);
assert_eq!(
TimingFunction::CubicBezier(0.4, 0.0, 0.6, 1.0).peak_overshoot(),
0.0
);
assert!(
(TimingFunction::CubicBezier(0.5, -0.6, 0.5, 1.2).peak_overshoot() - 0.6).abs() < 1e-5
);
}
}