use super::Easing;
#[derive(Clone, Copy, Debug, Default)]
pub struct CubicBezier {
pub x1: f64,
pub y1: f64,
pub x2: f64,
pub y2: f64,
}
impl PartialEq for CubicBezier {
fn eq(&self, o: &Self) -> bool {
self.x1.to_bits() == o.x1.to_bits()
&& self.y1.to_bits() == o.y1.to_bits()
&& self.x2.to_bits() == o.x2.to_bits()
&& self.y2.to_bits() == o.y2.to_bits()
}
}
impl Eq for CubicBezier {}
impl core::hash::Hash for CubicBezier {
fn hash<H: core::hash::Hasher>(&self, h: &mut H) {
for v in [self.x1, self.y1, self.x2, self.y2] {
v.to_bits().hash(h);
}
}
}
const fn clamp01(v: f64) -> f64 {
if v < 0.0 {
0.0
} else if v > 1.0 {
1.0
} else {
v
}
}
impl CubicBezier {
pub const fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
Self {
x1: clamp01(x1),
y1,
x2: clamp01(x2),
y2,
}
}
fn coeffs(a: f64, b: f64) -> (f64, f64, f64) {
let cc = 3.0 * a;
let cb = 3.0 * (b - a) - cc;
let ca = 1.0 - cc - cb;
(ca, cb, cc)
}
fn eval((a, b, c): (f64, f64, f64), s: f64) -> f64 {
((a * s + b) * s + c) * s
}
fn deriv((a, b, c): (f64, f64, f64), s: f64) -> f64 {
(3.0 * a * s + 2.0 * b) * s + c
}
fn solve_s(&self, x: f64) -> f64 {
let cx = Self::coeffs(self.x1, self.x2);
let mut s = x;
for _ in 0..8 {
let err = Self::eval(cx, s) - x;
if err.abs() < 1e-9 {
return s;
}
let d = Self::deriv(cx, s);
if d.abs() < 1e-7 {
break;
}
s -= err / d;
}
let (mut lo, mut hi) = (0.0, 1.0);
s = x;
for _ in 0..64 {
let v = Self::eval(cx, s);
if (v - x).abs() < 1e-9 {
break;
}
if v < x {
lo = s;
} else {
hi = s;
}
s = (lo + hi) / 2.0;
}
s
}
pub fn apply(&self, t: f64) -> f64 {
let t = t.clamp(0.0, 1.0);
if t == 0.0 || t == 1.0 {
return t;
}
Self::eval(Self::coeffs(self.y1, self.y2), self.solve_s(t))
}
pub fn slope(&self, t: f64) -> f64 {
let t = t.clamp(0.0, 1.0);
let s = self.solve_s(t);
let dx = Self::deriv(Self::coeffs(self.x1, self.x2), s);
let dy = Self::deriv(Self::coeffs(self.y1, self.y2), s);
if dx.abs() > 1e-6 {
return dy / dx;
}
let h = 1e-5;
let (a, b) = ((t - h).max(0.0), (t + h).min(1.0));
(self.apply(b) - self.apply(a)) / (b - a)
}
}
impl Easing {
pub const EASE: Easing = Easing::cubic_bezier(0.25, 0.1, 0.25, 1.0);
pub const CSS_EASE_IN: Easing = Easing::cubic_bezier(0.42, 0.0, 1.0, 1.0);
pub const CSS_EASE_OUT: Easing = Easing::cubic_bezier(0.0, 0.0, 0.58, 1.0);
pub const CSS_EASE_IN_OUT: Easing = Easing::cubic_bezier(0.42, 0.0, 0.58, 1.0);
pub const STANDARD: Easing = Easing::cubic_bezier(0.2, 0.0, 0.0, 1.0);
pub const EMPHASIZED_DECELERATE: Easing = Easing::cubic_bezier(0.05, 0.7, 0.1, 1.0);
pub const EMPHASIZED_ACCELERATE: Easing = Easing::cubic_bezier(0.3, 0.0, 0.8, 0.15);
pub const BACK_OUT: Easing = Easing::cubic_bezier(0.34, 1.56, 0.64, 1.0);
pub const fn cubic_bezier(x1: f64, y1: f64, x2: f64, y2: f64) -> Easing {
Easing::CubicBezier(CubicBezier::new(x1, y1, x2, y2))
}
pub fn slope(self, t: f64) -> f64 {
let t = t.clamp(0.0, 1.0);
match self {
Easing::Linear => 1.0,
Easing::EaseIn => 2.0 * t,
Easing::EaseOut => 2.0 * (1.0 - t),
Easing::EaseInOut => {
if t < 0.5 {
4.0 * t
} else {
4.0 * (1.0 - t)
}
}
Easing::CubicBezier(b) => b.slope(t),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bezier_matches_reference_points() {
let lin = CubicBezier::new(0.0, 0.0, 1.0, 1.0);
for t in [0.1, 0.33, 0.5, 0.9] {
assert!((lin.apply(t) - t).abs() < 1e-6);
}
assert!((Easing::EASE.apply(0.5) - 0.8024).abs() < 1e-3);
let e = Easing::CSS_EASE_IN_OUT;
assert!((e.apply(0.5) - 0.5).abs() < 1e-6);
assert!((e.apply(0.2) + e.apply(0.8) - 1.0).abs() < 1e-6);
let peak = (1..100)
.map(|i| Easing::BACK_OUT.apply(i as f64 / 100.0))
.fold(0.0, f64::max);
assert!(peak > 1.05, "{peak}");
assert_eq!(Easing::BACK_OUT.apply(1.0), 1.0);
}
#[test]
fn slopes_match_finite_differences() {
let h = 1e-6;
for e in [
Easing::Linear,
Easing::EaseIn,
Easing::EaseOut,
Easing::EaseInOut,
Easing::EASE,
Easing::STANDARD,
Easing::BACK_OUT,
Easing::EMPHASIZED_ACCELERATE,
] {
for t in [0.1, 0.3, 0.6, 0.9] {
let fd = (e.apply(t + h) - e.apply(t - h)) / (2.0 * h);
assert!((e.slope(t) - fd).abs() < 1e-3, "{e:?} at {t}");
}
}
assert_eq!(Easing::EaseOut.slope(1.0), 0.0);
assert!((Easing::STANDARD.slope(1.0)).abs() < 1e-3);
}
}