use std::{error::Error, fmt, time::Duration};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[non_exhaustive]
pub enum Easing {
Linear,
EaseIn,
Custom(CustomEasing),
Bezier(Bezier),
CircIn,
CircOut,
CircInOut,
BackIn,
BackOut,
BackInOut,
Anticipate,
#[default]
EaseOut,
EaseInOut,
}
impl Easing {
pub(crate) fn sample(self, t: f64) -> (f64, f64) {
match self {
Self::Linear => (t, 1.0),
Self::Custom(curve) => {
let result = (curve.sample)(t);
assert!(
result.0.is_finite() && result.1.is_finite(),
"custom easing must return finite value/derivative"
);
result
}
Self::EaseIn => (t * t * t, 3.0 * t * t),
Self::Bezier(curve) => curve.sample(t),
Self::CircIn => {
let root = (1.0 - t * t).max(1e-12).sqrt();
(1.0 - root, t / root)
}
Self::CircOut => {
let root = (1.0 - (t - 1.0).powi(2)).max(1e-12).sqrt();
(root, (1.0 - t) / root)
}
Self::CircInOut => {
if t < 0.5 {
let (v, d) = Self::CircIn.sample(t * 2.);
(v * 0.5, d)
} else {
let (v, d) = Self::CircOut.sample(t * 2. - 1.);
(0.5 + v * 0.5, d)
}
}
Self::BackIn => {
let (v, d) = Self::BackOut.sample(1. - t);
(1. - v, d)
}
Self::BackInOut => {
if t < 0.5 {
let (v, d) = Self::BackIn.sample(t * 2.);
(v * 0.5, d)
} else {
let (v, d) = Self::BackOut.sample(t * 2. - 1.);
(0.5 + v * 0.5, d)
}
}
Self::Anticipate => {
if t < 0.5 {
let (v, d) = Self::BackIn.sample(t * 2.);
(v * 0.5, d)
} else {
let decay = 2_f64.powf(-20. * (t - 0.5));
(1. - 0.5 * decay, 10. * std::f64::consts::LN_2 * decay)
}
}
Self::BackOut => {
let u = t - 1.0;
(
1.0 + 2.70158 * u.powi(3) + 1.70158 * u * u,
8.10474 * u * u + 3.40316 * u,
)
}
Self::EaseOut => (1.0 - (1.0 - t).powi(3), 3.0 * (1.0 - t).powi(2)),
Self::EaseInOut => (t * t * (3.0 - 2.0 * t), 6.0 * t * (1.0 - t)),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct CustomEasing {
sample: fn(f64) -> (f64, f64),
identity: u64,
}
impl PartialEq for CustomEasing {
fn eq(&self, other: &Self) -> bool {
self.identity == other.identity
}
}
impl CustomEasing {
pub fn new(sample: fn(f64) -> (f64, f64)) -> Result<Self, InvalidEasing> {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
let start = sample(0.);
let end = sample(1.);
if start.0 != 0. || end.0 != 1. || ![start.1, end.1].iter().all(|v| v.is_finite()) {
return Err(InvalidEasing);
}
Ok(Self {
sample,
identity: NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
})
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Spring {
stiffness: f64,
damping: f64,
mass: f64,
rest: Option<[f64; 2]>,
}
impl Spring {
pub fn new(stiffness: f32, damping: f32, mass: f32) -> Result<Self, InvalidSpring> {
if [stiffness, damping, mass]
.iter()
.any(|value| !value.is_finite() || *value <= 0.0)
{
return Err(InvalidSpring);
}
Ok(Self {
stiffness: stiffness.into(),
damping: damping.into(),
mass: mass.into(),
rest: None,
})
}
pub fn visual(duration: Duration, bounce: f32) -> Result<Self, InvalidSpring> {
if duration.is_zero() || !bounce.is_finite() || !(-1.0..1.0).contains(&bounce) {
return Err(InvalidSpring);
}
let ratio = 1.0 - f64::from(bounce);
let unit = Self {
stiffness: 1.0,
damping: 2.0 * ratio,
mass: 1.0,
rest: None,
};
let mut high = 1.0;
while unit.matrix(high)[0] > 0.1 {
high *= 2.0;
}
let mut low = 0.0;
for _ in 0..64 {
let mid = (low + high) * 0.5;
if unit.matrix(mid)[0] > 0.1 {
low = mid;
} else {
high = mid;
}
}
let omega = high / duration.as_secs_f64();
Ok(Self {
stiffness: omega * omega,
damping: 2.0 * ratio * omega,
mass: 1.0,
rest: None,
})
}
pub fn rest(mut self, distance: f64, speed: f64) -> Result<Self, InvalidSpring> {
if ![distance, speed].iter().all(|v| v.is_finite() && *v > 0.0) {
return Err(InvalidSpring);
}
self.rest = Some([distance, speed]);
Ok(self)
}
pub(crate) fn matrix(self, seconds: f64) -> [f64; 4] {
let omega = (self.stiffness / self.mass).sqrt();
let decay = self.damping / (2.0 * self.mass);
let discriminant = decay * decay - omega * omega;
let (a, b) = if discriminant.abs() <= omega * omega * 1e-10 {
let exponential = (-decay * seconds).exp();
(exponential, exponential * seconds)
} else if discriminant < 0.0 {
let frequency = (-discriminant).sqrt();
let (sine, cosine) = (frequency * seconds).sin_cos();
let exponential = (-decay * seconds).exp();
(exponential * cosine, exponential * sine / frequency)
} else {
let root = discriminant.sqrt();
let slow = -omega * omega / (decay + root);
let fast = -decay - root;
let slow_exp = (slow * seconds).exp();
let fast_exp = (fast * seconds).exp();
(
(slow_exp + fast_exp) * 0.5,
(slow_exp - fast_exp) / (2.0 * root),
)
};
[a + decay * b, b, -omega * omega * b, a - decay * b]
}
pub(crate) fn is_settled(
self,
position: f64,
velocity: f64,
target: f64,
epsilon: f64,
) -> bool {
let [distance, speed] = self
.rest
.unwrap_or([epsilon, epsilon * (self.stiffness / self.mass).sqrt()]);
(position - target).abs() <= distance && velocity.abs() <= speed
}
}
impl Default for Spring {
fn default() -> Self {
Self {
stiffness: 240.0,
damping: 26.0,
mass: 1.0,
rest: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InvalidSpring;
impl fmt::Display for InvalidSpring {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("invalid spring parameters, timing, bounce, or settling tolerances")
}
}
impl Error for InvalidSpring {}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum Transition {
Instant,
Tween(Duration, Easing),
Spring(Spring),
}
impl Default for Transition {
fn default() -> Self {
Self::Spring(Spring::default())
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Bezier {
points: [f64; 4],
}
impl Bezier {
pub fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Result<Self, InvalidEasing> {
if ![x1, y1, x2, y2].iter().all(|v| v.is_finite())
|| !(0.0..=1.0).contains(&x1)
|| !(0.0..=1.0).contains(&x2)
{
return Err(InvalidEasing);
}
Ok(Self {
points: [x1, y1, x2, y2],
})
}
fn sample(self, x: f64) -> (f64, f64) {
let [x1, y1, x2, y2] = self.points;
let evaluate = |u: f64, a: f64, b: f64| {
3.0 * (1.0 - u).powi(2) * u * a + 3.0 * (1.0 - u) * u * u * b + u * u * u
};
let derivative = |u: f64, a: f64, b: f64| {
3.0 * (1.0 - u).powi(2) * a + 6.0 * (1.0 - u) * u * (b - a) + 3.0 * u * u * (1.0 - b)
};
let mut low = 0.0;
let mut high = 1.0;
let mut u = x;
for _ in 0..32 {
let actual = evaluate(u, x1, x2);
if (actual - x).abs() < 1e-9 {
break;
}
if actual < x {
low = u;
} else {
high = u;
}
u = (low + high) * 0.5;
}
(
evaluate(u, y1, y2),
derivative(u, y1, y2) / derivative(u, x1, x2).max(1e-12),
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InvalidEasing;
impl fmt::Display for InvalidEasing {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("invalid Bezier controls or custom easing endpoints/derivatives")
}
}
impl Error for InvalidEasing {}