use std::any::Any;
use std::f64::consts::PI;
use std::fmt::{self, Debug};
use std::rc::Rc;
use inset_embedder::{Offset, clamp_double, lerp_double};
pub trait Curve: Any + Debug {
fn transform(&self, t: f64) -> f64 {
if t == 0.0 || t == 1.0 {
return t;
}
debug_assert!(
(0.0..=1.0).contains(&t),
"parametric value {t} is outside of [0, 1] range."
);
self.transform_internal(t)
}
fn transform_internal(&self, _t: f64) -> f64 {
unimplemented!()
}
}
impl dyn Curve {
pub fn flipped(self: Rc<dyn Curve>) -> FlippedCurve {
FlippedCurve::new(self)
}
}
#[derive(Debug)]
struct Linear;
impl Curve for Linear {
fn transform_internal(&self, t: f64) -> f64 {
t
}
}
pub struct SawTooth {
count: i64,
}
impl SawTooth {
pub const fn new(count: i64) -> SawTooth {
SawTooth { count }
}
pub const fn count(&self) -> i64 {
self.count
}
}
impl Curve for SawTooth {
fn transform_internal(&self, t: f64) -> f64 {
let t = t * self.count as f64;
t - t.trunc()
}
}
impl Debug for SawTooth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SawTooth({})", self.count)
}
}
pub struct Interval {
begin: f64,
end: f64,
curve: Rc<dyn Curve>,
}
impl Interval {
pub fn new(begin: f64, end: f64, curve: Rc<dyn Curve>) -> Interval {
Interval { begin, end, curve }
}
pub const fn begin(&self) -> f64 {
self.begin
}
pub const fn end(&self) -> f64 {
self.end
}
pub fn curve(&self) -> &Rc<dyn Curve> {
&self.curve
}
}
impl Curve for Interval {
fn transform_internal(&self, t: f64) -> f64 {
debug_assert!(self.begin >= 0.0);
debug_assert!(self.begin <= 1.0);
debug_assert!(self.end >= 0.0);
debug_assert!(self.end <= 1.0);
debug_assert!(self.end >= self.begin);
let t = clamp_double((t - self.begin) / (self.end - self.begin), 0.0, 1.0);
if t == 0.0 || t == 1.0 {
return t;
}
self.curve.transform(t)
}
}
impl Debug for Interval {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if (&*self.curve as &dyn Any)
.downcast_ref::<Linear>()
.is_none()
{
return write!(
f,
"Interval({:?}\u{22ef}{:?})\u{27a9}{:?}",
self.begin, self.end, self.curve
);
}
write!(f, "Interval({:?}\u{22ef}{:?})", self.begin, self.end)
}
}
pub struct Split {
split: f64,
begin_curve: Rc<dyn Curve>,
end_curve: Rc<dyn Curve>,
}
impl Split {
pub fn new(split: f64, begin_curve: Rc<dyn Curve>, end_curve: Rc<dyn Curve>) -> Split {
Split {
split,
begin_curve,
end_curve,
}
}
pub const fn split(&self) -> f64 {
self.split
}
pub fn begin_curve(&self) -> &Rc<dyn Curve> {
&self.begin_curve
}
pub fn end_curve(&self) -> &Rc<dyn Curve> {
&self.end_curve
}
}
impl Curve for Split {
fn transform(&self, t: f64) -> f64 {
debug_assert!((0.0..=1.0).contains(&t));
debug_assert!((0.0..=1.0).contains(&self.split));
if t == 0.0 || t == 1.0 {
return t;
}
if t == self.split {
return self.split;
}
if t < self.split {
let curve_progress = t / self.split;
let transformed = self.begin_curve.transform(curve_progress);
lerp_double(Some(0.0), Some(self.split), transformed).unwrap()
} else {
let curve_progress = (t - self.split) / (1.0 - self.split);
let transformed = self.end_curve.transform(curve_progress);
lerp_double(Some(self.split), Some(1.0), transformed).unwrap()
}
}
}
impl Debug for Split {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Split({:?}, {:?}, {:?})",
self.split, self.begin_curve, self.end_curve
)
}
}
pub struct Threshold {
threshold: f64,
}
impl Threshold {
pub const fn new(threshold: f64) -> Threshold {
Threshold { threshold }
}
pub const fn threshold(&self) -> f64 {
self.threshold
}
}
impl Curve for Threshold {
fn transform_internal(&self, t: f64) -> f64 {
debug_assert!(self.threshold >= 0.0);
debug_assert!(self.threshold <= 1.0);
if t < self.threshold { 0.0 } else { 1.0 }
}
}
impl Debug for Threshold {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Threshold")
}
}
pub struct Cubic {
a: f64,
b: f64,
c: f64,
d: f64,
}
impl Cubic {
pub const fn new(a: f64, b: f64, c: f64, d: f64) -> Cubic {
Cubic { a, b, c, d }
}
pub const fn a(&self) -> f64 {
self.a
}
pub const fn b(&self) -> f64 {
self.b
}
pub const fn c(&self) -> f64 {
self.c
}
pub const fn d(&self) -> f64 {
self.d
}
const CUBIC_ERROR_BOUND: f64 = 0.001;
fn evaluate_cubic(a: f64, b: f64, m: f64) -> f64 {
3.0 * a * (1.0 - m) * (1.0 - m) * m + 3.0 * b * (1.0 - m) * m * m + m * m * m
}
}
impl Curve for Cubic {
fn transform_internal(&self, t: f64) -> f64 {
assert!(!t.is_nan(), "t: must not be NaN");
if t <= 0.0 {
return 0.0;
}
if t >= 1.0 {
return 1.0;
}
let mut start = 0.0;
let mut end = 1.0;
loop {
let midpoint = (start + end) / 2.0;
let estimate = Cubic::evaluate_cubic(self.a, self.c, midpoint);
if (t - estimate).abs() < Cubic::CUBIC_ERROR_BOUND {
return Cubic::evaluate_cubic(self.b, self.d, midpoint);
}
if estimate < t {
start = midpoint;
} else {
end = midpoint;
}
}
}
}
impl Debug for Cubic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Cubic({:.2}, {:.2}, {:.2}, {:.2})",
self.a, self.b, self.c, self.d
)
}
}
pub struct ThreePointCubic {
a1: Offset,
b1: Offset,
midpoint: Offset,
a2: Offset,
b2: Offset,
}
impl ThreePointCubic {
pub const fn new(
a1: Offset,
b1: Offset,
midpoint: Offset,
a2: Offset,
b2: Offset,
) -> ThreePointCubic {
ThreePointCubic {
a1,
b1,
midpoint,
a2,
b2,
}
}
pub const fn a1(&self) -> Offset {
self.a1
}
pub const fn b1(&self) -> Offset {
self.b1
}
pub const fn midpoint(&self) -> Offset {
self.midpoint
}
pub const fn a2(&self) -> Offset {
self.a2
}
pub const fn b2(&self) -> Offset {
self.b2
}
}
impl Curve for ThreePointCubic {
fn transform_internal(&self, t: f64) -> f64 {
let first_curve = t < self.midpoint.dx();
let scale_x = if first_curve {
self.midpoint.dx()
} else {
1.0 - self.midpoint.dx()
};
let scale_y = if first_curve {
self.midpoint.dy()
} else {
1.0 - self.midpoint.dy()
};
let scaled_t = (t - if first_curve { 0.0 } else { self.midpoint.dx() }) / scale_x;
if first_curve {
Cubic::new(
self.a1.dx() / scale_x,
self.a1.dy() / scale_y,
self.b1.dx() / scale_x,
self.b1.dy() / scale_y,
)
.transform(scaled_t)
* scale_y
} else {
Cubic::new(
(self.a2.dx() - self.midpoint.dx()) / scale_x,
(self.a2.dy() - self.midpoint.dy()) / scale_y,
(self.b2.dx() - self.midpoint.dx()) / scale_x,
(self.b2.dy() - self.midpoint.dy()) / scale_y,
)
.transform(scaled_t)
* scale_y
+ self.midpoint.dy()
}
}
}
impl Debug for ThreePointCubic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("ThreePointCubic ")
}
}
pub struct FlippedCurve {
curve: Rc<dyn Curve>,
}
impl FlippedCurve {
pub fn new(curve: Rc<dyn Curve>) -> FlippedCurve {
FlippedCurve { curve }
}
pub fn curve(&self) -> &Rc<dyn Curve> {
&self.curve
}
}
impl Curve for FlippedCurve {
fn transform_internal(&self, t: f64) -> f64 {
1.0 - self.curve.transform(1.0 - t)
}
}
impl Debug for FlippedCurve {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "FlippedCurve({:?})", self.curve)
}
}
#[derive(Debug)]
struct DecelerateCurve;
impl Curve for DecelerateCurve {
fn transform_internal(&self, t: f64) -> f64 {
let t = 1.0 - t;
1.0 - t * t
}
}
fn bounce(t: f64) -> f64 {
if t < 1.0 / 2.75 {
return 7.5625 * t * t;
} else if t < 2.0 / 2.75 {
let t = t - 1.5 / 2.75;
return 7.5625 * t * t + 0.75;
} else if t < 2.5 / 2.75 {
let t = t - 2.25 / 2.75;
return 7.5625 * t * t + 0.9375;
}
let t = t - 2.625 / 2.75;
7.5625 * t * t + 0.984375
}
#[derive(Debug)]
struct BounceInCurve;
impl Curve for BounceInCurve {
fn transform_internal(&self, t: f64) -> f64 {
1.0 - bounce(1.0 - t)
}
}
#[derive(Debug)]
struct BounceOutCurve;
impl Curve for BounceOutCurve {
fn transform_internal(&self, t: f64) -> f64 {
bounce(t)
}
}
#[derive(Debug)]
struct BounceInOutCurve;
impl Curve for BounceInOutCurve {
fn transform_internal(&self, t: f64) -> f64 {
if t < 0.5 {
(1.0 - bounce(1.0 - t * 2.0)) * 0.5
} else {
bounce(t * 2.0 - 1.0) * 0.5 + 0.5
}
}
}
pub struct ElasticInCurve {
period: f64,
}
impl ElasticInCurve {
pub const fn new(period: f64) -> ElasticInCurve {
ElasticInCurve { period }
}
pub const fn period(&self) -> f64 {
self.period
}
}
impl Curve for ElasticInCurve {
fn transform_internal(&self, t: f64) -> f64 {
let s = self.period / 4.0;
let t = t - 1.0;
-(2.0_f64.powf(10.0 * t)) * ((t - s) * (PI * 2.0) / self.period).sin()
}
}
impl Debug for ElasticInCurve {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ElasticInCurve({:?})", self.period)
}
}
pub struct ElasticOutCurve {
period: f64,
}
impl ElasticOutCurve {
pub const fn new(period: f64) -> ElasticOutCurve {
ElasticOutCurve { period }
}
pub const fn period(&self) -> f64 {
self.period
}
}
impl Curve for ElasticOutCurve {
fn transform_internal(&self, t: f64) -> f64 {
let s = self.period / 4.0;
2.0_f64.powf(-10.0 * t) * ((t - s) * (PI * 2.0) / self.period).sin() + 1.0
}
}
impl Debug for ElasticOutCurve {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ElasticOutCurve({:?})", self.period)
}
}
pub struct ElasticInOutCurve {
period: f64,
}
impl ElasticInOutCurve {
pub const fn new(period: f64) -> ElasticInOutCurve {
ElasticInOutCurve { period }
}
pub const fn period(&self) -> f64 {
self.period
}
}
impl Curve for ElasticInOutCurve {
fn transform_internal(&self, t: f64) -> f64 {
let s = self.period / 4.0;
let t = 2.0 * t - 1.0;
if t < 0.0 {
-0.5 * 2.0_f64.powf(10.0 * t) * ((t - s) * (PI * 2.0) / self.period).sin()
} else {
2.0_f64.powf(-10.0 * t) * ((t - s) * (PI * 2.0) / self.period).sin() * 0.5 + 1.0
}
}
}
impl Debug for ElasticInOutCurve {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ElasticInOutCurve({:?})", self.period)
}
}
pub enum Curves {}
macro_rules! predefined_curves {
($( $(#[$doc:meta])* $name:ident = $value:expr; )*) => {
impl Curves {
$(
$(#[$doc])*
pub fn $name() -> Rc<dyn Curve> {
thread_local! {
static CURVE: Rc<dyn Curve> = Rc::new($value);
}
CURVE.with(Rc::clone)
}
)*
}
};
}
predefined_curves! {
linear = Linear;
decelerate = DecelerateCurve;
fast_linear_to_slow_ease_in = Cubic::new(0.18, 1.0, 0.04, 1.0);
fast_ease_in_to_slow_ease_out = ThreePointCubic::new(
Offset::new(0.056, 0.024),
Offset::new(0.108, 0.3085),
Offset::new(0.198, 0.541),
Offset::new(0.3655, 1.0),
Offset::new(0.5465, 0.989),
);
ease = Cubic::new(0.25, 0.1, 0.25, 1.0);
ease_in = Cubic::new(0.42, 0.0, 1.0, 1.0);
ease_in_to_linear = Cubic::new(0.67, 0.03, 0.65, 0.09);
ease_in_sine = Cubic::new(0.47, 0.0, 0.745, 0.715);
ease_in_quad = Cubic::new(0.55, 0.085, 0.68, 0.53);
ease_in_cubic = Cubic::new(0.55, 0.055, 0.675, 0.19);
ease_in_quart = Cubic::new(0.895, 0.03, 0.685, 0.22);
ease_in_quint = Cubic::new(0.755, 0.05, 0.855, 0.06);
ease_in_expo = Cubic::new(0.95, 0.05, 0.795, 0.035);
ease_in_circ = Cubic::new(0.6, 0.04, 0.98, 0.335);
ease_in_back = Cubic::new(0.6, -0.28, 0.735, 0.045);
ease_out = Cubic::new(0.0, 0.0, 0.58, 1.0);
linear_to_ease_out = Cubic::new(0.35, 0.91, 0.33, 0.97);
ease_out_sine = Cubic::new(0.39, 0.575, 0.565, 1.0);
ease_out_quad = Cubic::new(0.25, 0.46, 0.45, 0.94);
ease_out_cubic = Cubic::new(0.215, 0.61, 0.355, 1.0);
ease_out_quart = Cubic::new(0.165, 0.84, 0.44, 1.0);
ease_out_quint = Cubic::new(0.23, 1.0, 0.32, 1.0);
ease_out_expo = Cubic::new(0.19, 1.0, 0.22, 1.0);
ease_out_circ = Cubic::new(0.075, 0.82, 0.165, 1.0);
ease_out_back = Cubic::new(0.175, 0.885, 0.32, 1.275);
ease_in_out = Cubic::new(0.42, 0.0, 0.58, 1.0);
ease_in_out_sine = Cubic::new(0.445, 0.05, 0.55, 0.95);
ease_in_out_quad = Cubic::new(0.455, 0.03, 0.515, 0.955);
ease_in_out_cubic = Cubic::new(0.645, 0.045, 0.355, 1.0);
ease_in_out_cubic_emphasized = ThreePointCubic::new(
Offset::new(0.05, 0.0),
Offset::new(0.133333, 0.06),
Offset::new(0.166666, 0.4),
Offset::new(0.208333, 0.82),
Offset::new(0.25, 1.0),
);
ease_in_out_quart = Cubic::new(0.77, 0.0, 0.175, 1.0);
ease_in_out_quint = Cubic::new(0.86, 0.0, 0.07, 1.0);
ease_in_out_expo = Cubic::new(1.0, 0.0, 0.0, 1.0);
ease_in_out_circ = Cubic::new(0.785, 0.135, 0.15, 0.86);
ease_in_out_back = Cubic::new(0.68, -0.55, 0.265, 1.55);
fast_out_slow_in = Cubic::new(0.4, 0.0, 0.2, 1.0);
slow_middle = Cubic::new(0.15, 0.85, 0.85, 0.15);
bounce_in = BounceInCurve;
bounce_out = BounceOutCurve;
bounce_in_out = BounceInOutCurve;
elastic_in = ElasticInCurve::new(0.4);
elastic_out = ElasticOutCurve::new(0.4);
elastic_in_out = ElasticInOutCurve::new(0.4);
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_maps_zero_to_zero_and_one_to_one(curve: &dyn Curve) {
assert_eq!(curve.transform(0.0), 0.0, "{curve:?}");
assert_eq!(curve.transform(1.0), 1.0, "{curve:?}");
}
#[test]
fn every_predefined_curve_maps_the_endpoints_to_themselves() {
for curve in [
Curves::linear(),
Curves::decelerate(),
Curves::fast_linear_to_slow_ease_in(),
Curves::fast_ease_in_to_slow_ease_out(),
Curves::ease(),
Curves::ease_in(),
Curves::ease_in_to_linear(),
Curves::ease_in_sine(),
Curves::ease_in_quad(),
Curves::ease_in_cubic(),
Curves::ease_in_quart(),
Curves::ease_in_quint(),
Curves::ease_in_expo(),
Curves::ease_in_circ(),
Curves::ease_in_back(),
Curves::ease_out(),
Curves::linear_to_ease_out(),
Curves::ease_out_sine(),
Curves::ease_out_quad(),
Curves::ease_out_cubic(),
Curves::ease_out_quart(),
Curves::ease_out_quint(),
Curves::ease_out_expo(),
Curves::ease_out_circ(),
Curves::ease_out_back(),
Curves::ease_in_out(),
Curves::ease_in_out_sine(),
Curves::ease_in_out_quad(),
Curves::ease_in_out_cubic(),
Curves::ease_in_out_cubic_emphasized(),
Curves::ease_in_out_quart(),
Curves::ease_in_out_quint(),
Curves::ease_in_out_expo(),
Curves::ease_in_out_circ(),
Curves::ease_in_out_back(),
Curves::fast_out_slow_in(),
Curves::slow_middle(),
Curves::bounce_in(),
Curves::bounce_out(),
Curves::bounce_in_out(),
Curves::elastic_in(),
Curves::elastic_out(),
Curves::elastic_in_out(),
] {
assert_maps_zero_to_zero_and_one_to_one(&*curve);
}
}
#[test]
fn a_predefined_curve_is_one_object_so_two_reads_compare_equal() {
assert!(Rc::ptr_eq(&Curves::ease_in(), &Curves::ease_in()));
assert!(!Rc::ptr_eq(&Curves::ease_in(), &Curves::ease_out()));
assert!(!Rc::ptr_eq(
&(Rc::new(Cubic::new(0.42, 0.0, 1.0, 1.0)) as Rc<dyn Curve>),
&Curves::ease_in()
));
}
#[test]
fn linear_returns_its_input() {
let linear = Curves::linear();
for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
assert_eq!(linear.transform(t), t);
}
}
#[test]
fn a_sawtooth_repeats_and_drops_back_to_zero() {
let curve = SawTooth::new(3);
assert_eq!(curve.transform(0.0), 0.0);
assert!((curve.transform(0.5 / 3.0) - 0.5).abs() < 1e-9);
assert!(curve.transform(0.999 / 3.0) > 0.99);
assert!(curve.transform(1.001 / 3.0) < 0.01);
assert_eq!(curve.transform(1.0), 1.0);
}
#[test]
fn an_interval_is_flat_outside_its_bounds() {
let curve = Interval::new(0.25, 0.75, Curves::linear());
assert_eq!(curve.transform(0.0), 0.0);
assert_eq!(curve.transform(0.25), 0.0);
assert_eq!(curve.transform(0.5), 0.5);
assert_eq!(curve.transform(0.75), 1.0);
assert_eq!(curve.transform(1.0), 1.0);
}
#[test]
fn a_split_runs_each_half_over_its_own_curve() {
let curve = Split::new(0.5, Curves::linear(), Curves::linear());
assert_eq!(curve.transform(0.0), 0.0);
assert_eq!(curve.transform(0.25), 0.25);
assert_eq!(curve.transform(0.5), 0.5);
assert_eq!(curve.transform(0.75), 0.75);
assert_eq!(curve.transform(1.0), 1.0);
}
#[test]
fn a_threshold_jumps_once() {
let curve = Threshold::new(0.5);
assert_eq!(curve.transform(0.0), 0.0);
assert_eq!(curve.transform(0.25), 0.0);
assert_eq!(curve.transform(0.5), 1.0);
assert_eq!(curve.transform(0.75), 1.0);
assert_eq!(curve.transform(1.0), 1.0);
}
#[test]
fn a_flipped_curve_reverses_and_inverts_its_curve() {
let decelerate = Curves::decelerate();
let flipped = FlippedCurve::new(Rc::clone(&decelerate));
for t in [0.1, 0.3, 0.5, 0.7, 0.9] {
let expected = 1.0 - decelerate.transform(1.0 - t);
assert!((flipped.transform(t) - expected).abs() < 1e-12);
}
}
#[test]
fn flipped_on_an_erased_curve_wraps_it() {
let flipped = Curves::decelerate().flipped();
assert!(
(flipped.transform(0.25) - (1.0 - Curves::decelerate().transform(0.75))).abs() < 1e-12
);
}
#[test]
fn a_cubic_stays_inside_the_unit_interval() {
let curve = Curves::ease_in_out();
let mut previous = 0.0;
for step in 0..=100 {
let value = curve.transform(step as f64 / 100.0);
assert!((0.0..=1.0).contains(&value), "{value} at step {step}");
assert!(value >= previous, "{value} < {previous} at step {step}");
previous = value;
}
}
#[test]
fn a_three_point_cubic_passes_through_its_midpoint() {
let curve = ThreePointCubic::new(
Offset::new(0.05, 0.0),
Offset::new(0.133333, 0.06),
Offset::new(0.166666, 0.4),
Offset::new(0.208333, 0.82),
Offset::new(0.25, 1.0),
);
assert!((curve.transform(0.166666) - 0.4).abs() < 0.01);
}
#[test]
fn the_elastic_curves_overshoot_between_the_endpoints() {
assert!(Curves::elastic_out().transform(0.15) > 1.0);
assert!(Curves::elastic_in().transform(0.85) < 0.0);
}
#[test]
fn debug_matches_flutter_to_string() {
assert_eq!(format!("{:?}", SawTooth::new(3)), "SawTooth(3)");
assert_eq!(
format!("{:?}", Cubic::new(0.42, 0.0, 1.0, 1.0)),
"Cubic(0.42, 0.00, 1.00, 1.00)"
);
assert_eq!(
format!("{:?}", Interval::new(0.0, 0.5, Curves::linear())),
"Interval(0.0\u{22ef}0.5)"
);
assert_eq!(
format!("{:?}", Interval::new(0.0, 0.5, Curves::ease())),
"Interval(0.0\u{22ef}0.5)\u{27a9}Cubic(0.25, 0.10, 0.25, 1.00)"
);
assert_eq!(
format!("{:?}", ElasticInCurve::new(0.4)),
"ElasticInCurve(0.4)"
);
assert_eq!(
format!("{:?}", FlippedCurve::new(Curves::linear())),
"FlippedCurve(Linear)"
);
assert_eq!(format!("{:?}", Threshold::new(0.5)), "Threshold");
assert_eq!(
format!("{:?}", Curves::ease_in_out_cubic_emphasized()),
"ThreePointCubic "
);
}
}