use std::f64::consts::PI;
pub trait Ease {
fn ease(&self, t: f64) -> f64;
}
impl<F: Fn(f64) -> f64> Ease for F {
fn ease(&self, t: f64) -> f64 {
self(t)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CubicBezier {
pub x1: f64,
pub y1: f64,
pub x2: f64,
pub y2: f64,
}
impl CubicBezier {
const NEWTON_ITERATIONS: usize = 8;
const PRECISION: f64 = 1e-7;
pub const fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
Self { x1, y1, x2, y2 }
}
pub const fn from_array([x1, y1, x2, y2]: [f64; 4]) -> Self {
Self::new(x1, y1, x2, y2)
}
pub const fn to_array(self) -> [f64; 4] {
[self.x1, self.y1, self.x2, self.y2]
}
fn coeffs(p1: f64, p2: f64) -> (f64, f64, f64) {
let c = 3.0 * p1;
let b = 3.0 * (p2 - p1) - c;
(1.0 - c - b, b, c)
}
#[cfg(test)]
fn bezier(p1: f64, p2: f64, s: f64) -> f64 {
let (a, b, c) = Self::coeffs(p1, p2);
((a * s + b) * s + c) * s
}
fn solve_s(&self, x: f64) -> f64 {
let (a, b, c) = Self::coeffs(self.x1, self.x2);
let eval = |s: f64| ((a * s + b) * s + c) * s;
let mut s = x;
for _ in 0..Self::NEWTON_ITERATIONS {
let err = eval(s) - x;
if err.abs() < Self::PRECISION {
return s;
}
let slope = (3.0 * a * s + 2.0 * b) * s + c;
if slope.abs() < 1e-6 {
break;
}
s = (s - err / slope).clamp(0.0, 1.0);
}
let (mut lo, mut hi) = (0.0, 1.0);
s = x;
for _ in 0..40 {
let v = eval(s);
if (v - x).abs() < Self::PRECISION {
break;
}
if v < x {
lo = s;
} else {
hi = s;
}
s = 0.5 * (lo + hi);
}
s
}
}
impl Ease for CubicBezier {
fn ease(&self, t: f64) -> f64 {
if t <= 0.0 {
return 0.0;
}
if t >= 1.0 {
return 1.0;
}
if self.x1 == self.y1 && self.x2 == self.y2 {
return t;
}
let (a, b, c) = Self::coeffs(self.y1, self.y2);
let s = self.solve_s(t);
((a * s + b) * s + c) * s
}
}
pub mod presets {
use super::CubicBezier;
pub const LINEAR: CubicBezier = CubicBezier::new(0.0, 0.0, 1.0, 1.0);
pub const EASE: CubicBezier = CubicBezier::new(0.25, 0.1, 0.25, 1.0);
pub const EASE_IN: CubicBezier = CubicBezier::new(0.42, 0.0, 1.0, 1.0);
pub const EASE_OUT: CubicBezier = CubicBezier::new(0.0, 0.0, 0.58, 1.0);
pub const EASE_IN_OUT: CubicBezier = CubicBezier::new(0.42, 0.0, 0.58, 1.0);
pub const IN_SINE: CubicBezier = CubicBezier::new(0.12, 0.0, 0.39, 0.0);
pub const OUT_SINE: CubicBezier = CubicBezier::new(0.61, 1.0, 0.88, 1.0);
pub const IN_OUT_SINE: CubicBezier = CubicBezier::new(0.37, 0.0, 0.63, 1.0);
pub const IN_QUAD: CubicBezier = CubicBezier::new(0.11, 0.0, 0.5, 0.0);
pub const OUT_QUAD: CubicBezier = CubicBezier::new(0.5, 1.0, 0.89, 1.0);
pub const IN_OUT_QUAD: CubicBezier = CubicBezier::new(0.45, 0.0, 0.55, 1.0);
pub const IN_CUBIC: CubicBezier = CubicBezier::new(0.32, 0.0, 0.67, 0.0);
pub const OUT_CUBIC: CubicBezier = CubicBezier::new(0.33, 1.0, 0.68, 1.0);
pub const IN_OUT_CUBIC: CubicBezier = CubicBezier::new(0.65, 0.0, 0.35, 1.0);
pub const IN_QUART: CubicBezier = CubicBezier::new(0.5, 0.0, 0.75, 0.0);
pub const OUT_QUART: CubicBezier = CubicBezier::new(0.25, 1.0, 0.5, 1.0);
pub const IN_OUT_QUART: CubicBezier = CubicBezier::new(0.76, 0.0, 0.24, 1.0);
pub const IN_EXPO: CubicBezier = CubicBezier::new(0.7, 0.0, 0.84, 0.0);
pub const OUT_EXPO: CubicBezier = CubicBezier::new(0.16, 1.0, 0.3, 1.0);
pub const IN_OUT_EXPO: CubicBezier = CubicBezier::new(0.87, 0.0, 0.13, 1.0);
pub const IN_BACK: CubicBezier = CubicBezier::new(0.36, 0.0, 0.66, -0.56);
pub const OUT_BACK: CubicBezier = CubicBezier::new(0.34, 1.56, 0.64, 1.0);
pub const IN_OUT_BACK: CubicBezier = CubicBezier::new(0.68, -0.6, 0.32, 1.6);
pub const DEFAULT: CubicBezier = IN_OUT_CUBIC;
}
pub fn elastic_out(t: f64) -> f64 {
if t <= 0.0 || t >= 1.0 {
return t.clamp(0.0, 1.0);
}
let c4 = 2.0 * PI / 3.0;
2f64.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
}
pub fn elastic_in(t: f64) -> f64 {
1.0 - elastic_out(1.0 - t)
}
pub fn elastic_in_out(t: f64) -> f64 {
if t <= 0.0 || t >= 1.0 {
return t.clamp(0.0, 1.0);
}
let c5 = 2.0 * PI / 4.5;
if t < 0.5 {
-(2f64.powf(20.0 * t - 10.0) * ((20.0 * t - 11.125) * c5).sin()) / 2.0
} else {
2f64.powf(-20.0 * t + 10.0) * ((20.0 * t - 11.125) * c5).sin() / 2.0 + 1.0
}
}
pub fn bounce_out(t: f64) -> f64 {
const N: f64 = 7.5625;
const D: f64 = 2.75;
let t = t.clamp(0.0, 1.0);
if t < 1.0 / D {
N * t * t
} else if t < 2.0 / D {
let t = t - 1.5 / D;
N * t * t + 0.75
} else if t < 2.5 / D {
let t = t - 2.25 / D;
N * t * t + 0.9375
} else {
let t = t - 2.625 / D;
N * t * t + 0.984_375
}
}
pub fn bounce_in(t: f64) -> f64 {
1.0 - bounce_out(1.0 - t)
}
pub fn bounce_in_out(t: f64) -> f64 {
if t < 0.5 {
(1.0 - bounce_out(1.0 - 2.0 * t)) / 2.0
} else {
(1.0 + bounce_out(2.0 * t - 1.0)) / 2.0
}
}
#[cfg(test)]
mod tests {
use super::presets::*;
use super::*;
fn reference(c: &CubicBezier, x: f64) -> f64 {
let (mut lo, mut hi) = (0.0f64, 1.0f64);
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
if CubicBezier::bezier(c.x1, c.x2, mid) < x {
lo = mid;
} else {
hi = mid;
}
}
CubicBezier::bezier(c.y1, c.y2, 0.5 * (lo + hi))
}
#[test]
fn bezier_matches_reference_within_tolerance() {
let curves = [
EASE,
EASE_IN,
EASE_OUT,
EASE_IN_OUT,
IN_OUT_CUBIC,
IN_EXPO,
OUT_EXPO,
IN_OUT_EXPO,
IN_OUT_BACK,
OUT_BACK,
IN_BACK,
CubicBezier::new(1.0, 0.0, 0.0, 1.0),
CubicBezier::new(0.0, 1.0, 1.0, 0.0),
];
for c in curves {
for i in 0..=1000 {
let x = i as f64 / 1000.0;
let got = c.ease(x);
let want = reference(&c, x);
assert!((got - want).abs() <= 1e-4, "{c:?} at {x}: {got} vs {want}");
}
}
}
#[test]
fn endpoints_and_linear() {
for c in [LINEAR, EASE, IN_OUT_BACK] {
assert_eq!(c.ease(0.0), 0.0);
assert_eq!(c.ease(1.0), 1.0);
}
assert_eq!(LINEAR.ease(0.3), 0.3);
for f in [
elastic_in,
elastic_out,
elastic_in_out,
bounce_in,
bounce_out,
bounce_in_out,
] {
assert!(f.ease(0.0).abs() < 1e-9);
assert!((f.ease(1.0) - 1.0).abs() < 1e-9);
}
}
}