use std::{fmt, sync::Arc};
#[cfg(test)]
mod tests;
#[derive(Clone)]
pub enum Easing {
Linear,
SineIn,
SineOut,
SineInOut,
QuadraticIn,
QuadraticOut,
QuadraticInOut,
CubicIn,
CubicOut,
CubicInOut,
QuarticIn,
QuarticOut,
QuarticInOut,
ExponentialIn,
ExponentialOut,
ExponentialInOut,
#[non_exhaustive]
BackIn {
overshoot: f64,
},
#[non_exhaustive]
BackOut {
overshoot: f64,
},
#[non_exhaustive]
BackInOut {
overshoot: f64,
},
#[non_exhaustive]
ElasticOut {
amplitude: f64,
period: f64,
},
BounceOut,
#[non_exhaustive]
Steps(u32),
#[non_exhaustive]
CubicBezier {
x1: f64,
y1: f64,
x2: f64,
y2: f64,
},
Custom(Arc<dyn Fn(f64) -> f64 + Send + Sync>),
}
impl Easing {
pub fn back_in(overshoot: f64) -> Result<Self, crate::ValidationError> {
check_overshoot(overshoot).map(|overshoot| Self::BackIn { overshoot })
}
pub fn back_out(overshoot: f64) -> Result<Self, crate::ValidationError> {
check_overshoot(overshoot).map(|overshoot| Self::BackOut { overshoot })
}
pub fn back_in_out(overshoot: f64) -> Result<Self, crate::ValidationError> {
check_overshoot(overshoot).map(|overshoot| Self::BackInOut { overshoot })
}
pub fn elastic_out(amplitude: f64, period: f64) -> Result<Self, crate::ValidationError> {
if !amplitude.is_finite() || amplitude < 1.0 || !period.is_finite() || period <= 0.0 {
return Err(easing_error(
"elastic",
"elastic amplitude must be finite and at least one; period must be finite and positive",
));
}
Ok(Self::ElasticOut { amplitude, period })
}
pub fn steps(count: u32) -> Result<Self, crate::ValidationError> {
if count == 0 {
return Err(easing_error("steps", "easing steps must be nonzero"));
}
Ok(Self::Steps(count))
}
pub fn cubic_bezier(
x1: f64,
y1: f64,
x2: f64,
y2: f64,
) -> Result<Self, crate::ValidationError> {
if ![x1, y1, x2, y2].iter().all(|value| value.is_finite())
|| !(0.0..=1.0).contains(&x1)
|| !(0.0..=1.0).contains(&x2)
{
return Err(easing_error(
"controls",
"Bezier controls must be finite with X in 0..=1",
));
}
Ok(Self::CubicBezier { x1, y1, x2, y2 })
}
pub fn apply(&self, progress: f64) -> f64 {
let progress = progress.clamp(0.0, 1.0);
if progress == 0.0 && !matches!(self, Self::Custom(_)) {
return 0.0;
}
if progress == 1.0 && !matches!(self, Self::Custom(_)) {
return 1.0;
}
match self {
Self::Linear => progress,
Self::SineIn => {
let sine = libm::sin(progress * std::f64::consts::FRAC_PI_4);
2.0 * sine * sine
}
Self::SineOut => libm::sin(progress * std::f64::consts::FRAC_PI_2),
Self::SineInOut => {
if progress == 0.5 {
return 0.5;
}
let sine = libm::sin(progress * std::f64::consts::FRAC_PI_2);
sine * sine
}
Self::QuadraticIn => progress * progress,
Self::QuadraticOut => progress * (2.0 - progress),
Self::QuadraticInOut => mirrored(progress, |value| value * value),
Self::CubicIn => progress * progress * progress,
Self::CubicOut => progress * (3.0 + progress * (progress - 3.0)),
Self::CubicInOut => mirrored(progress, |value| value * value * value),
Self::QuarticIn => progress * progress * progress * progress,
Self::QuarticOut => progress * (4.0 + progress * (-6.0 + progress * (4.0 - progress))),
Self::QuarticInOut => mirrored(progress, |value| value * value * value * value),
Self::ExponentialIn => libm::exp2(10.0 * (progress - 1.0)),
Self::ExponentialOut => -libm::expm1(-10.0 * std::f64::consts::LN_2 * progress),
Self::ExponentialInOut => mirrored(progress, |value| libm::exp2(10.0 * (value - 1.0))),
Self::BackIn { overshoot } => back_in(progress, *overshoot),
Self::BackOut { overshoot } => 1.0 - back_in(1.0 - progress, *overshoot),
Self::BackInOut { overshoot } => mirrored(progress, |value| back_in(value, *overshoot)),
Self::ElasticOut { amplitude, period } => {
let phase = libm::asin(1.0 / amplitude) / std::f64::consts::TAU;
let cycles = libm::fmod(progress, *period) / period;
1.0 + amplitude
* libm::exp2(-10.0 * progress)
* libm::sin(std::f64::consts::TAU * (cycles - phase))
}
Self::BounceOut => bounce_out(progress),
Self::Steps(count) => libm::floor(progress * f64::from(*count)) / f64::from(*count),
Self::CubicBezier { x1, y1, x2, y2 } => cubic_bezier(progress, *x1, *y1, *x2, *y2),
Self::Custom(function) => function(progress),
}
}
pub fn try_apply(&self, progress: f64) -> Result<f64, crate::ValidationError> {
if !progress.is_finite() {
return Err(easing_error("progress", "easing progress must be finite"));
}
let value = self.apply(progress);
if !value.is_finite() {
return Err(easing_error("output", "easing output must be finite"));
}
Ok(value)
}
pub fn reversed(self) -> Self {
Self::Custom(Arc::new(move |progress| 1.0 - self.apply(1.0 - progress)))
}
pub fn mirrored(self) -> Self {
Self::Custom(Arc::new(move |progress| {
mirrored(progress, |value| self.apply(value))
}))
}
}
impl fmt::Debug for Easing {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Linear => formatter.write_str("Linear"),
Self::SineIn => formatter.write_str("SineIn"),
Self::SineOut => formatter.write_str("SineOut"),
Self::SineInOut => formatter.write_str("SineInOut"),
Self::QuadraticIn => formatter.write_str("QuadraticIn"),
Self::QuadraticOut => formatter.write_str("QuadraticOut"),
Self::QuadraticInOut => formatter.write_str("QuadraticInOut"),
Self::CubicIn => formatter.write_str("CubicIn"),
Self::CubicOut => formatter.write_str("CubicOut"),
Self::CubicInOut => formatter.write_str("CubicInOut"),
Self::QuarticIn => formatter.write_str("QuarticIn"),
Self::QuarticOut => formatter.write_str("QuarticOut"),
Self::QuarticInOut => formatter.write_str("QuarticInOut"),
Self::ExponentialIn => formatter.write_str("ExponentialIn"),
Self::ExponentialOut => formatter.write_str("ExponentialOut"),
Self::ExponentialInOut => formatter.write_str("ExponentialInOut"),
Self::BackIn { overshoot } => formatter
.debug_struct("BackIn")
.field("overshoot", overshoot)
.finish(),
Self::BackOut { overshoot } => formatter
.debug_struct("BackOut")
.field("overshoot", overshoot)
.finish(),
Self::BackInOut { overshoot } => formatter
.debug_struct("BackInOut")
.field("overshoot", overshoot)
.finish(),
Self::ElasticOut { amplitude, period } => formatter
.debug_struct("ElasticOut")
.field("amplitude", amplitude)
.field("period", period)
.finish(),
Self::BounceOut => formatter.write_str("BounceOut"),
Self::Steps(count) => formatter.debug_tuple("Steps").field(count).finish(),
Self::CubicBezier { x1, y1, x2, y2 } => formatter
.debug_struct("CubicBezier")
.field("x1", x1)
.field("y1", y1)
.field("x2", x2)
.field("y2", y2)
.finish(),
Self::Custom(_) => formatter.write_str("Custom(..)"),
}
}
}
fn check_overshoot(overshoot: f64) -> Result<f64, crate::ValidationError> {
if overshoot.is_finite() && overshoot >= 0.0 {
Ok(overshoot)
} else {
Err(easing_error(
"overshoot",
"back overshoot must be finite and nonnegative",
))
}
}
fn easing_error(field: &str, message: &str) -> crate::ValidationError {
crate::ValidationError::invalid("easing", field, message)
}
fn mirrored(progress: f64, inward: impl Fn(f64) -> f64) -> f64 {
if progress < 0.5 {
inward(2.0 * progress) * 0.5
} else {
1.0 - inward(2.0 * (1.0 - progress)) * 0.5
}
}
fn back_in(progress: f64, overshoot: f64) -> f64 {
progress * progress * (progress + overshoot * (progress - 1.0))
}
fn bounce_out(progress: f64) -> f64 {
let (shifted, offset) = if progress < 1.0 / 2.75 {
(progress, 0.0)
} else if progress < 2.0 / 2.75 {
(progress - 1.5 / 2.75, 0.75)
} else if progress < 2.5 / 2.75 {
(progress - 2.25 / 2.75, 0.9375)
} else {
(progress - 2.625 / 2.75, 0.984375)
};
7.5625 * shifted * shifted + offset
}
fn coordinate(parameter: f64, first: f64, second: f64) -> f64 {
let inverse = 1.0 - parameter;
let first_weight = 3.0 * inverse * inverse * parameter;
let second_weight = 3.0 * inverse * parameter * parameter;
first_weight * first + second_weight * second + parameter * parameter * parameter
}
fn cubic_bezier(progress: f64, x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
coordinate(bezier_parameter(progress, x1, x2), y1, y2)
}
fn bezier_parameter(progress: f64, x1: f64, x2: f64) -> f64 {
if progress > 0.5 {
return 1.0 - bezier_parameter(1.0 - progress, 1.0 - x2, 1.0 - x1);
}
let mut lower = 0.0;
let mut upper = 1.0;
for _ in 0..1_100 {
let parameter = lower + (upper - lower) * 0.5;
let horizontal = coordinate(parameter, x1, x2);
if horizontal == progress || parameter == lower || parameter == upper {
return parameter;
}
if horizontal < progress {
lower = parameter;
} else {
upper = parameter;
}
}
lower + (upper - lower) * 0.5
}