Skip to main content

argui_animation/
easing.rs

1use std::{fmt, sync::Arc};
2
3type CustomFunction = dyn Fn(f32) -> f32 + Send + Sync + 'static;
4
5#[derive(Clone, Default)]
6pub enum Easing {
7    #[default]
8    Linear,
9    CubicBezier(CubicBezier),
10    Steps(Steps),
11    PiecewiseLinear(Box<[LinearStop]>),
12    Custom(Arc<CustomFunction>),
13}
14
15impl Easing {
16    #[must_use]
17    pub fn custom(function: impl Fn(f32) -> f32 + Send + Sync + 'static) -> Self {
18        Self::Custom(Arc::new(function))
19    }
20
21    pub fn piecewise_linear(stops: impl Into<Vec<LinearStop>>) -> Result<Self, EasingError> {
22        let stops = stops.into();
23        if stops.len() < 2
24            || stops.first().is_none_or(|stop| stop.input != 0.0)
25            || stops.last().is_none_or(|stop| stop.input != 1.0)
26            || stops.iter().any(|stop| {
27                !stop.input.is_finite()
28                    || !stop.output.is_finite()
29                    || !(0.0..=1.0).contains(&stop.input)
30            })
31            || stops.windows(2).any(|pair| pair[0].input > pair[1].input)
32        {
33            return Err(EasingError::InvalidStops);
34        }
35        Ok(Self::PiecewiseLinear(stops.into_boxed_slice()))
36    }
37
38    #[must_use]
39    pub fn sample(&self, progress: f32) -> f32 {
40        let progress = progress.clamp(0.0, 1.0);
41        match self {
42            Self::Linear => progress,
43            Self::CubicBezier(curve) => curve.sample(progress),
44            Self::Steps(steps) => steps.sample(progress),
45            Self::PiecewiseLinear(stops) => sample_stops(stops, progress),
46            Self::Custom(function) => function(progress),
47        }
48    }
49}
50
51impl fmt::Debug for Easing {
52    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            Self::Linear => formatter.write_str("Linear"),
55            Self::CubicBezier(value) => formatter.debug_tuple("CubicBezier").field(value).finish(),
56            Self::Steps(value) => formatter.debug_tuple("Steps").field(value).finish(),
57            Self::PiecewiseLinear(value) => formatter
58                .debug_tuple("PiecewiseLinear")
59                .field(value)
60                .finish(),
61            Self::Custom(_) => formatter.write_str("Custom(..)"),
62        }
63    }
64}
65
66impl PartialEq for Easing {
67    fn eq(&self, other: &Self) -> bool {
68        match (self, other) {
69            (Self::Linear, Self::Linear) => true,
70            (Self::CubicBezier(left), Self::CubicBezier(right)) => left == right,
71            (Self::Steps(left), Self::Steps(right)) => left == right,
72            (Self::PiecewiseLinear(left), Self::PiecewiseLinear(right)) => left == right,
73            (Self::Custom(left), Self::Custom(right)) => Arc::ptr_eq(left, right),
74            _ => false,
75        }
76    }
77}
78
79#[derive(Clone, Copy, Debug, PartialEq)]
80pub struct CubicBezier {
81    x1: f32,
82    y1: f32,
83    x2: f32,
84    y2: f32,
85}
86
87impl CubicBezier {
88    pub fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Result<Self, EasingError> {
89        if [x1, y1, x2, y2].iter().any(|value| !value.is_finite())
90            || !(0.0..=1.0).contains(&x1)
91            || !(0.0..=1.0).contains(&x2)
92        {
93            return Err(EasingError::InvalidBezier);
94        }
95        Ok(Self { x1, y1, x2, y2 })
96    }
97
98    #[must_use]
99    pub fn sample(self, progress: f32) -> f32 {
100        let progress = progress.clamp(0.0, 1.0);
101        let mut low = 0.0;
102        let mut high = 1.0;
103        let mut parameter = progress;
104        for _ in 0..16 {
105            let x = cubic(parameter, self.x1, self.x2);
106            if (x - progress).abs() <= 1.0e-5 {
107                break;
108            }
109            if x < progress {
110                low = parameter;
111            } else {
112                high = parameter;
113            }
114            parameter = (low + high) * 0.5;
115        }
116        cubic(parameter, self.y1, self.y2)
117    }
118}
119
120fn cubic(parameter: f32, first: f32, second: f32) -> f32 {
121    let inverse = 1.0 - parameter;
122    3.0 * inverse * inverse * parameter * first
123        + 3.0 * inverse * parameter * parameter * second
124        + parameter * parameter * parameter
125}
126
127#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128pub enum StepPosition {
129    JumpStart,
130    JumpEnd,
131    JumpNone,
132    JumpBoth,
133}
134
135#[derive(Clone, Copy, Debug, Eq, PartialEq)]
136pub struct Steps {
137    count: u32,
138    position: StepPosition,
139}
140
141impl Steps {
142    pub fn new(count: u32, position: StepPosition) -> Result<Self, EasingError> {
143        if count == 0 || (position == StepPosition::JumpNone && count == 1) {
144            return Err(EasingError::InvalidSteps);
145        }
146        Ok(Self { count, position })
147    }
148
149    #[must_use]
150    pub fn sample(self, progress: f32) -> f32 {
151        let progress = progress.clamp(0.0, 1.0);
152        let count = self.count as f32;
153        match self.position {
154            StepPosition::JumpStart => ((progress * count).floor() + 1.0).min(count) / count,
155            StepPosition::JumpEnd => (progress * count).floor() / count,
156            StepPosition::JumpNone => ((progress * count).floor() / (count - 1.0)).clamp(0.0, 1.0),
157            StepPosition::JumpBoth => ((progress * count).floor() + 1.0) / (count + 1.0),
158        }
159    }
160}
161
162#[derive(Clone, Copy, Debug, PartialEq)]
163pub struct LinearStop {
164    pub input: f32,
165    pub output: f32,
166}
167
168impl LinearStop {
169    #[must_use]
170    pub const fn new(input: f32, output: f32) -> Self {
171        Self { input, output }
172    }
173}
174
175fn sample_stops(stops: &[LinearStop], progress: f32) -> f32 {
176    let upper = stops.partition_point(|stop| stop.input <= progress);
177    if upper == stops.len() {
178        return stops[upper - 1].output;
179    }
180    let from = stops[upper - 1];
181    let to = stops[upper];
182    if from.input == to.input {
183        return to.output;
184    }
185    let local = (progress - from.input) / (to.input - from.input);
186    from.output + (to.output - from.output) * local
187}
188
189#[derive(Clone, Copy, Debug, Eq, PartialEq)]
190pub enum EasingError {
191    InvalidBezier,
192    InvalidSteps,
193    InvalidStops,
194}
195
196impl fmt::Display for EasingError {
197    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
198        match self {
199            Self::InvalidBezier => formatter.write_str(
200                "Bezier x coordinates must be within 0..=1 and all coordinates must be finite",
201            ),
202            Self::InvalidSteps => {
203                formatter.write_str("steps require at least one jump, or two for jump-none")
204            }
205            Self::InvalidStops => {
206                formatter.write_str("linear stops must be finite, sorted, and cover 0..=1")
207            }
208        }
209    }
210}
211
212impl std::error::Error for EasingError {}