#[derive(Clone, Debug, PartialEq)]
pub enum Curve {
Linear,
Decelerate,
Cubic {
x1: f32,
y1: f32,
x2: f32,
y2: f32,
},
CriticalSpring {
omega: f32,
},
Interval {
begin: f32,
end: f32,
inner: Box<Curve>,
},
Flipped(Box<Curve>),
}
impl Curve {
pub fn ease() -> Curve {
Curve::Cubic {
x1: 0.25,
y1: 0.1,
x2: 0.25,
y2: 1.0,
}
}
pub fn ease_in() -> Curve {
Curve::Cubic {
x1: 0.42,
y1: 0.0,
x2: 1.0,
y2: 1.0,
}
}
pub fn ease_out() -> Curve {
Curve::Cubic {
x1: 0.0,
y1: 0.0,
x2: 0.58,
y2: 1.0,
}
}
pub fn ease_in_out() -> Curve {
Curve::Cubic {
x1: 0.42,
y1: 0.0,
x2: 0.58,
y2: 1.0,
}
}
pub fn critical_spring() -> Curve {
Curve::CriticalSpring { omega: 8.0 }
}
pub fn transform(&self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
match self {
Curve::Linear => t,
Curve::Decelerate => {
let remaining = 1.0 - t;
1.0 - remaining * remaining
}
Curve::Cubic { x1, y1, x2, y2 } => cubic_bezier(*x1, *y1, *x2, *y2, t),
Curve::CriticalSpring { omega } => {
let resp = |x: f32| 1.0 - (-omega * x).exp() * (1.0 + omega * x);
let end = resp(1.0);
if end == 0.0 {
t
} else {
resp(t) / end
}
}
Curve::Interval { begin, end, inner } => {
if t <= *begin {
0.0
} else if t >= *end {
1.0
} else {
inner.transform((t - begin) / (end - begin))
}
}
Curve::Flipped(inner) => 1.0 - inner.transform(1.0 - t),
}
}
}
fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32, t: f32) -> f32 {
let bezier = |p1: f32, p2: f32, s: f32| {
let inv = 1.0 - s;
3.0 * inv * inv * s * p1 + 3.0 * inv * s * s * p2 + s * s * s
};
let mut lo = 0.0f32;
let mut hi = 1.0f32;
let mut s = t;
for _ in 0..24 {
s = 0.5 * (lo + hi);
let x = bezier(x1, x2, s);
if (x - t).abs() < 1e-5 {
break;
}
if x < t {
lo = s;
} else {
hi = s;
}
}
bezier(y1, y2, s)
}
#[cfg(test)]
mod tests {
use super::*;
fn endpoints(curve: &Curve) {
assert!(curve.transform(0.0).abs() < 1e-4, "f(0) != 0: {:?}", curve);
assert!(
(curve.transform(1.0) - 1.0).abs() < 1e-4,
"f(1) != 1: {:?}",
curve
);
}
#[test]
fn all_curves_hit_endpoints() {
endpoints(&Curve::Linear);
endpoints(&Curve::ease());
endpoints(&Curve::ease_in());
endpoints(&Curve::ease_out());
endpoints(&Curve::ease_in_out());
endpoints(&Curve::critical_spring());
}
#[test]
fn linear_is_identity() {
for i in 0..=10 {
let t = i as f32 / 10.0;
assert!((Curve::Linear.transform(t) - t).abs() < 1e-6);
}
}
#[test]
fn ease_in_starts_slow() {
assert!(Curve::ease_in().transform(0.5) < 0.5);
assert!(Curve::ease_out().transform(0.5) > 0.5);
}
#[test]
fn curves_are_monotonic() {
for curve in [
Curve::ease(),
Curve::ease_in(),
Curve::ease_out(),
Curve::ease_in_out(),
Curve::critical_spring(),
] {
let mut prev = 0.0;
for i in 0..=100 {
let v = curve.transform(i as f32 / 100.0);
assert!(v >= prev - 1e-4, "{:?} decreases at {i}", curve);
assert!(v <= 1.0 + 1e-4, "{:?} exceeds 1 at {i}", curve);
prev = v;
}
}
}
#[test]
fn interval_gates_the_subwindow() {
let staggered = Curve::Interval {
begin: 0.5,
end: 1.0,
inner: Box::new(Curve::Linear),
};
assert_eq!(staggered.transform(0.25), 0.0, "still before begin");
assert!(
(staggered.transform(0.75) - 0.5).abs() < 1e-4,
"half way at the middle of the window"
);
assert_eq!(staggered.transform(1.0), 1.0);
}
#[test]
fn flipped_mirrors() {
let flipped = Curve::Flipped(Box::new(Curve::ease_in()));
assert!(flipped.transform(0.5) > 0.5);
endpoints(&flipped);
}
#[test]
fn critical_spring_matches_reference_shape() {
let c = Curve::critical_spring();
assert!(c.transform(0.5) > 0.7);
assert!(c.transform(1.0) <= 1.0 + 1e-6);
}
}