use std::f64::consts::PI;
#[derive(Clone, Debug, PartialEq)]
#[allow(dead_code)] pub enum EasingStyle {
Linear,
EaseInSine,
EaseOutSine,
EaseInOutSine,
EaseInQuad,
EaseOutQuad,
EaseInOutQuad,
EaseInCubic,
EaseOutCubic,
EaseInOutCubic,
EaseInQuart,
EaseOutQuart,
EaseInOutQuart,
EaseInQuint,
EaseOutQuint,
EaseInOutQuint,
EaseInExpo,
EaseOutExpo,
EaseInOutExpo,
EaseInCirc,
EaseOutCirc,
EaseInOutCirc,
EaseInBack,
EaseOutBack,
EaseInOutBack,
EaseInElastic,
EaseOutElastic,
EaseInOutElastic,
EaseInBounce,
EaseOutBounce,
EaseInOutBounce,
CubicBezier(f64, f64, f64, f64),
}
#[inline]
pub fn apply_ease(t: f64, style: EasingStyle) -> f64 {
match style {
EasingStyle::Linear => Linear::evaluate(t),
EasingStyle::EaseInSine => EaseInSine::evaluate(t),
EasingStyle::EaseOutSine => EaseOutSine::evaluate(t),
EasingStyle::EaseInOutSine => EaseInOutSine::evaluate(t),
EasingStyle::EaseInQuad => EaseInQuad::evaluate(t),
EasingStyle::EaseOutQuad => EaseOutQuad::evaluate(t),
EasingStyle::EaseInOutQuad => EaseInOutQuad::evaluate(t),
EasingStyle::EaseInCubic => EaseInCubic::evaluate(t),
EasingStyle::EaseOutCubic => EaseOutCubic::evaluate(t),
EasingStyle::EaseInOutCubic => EaseInOutCubic::evaluate(t),
EasingStyle::EaseInQuart => EaseInQuart::evaluate(t),
EasingStyle::EaseOutQuart => EaseOutQuart::evaluate(t),
EasingStyle::EaseInOutQuart => EaseInOutQuart::evaluate(t),
EasingStyle::EaseInQuint => EaseInQuint::evaluate(t),
EasingStyle::EaseOutQuint => EaseOutQuint::evaluate(t),
EasingStyle::EaseInOutQuint => EaseInOutQuint::evaluate(t),
EasingStyle::EaseInExpo => EaseInExpo::evaluate(t),
EasingStyle::EaseOutExpo => EaseOutExpo::evaluate(t),
EasingStyle::EaseInOutExpo => EaseInOutExpo::evaluate(t),
EasingStyle::EaseInCirc => EaseInCirc::evaluate(t),
EasingStyle::EaseOutCirc => EaseOutCirc::evaluate(t),
EasingStyle::EaseInOutCirc => EaseInOutCirc::evaluate(t),
EasingStyle::EaseInBack => EaseInBack::evaluate(t),
EasingStyle::EaseOutBack => EaseOutBack::evaluate(t),
EasingStyle::EaseInOutBack => EaseInOutBack::evaluate(t),
EasingStyle::EaseInElastic => EaseInElastic::evaluate(t),
EasingStyle::EaseOutElastic => EaseOutElastic::evaluate(t),
EasingStyle::EaseInOutElastic => EaseInOutElastic::evaluate(t),
EasingStyle::EaseInBounce => EaseInBounce::evaluate(t),
EasingStyle::EaseOutBounce => EaseOutBounce::evaluate(t),
EasingStyle::EaseInOutBounce => EaseInOutBounce::evaluate(t),
EasingStyle::CubicBezier(x1, y1, x2, y2) => {
CubicBezierSolver { x1, y1, x2, y2 }.evaluate(t)
}
}
}
#[inline]
pub fn ease_position(style: &crate::animation::config::PositionAnimation, t: f64) -> f64 {
let curve = match style {
crate::animation::config::PositionAnimation::Linear => EasingStyle::Linear,
crate::animation::config::PositionAnimation::EaseInOut => EasingStyle::EaseInOutCubic,
crate::animation::config::PositionAnimation::EaseOutCubic => EasingStyle::EaseOutCubic,
crate::animation::config::PositionAnimation::EaseOutExpo => EasingStyle::EaseOutExpo,
crate::animation::config::PositionAnimation::EaseElastic => EasingStyle::EaseOutElastic,
crate::animation::config::PositionAnimation::Custom(custom) => {
return apply_ease(t, custom.clone());
}
};
apply_ease(t, curve)
}
#[inline]
pub fn ease_size(
style: &crate::animation::config::SizeAnimation,
from_size_matches: bool,
t: f64,
) -> f64 {
let curve = match style {
crate::animation::config::SizeAnimation::Linear => EasingStyle::Linear,
crate::animation::config::SizeAnimation::EaseInOut => EasingStyle::EaseInOutCubic,
crate::animation::config::SizeAnimation::DisabledIfUnchanged => {
if from_size_matches {
return t;
}
EasingStyle::Linear
}
};
apply_ease(t, curve)
}
struct Linear;
impl Linear {
#[inline]
fn evaluate(t: f64) -> f64 {
t
}
}
struct EaseInSine;
impl EaseInSine {
#[inline]
fn evaluate(t: f64) -> f64 {
1.0 - f64::cos((t * PI) / 2.0)
}
}
struct EaseOutSine;
impl EaseOutSine {
#[inline]
fn evaluate(t: f64) -> f64 {
f64::sin((t * PI) / 2.0)
}
}
struct EaseInOutSine;
impl EaseInOutSine {
#[inline]
fn evaluate(t: f64) -> f64 {
-(f64::cos(PI * t) - 1.0) / 2.0
}
}
struct EaseInQuad;
impl EaseInQuad {
#[inline]
fn evaluate(t: f64) -> f64 {
t * t
}
}
struct EaseOutQuad;
impl EaseOutQuad {
#[inline]
fn evaluate(t: f64) -> f64 {
(1.0 - t).mul_add(-(1.0 - t), 1.0)
}
}
struct EaseInOutQuad;
impl EaseInOutQuad {
#[inline]
fn evaluate(t: f64) -> f64 {
if t < 0.5 {
2.0 * t * t
} else {
1.0 - (-2.0f64).mul_add(t, 2.0).powi(2) / 2.0
}
}
}
struct EaseInCubic;
impl EaseInCubic {
#[inline]
fn evaluate(t: f64) -> f64 {
t * t * t
}
}
struct EaseOutCubic;
impl EaseOutCubic {
#[inline]
fn evaluate(t: f64) -> f64 {
1.0 - (1.0 - t).powi(3)
}
}
struct EaseInOutCubic;
impl EaseInOutCubic {
#[inline]
fn evaluate(t: f64) -> f64 {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0f64).mul_add(t, 2.0).powi(3) / 2.0
}
}
}
struct EaseInQuart;
impl EaseInQuart {
#[inline]
fn evaluate(t: f64) -> f64 {
t * t * t * t
}
}
struct EaseOutQuart;
impl EaseOutQuart {
#[inline]
fn evaluate(t: f64) -> f64 {
1.0 - (1.0 - t).powi(4)
}
}
struct EaseInOutQuart;
impl EaseInOutQuart {
#[inline]
fn evaluate(t: f64) -> f64 {
if t < 0.5 {
8.0 * t * t * t * t
} else {
1.0 - (-2.0f64).mul_add(t, 2.0).powi(4) / 2.0
}
}
}
struct EaseInQuint;
impl EaseInQuint {
#[inline]
fn evaluate(t: f64) -> f64 {
t * t * t * t * t
}
}
struct EaseOutQuint;
impl EaseOutQuint {
#[inline]
fn evaluate(t: f64) -> f64 {
1.0 - (1.0 - t).powi(5)
}
}
struct EaseInOutQuint;
impl EaseInOutQuint {
#[inline]
fn evaluate(t: f64) -> f64 {
if t < 0.5 {
16.0 * t * t * t * t * t
} else {
1.0 - (-2.0f64).mul_add(t, 2.0).powi(5) / 2.0
}
}
}
struct EaseInExpo;
impl EaseInExpo {
#[inline]
fn evaluate(t: f64) -> f64 {
if t.abs() < f64::EPSILON {
return 0.0;
}
10.0f64.mul_add(t, -10.0).exp2()
}
}
struct EaseOutExpo;
impl EaseOutExpo {
#[inline]
fn evaluate(t: f64) -> f64 {
if (t - 1.0).abs() < f64::EPSILON {
return 1.0;
}
1.0 - (-10.0 * t).exp2()
}
}
struct EaseInOutExpo;
impl EaseInOutExpo {
#[inline]
fn evaluate(t: f64) -> f64 {
if t.abs() < f64::EPSILON || (t - 1.0).abs() < f64::EPSILON {
return t;
}
if t < 0.5 {
20.0f64.mul_add(t, -10.0).exp2() / 2.0
} else {
(2.0 - (-20.0f64).mul_add(t, 10.0).exp2()) / 2.0
}
}
}
struct EaseInCirc;
impl EaseInCirc {
#[inline]
fn evaluate(t: f64) -> f64 {
1.0 - f64::sqrt(t.mul_add(-t, 1.0))
}
}
struct EaseOutCirc;
impl EaseOutCirc {
#[inline]
fn evaluate(t: f64) -> f64 {
f64::sqrt((t - 1.0).mul_add(-(t - 1.0), 1.0))
}
}
struct EaseInOutCirc;
impl EaseInOutCirc {
#[inline]
fn evaluate(t: f64) -> f64 {
if t < 0.5 {
(1.0 - f64::sqrt((2.0 * t).mul_add(-(2.0 * t), 1.0))) / 2.0
} else {
(f64::sqrt(
(-2.0f64)
.mul_add(t, 2.0)
.mul_add(-(-2.0f64).mul_add(t, 2.0), 1.0),
) + 1.0)
/ 2.0
}
}
}
struct EaseInBack;
impl EaseInBack {
#[inline]
fn evaluate(t: f64) -> f64 {
let c1 = 1.70158_f64;
let c3 = c1 + 1.0;
(c3 * t * t).mul_add(t, -c1 * t * t)
}
}
struct EaseOutBack;
impl EaseOutBack {
#[inline]
fn evaluate(t: f64) -> f64 {
let c1: f64 = 1.70158;
let c3: f64 = c1 + 1.0;
c1.mul_add((t - 1.0).powi(2), c3.mul_add((t - 1.0).powi(3), 1.0))
}
}
struct EaseInOutBack;
impl EaseInOutBack {
#[inline]
fn evaluate(t: f64) -> f64 {
let c1: f64 = 1.70158;
let c2: f64 = c1 * 1.525;
if t < 0.5 {
((2.0 * t).powi(2) * ((c2 + 1.0) * 2.0).mul_add(t, -c2)) / 2.0
} else {
((2.0f64.mul_add(t, -2.0))
.powi(2)
.mul_add((c2 + 1.0).mul_add(t.mul_add(2.0, -2.0), c2), 2.0))
/ 2.0
}
}
}
struct EaseInElastic;
impl EaseInElastic {
#[inline]
fn evaluate(t: f64) -> f64 {
if t.abs() < f64::EPSILON || (t - 1.0).abs() < f64::EPSILON {
return t;
}
let c4 = (2.0 * PI) / 3.0;
-(10.0f64.mul_add(t, -10.0).exp2()) * f64::sin(t.mul_add(10.0, -10.75) * c4)
}
}
struct EaseOutElastic;
impl EaseOutElastic {
#[inline]
fn evaluate(t: f64) -> f64 {
if t.abs() < f64::EPSILON || (t - 1.0).abs() < f64::EPSILON {
return t;
}
let c4 = (2.0 * PI) / 3.0;
(-10.0 * t)
.exp2()
.mul_add(f64::sin(t.mul_add(10.0, -0.75) * c4), 1.0)
}
}
struct EaseInOutElastic;
impl EaseInOutElastic {
#[inline]
fn evaluate(t: f64) -> f64 {
if t.abs() < f64::EPSILON || (t - 1.0).abs() < f64::EPSILON {
return t;
}
let c5 = (2.0 * PI) / 4.5;
if t < 0.5 {
-(20.0f64.mul_add(t, -10.0).exp2() * f64::sin(20.0f64.mul_add(t, -11.125) * c5)) / 2.0
} else {
((-20.0f64).mul_add(t, 10.0).exp2() * f64::sin(20.0f64.mul_add(t, -11.125) * c5)) / 2.0
+ 1.0
}
}
}
struct EaseOutBounce;
impl EaseOutBounce {
#[inline]
fn evaluate(t: f64) -> f64 {
let mut time = t;
let n1 = 7.5625_f64;
let d1 = 2.75_f64;
if t < 1.0 / d1 {
n1 * time * time
} else if time < 2.0 / d1 {
time -= 1.5 / d1;
(n1 * time).mul_add(time, 0.75)
} else if time < 2.5 / d1 {
time -= 2.25 / d1;
(n1 * time).mul_add(time, 0.9375)
} else {
time -= 2.625 / d1;
(n1 * time).mul_add(time, 0.984_375)
}
}
}
struct EaseInBounce;
impl EaseInBounce {
#[inline]
fn evaluate(t: f64) -> f64 {
1.0 - EaseOutBounce::evaluate(1.0 - t)
}
}
struct EaseInOutBounce;
impl EaseInOutBounce {
#[inline]
fn evaluate(t: f64) -> f64 {
if t < 0.5 {
(1.0 - EaseOutBounce::evaluate(2.0f64.mul_add(-t, 1.0))) / 2.0
} else {
(1.0 + EaseOutBounce::evaluate(2.0f64.mul_add(t, -1.0))) / 2.0
}
}
}
struct CubicBezierSolver {
x1: f64,
y1: f64,
x2: f64,
y2: f64,
}
impl CubicBezierSolver {
fn x(&self, s: f64) -> f64 {
3.0 * self.x1 * s * (1.0 - s).powi(2) + 3.0 * self.x2 * s.powi(2) * (1.0 - s) + s.powi(3)
}
fn y(&self, s: f64) -> f64 {
3.0 * self.y1 * s * (1.0 - s).powi(2) + 3.0 * self.y2 * s.powi(2) * (1.0 - s) + s.powi(3)
}
fn dx_ds(&self, s: f64) -> f64 {
3.0 * self.x1 * (1.0 - s) * (1.0 - 3.0 * s)
+ 3.0 * self.x2 * (2.0 * s - 3.0 * s.powi(2))
+ 3.0 * s.powi(2)
}
fn find_s(&self, t: f64) -> f64 {
if t <= 0.0 {
return 0.0;
}
if t >= 1.0 {
return 1.0;
}
let mut s = t; for _ in 0..8 {
let x_val = self.x(s);
let dx_val = self.dx_ds(s);
if dx_val.abs() < 1e-6 {
break;
}
let delta = (x_val - t) / dx_val;
s = (s - delta).clamp(0.0, 1.0);
if delta.abs() < 1e-6 {
break;
}
}
s
}
fn evaluate(&self, t: f64) -> f64 {
let s = self.find_s(t.clamp(0.0, 1.0));
self.y(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f64 = 1e-9;
fn assert_boundaries(style: EasingStyle) {
let s0 = style.clone();
assert!(
(apply_ease(0.0, s0)).abs() < EPSILON,
"expected 0.0 at t=0 for {style:?}"
);
assert!(
(apply_ease(1.0, style) - 1.0).abs() < EPSILON,
"expected 1.0 at t=1"
);
}
#[test]
fn linear_midpoint() {
assert!((Linear::evaluate(0.5) - 0.5).abs() < EPSILON);
}
#[test]
fn ease_in_out_cubic_symmetric_at_midpoint() {
assert!((EaseInOutCubic::evaluate(0.5) - 0.5).abs() < EPSILON);
}
#[test]
fn ease_out_cubic_at_one() {
assert!((EaseOutCubic::evaluate(1.0) - 1.0).abs() < EPSILON);
}
#[test]
fn boundaries_non_elastic() {
for style in [
EasingStyle::Linear,
EasingStyle::EaseInSine,
EasingStyle::EaseOutSine,
EasingStyle::EaseInOutSine,
EasingStyle::EaseInQuad,
EasingStyle::EaseOutQuad,
EasingStyle::EaseInOutQuad,
EasingStyle::EaseInCubic,
EasingStyle::EaseOutCubic,
EasingStyle::EaseInOutCubic,
EasingStyle::EaseInQuart,
EasingStyle::EaseOutQuart,
EasingStyle::EaseInOutQuart,
EasingStyle::EaseInQuint,
EasingStyle::EaseOutQuint,
EasingStyle::EaseInOutQuint,
EasingStyle::EaseInExpo,
EasingStyle::EaseOutExpo,
EasingStyle::EaseInOutExpo,
EasingStyle::EaseInCirc,
EasingStyle::EaseOutCirc,
EasingStyle::EaseInOutCirc,
EasingStyle::EaseInBounce,
EasingStyle::EaseOutBounce,
EasingStyle::EaseInOutBounce,
] {
assert_boundaries(style);
}
}
#[test]
fn cubic_bezier_boundaries() {
let style = EasingStyle::CubicBezier(0.25, 0.1, 0.25, 1.0);
assert!((apply_ease(0.0, style.clone())).abs() < EPSILON);
assert!((apply_ease(1.0, style) - 1.0).abs() < EPSILON);
}
#[test]
fn cubic_bezier_linear_control_points_approximate_linear() {
let style = EasingStyle::CubicBezier(0.33, 0.33, 0.66, 0.66);
let result = apply_ease(0.5, style);
assert!((result - 0.5).abs() < 0.01, "got {result}");
}
#[test]
fn ease_position_adapter_linear() {
let r = ease_position(&crate::animation::config::PositionAnimation::Linear, 0.5);
assert!((r - 0.5).abs() < EPSILON);
}
#[test]
fn ease_size_disabled_if_unchanged_passthrough() {
let r = ease_size(
&crate::animation::config::SizeAnimation::DisabledIfUnchanged,
true,
0.7,
);
assert!((r - 0.7).abs() < EPSILON);
}
#[test]
fn ease_size_disabled_if_unchanged_linear_when_changed() {
let r = ease_size(
&crate::animation::config::SizeAnimation::DisabledIfUnchanged,
false,
0.6,
);
assert!((r - 0.6).abs() < EPSILON);
}
#[test]
fn ease_position_custom_linear_is_identity() {
let style = crate::animation::config::PositionAnimation::Custom(EasingStyle::Linear);
for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
let result = ease_position(&style, t);
assert!(
(result - t).abs() < EPSILON,
"Custom(Linear) should be identity at t={t}, got {result}"
);
}
}
#[test]
fn ease_position_custom_ease_out_expo_matches_apply_ease() {
let style = crate::animation::config::PositionAnimation::Custom(EasingStyle::EaseOutExpo);
for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
let result = ease_position(&style, t);
let expected = apply_ease(t, EasingStyle::EaseOutExpo);
assert!(
(result - expected).abs() < EPSILON,
"Custom(EaseOutExpo) mismatch at t={t}: got {result}, expected {expected}"
);
}
}
#[test]
fn ease_position_custom_ease_out_back_can_exceed_one() {
let style = crate::animation::config::PositionAnimation::Custom(EasingStyle::EaseOutBack);
let at_one = ease_position(&style, 1.0);
assert!(
(at_one - 1.0).abs() < EPSILON,
"EaseOutBack should return 1.0 at t=1"
);
let at_08 = ease_position(&style, 0.8);
assert!(
at_08 > 1.0,
"EaseOutBack should overshoot at t=0.8, got {at_08}"
);
}
}