use std::ops::{Add, Div, Mul, Neg, Sub};
pub trait BezierFloat:
Copy
+ PartialEq
+ PartialOrd
+ Send
+ Sync
+ Add<Output = Self>
+ Sub<Output = Self>
+ Mul<Output = Self>
+ Div<Output = Self>
+ Neg<Output = Self>
+ 'static
{
const ZERO: Self;
const ONE: Self;
const TWO: Self;
const THREE: Self;
const SIX: Self;
const PI: Self;
fn sqrt(self) -> Self;
fn cbrt(self) -> Self;
fn atan(self) -> Self;
fn cos(self) -> Self;
}
macro_rules! impl_bezier_float {
($type:ty, $consts:ident) => {
impl BezierFloat for $type {
const ZERO: Self = 0.0;
const ONE: Self = 1.0;
const TWO: Self = 2.0;
const THREE: Self = 3.0;
const SIX: Self = 6.0;
const PI: Self = std::$consts::consts::PI;
#[inline]
fn sqrt(self) -> Self {
<$type>::sqrt(self)
}
#[inline]
fn cbrt(self) -> Self {
<$type>::cbrt(self)
}
#[inline]
fn atan(self) -> Self {
<$type>::atan(self)
}
#[inline]
fn cos(self) -> Self {
<$type>::cos(self)
}
}
};
}
impl_bezier_float!(f32, f32);
impl_bezier_float!(f64, f64);
pub type BezierEasing<T = f64> = Box<dyn Fn(T) -> T + Send + Sync + 'static>;
#[inline]
fn linear_easing<T: BezierFloat>(x: T) -> T {
x
}
#[inline]
fn y<T: BezierFloat>(t: T, ay: T, by: T, cy: T) -> T {
((ay * t + T::THREE * by) * t + cy) * t
}
#[inline]
fn x2t<T: BezierFloat>(x: T, a: T, b: T, c: T, d: T) -> T {
let q = a + b * x;
let s = q * q + c;
if s > T::ZERO {
let root = s.sqrt();
return (q + root).cbrt() + (q - root).cbrt() - d;
}
let l = (q * q - s).sqrt().cbrt();
let angle = if q != T::ZERO {
((-s).sqrt() / q).atan()
} else {
-T::PI / T::TWO
};
let phi = if b < T::ZERO {
(if q > T::ZERO { T::TWO * T::PI } else { T::PI }) - angle
} else if d < T::ZERO {
(if q > T::ZERO {
T::TWO * T::PI
} else {
-T::THREE * T::PI
}) + angle
} else {
(if q > T::ZERO { T::ZERO } else { T::PI }) + angle
};
T::TWO * l * (phi / T::THREE).cos() - d
}
#[derive(Debug, Clone)]
pub struct BezierEasingError(String);
impl std::fmt::Display for BezierEasingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for BezierEasingError {}
pub fn bezier_easing<T: BezierFloat>(
m_x1: T,
m_y1: T,
m_x2: T,
m_y2: T,
) -> Result<BezierEasing<T>, BezierEasingError> {
if !(T::ZERO <= m_x1 && m_x1 <= T::ONE && T::ZERO <= m_x2 && m_x2 <= T::ONE) {
return Err(BezierEasingError(
"bezier x values must be in [0, 1] range".to_string(),
));
}
if m_x1 == m_y1 && m_x2 == m_y2 {
return Ok(Box::new(linear_easing));
}
let a = T::SIX * (T::THREE * m_x1 - T::THREE * m_x2 + T::ONE);
let b = T::SIX * (m_x2 - T::TWO * m_x1);
let c = T::THREE * m_x1;
let ay = T::THREE * m_y1 - T::THREE * m_y2 + T::ONE;
let by = m_y2 - T::TWO * m_y1;
let cy = T::THREE * m_y1;
if a == T::ZERO {
return Ok(Box::new(move |x| {
if x == T::ZERO || x == T::ONE {
return x;
}
y(x, ay, by, cy)
}));
}
let a2 = a * a;
let b2 = b * b;
let d = b / a;
let e = (T::THREE * b * c) / a2 - (b2 * b) / (a2 * a);
let w1 = (T::TWO * c) / a - b2 / a2;
let w = w1 * w1 * w1;
let o = T::THREE / a;
Ok(Box::new(move |x| {
if x == T::ZERO || x == T::ONE {
return x;
}
y(x2t(x, e, o, w, d), ay, by, cy)
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() -> Result<(), BezierEasingError> {
let ease = bezier_easing(0.0_f64, 0.0, 1.0, 0.5)?;
assert_eq!(ease(0.0), 0.0);
assert!((ease(0.5) - 0.3125).abs() < 0.000001);
assert_eq!(ease(1.0), 1.0);
Ok(())
}
#[test]
fn handles_low_slope_curves() -> Result<(), BezierEasingError> {
let ease = bezier_easing(0.0_f64, 1.0, 0.0, 1.0)?;
let y = ease(0.000001);
assert!(y > 0.02 && y < 0.04);
Ok(())
}
#[test]
fn supports_f32() -> Result<(), BezierEasingError> {
let ease = bezier_easing(0.0_f32, 0.0, 1.0, 0.5)?;
assert!((ease(0.5) - 0.3125).abs() < 0.000001);
Ok(())
}
}