1use std::{fmt, num::NonZeroU32, rc::Rc, sync::Arc};
2
3use crate::animation::cubic_bezier;
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum StepPosition {
8 JumpStart,
9 JumpEnd,
10 JumpNone,
11 JumpBoth,
12}
13
14#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct LinearStop {
17 pub output: f32,
18 pub input: Option<f32>,
19}
20
21impl LinearStop {
22 pub const fn new(output: f32) -> Self {
23 Self {
24 output,
25 input: None,
26 }
27 }
28
29 pub const fn at(output: f32, input: f32) -> Self {
30 Self {
31 output,
32 input: Some(input),
33 }
34 }
35}
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum EasingError {
40 #[deprecated(note = "use InvalidBezierControlPoint")]
43 InvalidBezierX,
44 InvalidBezierControlPoint,
45 InvalidStepCount,
46 InvalidLinearStops,
47}
48
49impl fmt::Display for EasingError {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 #[allow(deprecated)]
52 match self {
53 Self::InvalidBezierX | Self::InvalidBezierControlPoint => {
54 f.write_str("cubic Bézier control points must be finite and x must be within 0..=1")
55 }
56 Self::InvalidStepCount => f.write_str("step easing requires a valid step count"),
57 Self::InvalidLinearStops => f.write_str("linear easing stops are invalid"),
58 }
59 }
60}
61
62impl std::error::Error for EasingError {}
63
64#[derive(Clone, Default)]
66pub enum Easing {
67 Linear,
68 Ease,
69 EaseIn,
70 #[default]
71 EaseOut,
72 EaseInOut,
73 CubicBezier {
74 x1: f32,
75 y1: f32,
76 x2: f32,
77 y2: f32,
78 },
79 Steps {
80 count: NonZeroU32,
81 position: StepPosition,
82 },
83 LinearStops(Arc<[(f32, f32)]>),
84 Custom(Rc<dyn Fn(f32) -> f32>),
85}
86
87impl fmt::Debug for Easing {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 match self {
90 Self::Linear => f.write_str("Linear"),
91 Self::Ease => f.write_str("Ease"),
92 Self::EaseIn => f.write_str("EaseIn"),
93 Self::EaseOut => f.write_str("EaseOut"),
94 Self::EaseInOut => f.write_str("EaseInOut"),
95 Self::CubicBezier { x1, y1, x2, y2 } => f
96 .debug_struct("CubicBezier")
97 .field("x1", x1)
98 .field("y1", y1)
99 .field("x2", x2)
100 .field("y2", y2)
101 .finish(),
102 Self::Steps { count, position } => f
103 .debug_struct("Steps")
104 .field("count", count)
105 .field("position", position)
106 .finish(),
107 Self::LinearStops(stops) => f.debug_tuple("LinearStops").field(stops).finish(),
108 Self::Custom(_) => f.write_str("Custom(..)"),
109 }
110 }
111}
112
113impl Easing {
114 pub fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> Result<Self, EasingError> {
115 if !x1.is_finite()
116 || !x2.is_finite()
117 || !(0.0..=1.0).contains(&x1)
118 || !(0.0..=1.0).contains(&x2)
119 || !y1.is_finite()
120 || !y2.is_finite()
121 {
122 return Err(EasingError::InvalidBezierControlPoint);
123 }
124 Ok(Self::CubicBezier { x1, y1, x2, y2 })
125 }
126
127 pub fn steps(count: u32, position: StepPosition) -> Result<Self, EasingError> {
128 let count = NonZeroU32::new(count).ok_or(EasingError::InvalidStepCount)?;
129 if position == StepPosition::JumpNone && count.get() == 1 {
130 return Err(EasingError::InvalidStepCount);
131 }
132 Ok(Self::Steps { count, position })
133 }
134
135 pub fn linear_stops(stops: impl IntoIterator<Item = LinearStop>) -> Result<Self, EasingError> {
136 let mut stops: Vec<_> = stops.into_iter().collect();
137 if stops.len() < 2
138 || stops
139 .iter()
140 .any(|stop| !stop.output.is_finite() || stop.input.is_some_and(|v| !v.is_finite()))
141 {
142 return Err(EasingError::InvalidLinearStops);
143 }
144
145 if stops[0].input.is_none() {
146 stops[0].input = Some(0.0);
147 }
148 let last = stops.len() - 1;
149 if stops[last].input.is_none() {
150 stops[last].input = Some(1.0);
151 }
152
153 let mut anchor = 0;
154 while anchor < last {
155 let Some(next) = ((anchor + 1)..=last).find(|&ix| stops[ix].input.is_some()) else {
156 return Err(EasingError::InvalidLinearStops);
157 };
158 let from = stops[anchor].input.unwrap();
159 let to = stops[next].input.unwrap();
160 if !(0.0..=1.0).contains(&from) || !(0.0..=1.0).contains(&to) || to < from {
161 return Err(EasingError::InvalidLinearStops);
162 }
163 let span = (next - anchor) as f32;
164 for (offset, stop) in stops[(anchor + 1)..next].iter_mut().enumerate() {
165 stop.input = Some(from + (to - from) * (offset + 1) as f32 / span);
166 }
167 anchor = next;
168 }
169
170 Ok(Self::LinearStops(
171 stops
172 .into_iter()
173 .map(|stop| (stop.input.unwrap(), stop.output))
174 .collect(),
175 ))
176 }
177
178 #[inline]
179 pub fn sample(&self, progress: f32) -> f32 {
180 let progress = progress.clamp(0.0, 1.0);
181 match self {
182 Self::Linear => progress,
183 Self::Ease => cubic_bezier(0.25, 0.1, 0.25, 1.0)(progress),
184 Self::EaseIn => cubic_bezier(0.42, 0.0, 1.0, 1.0)(progress),
185 Self::EaseOut => cubic_bezier(0.0, 0.0, 0.58, 1.0)(progress),
186 Self::EaseInOut => cubic_bezier(0.42, 0.0, 0.58, 1.0)(progress),
187 Self::CubicBezier { x1, y1, x2, y2 } => cubic_bezier(*x1, *y1, *x2, *y2)(progress),
188 Self::Steps { count, position } => {
189 let count = count.get() as f32;
190 let (jumps, offset) = match position {
191 StepPosition::JumpStart => (count, 1.0),
192 StepPosition::JumpEnd => (count, 0.0),
193 StepPosition::JumpNone => (count - 1.0, 0.0),
194 StepPosition::JumpBoth => (count + 1.0, 1.0),
195 };
196 ((progress * count).floor() + offset).clamp(0.0, jumps) / jumps
197 }
198 Self::LinearStops(stops) => {
199 let upper = stops.partition_point(|(input, _)| *input <= progress);
200 if upper == 0 {
201 return stops[0].1;
202 }
203 if upper == stops.len() {
204 return stops[stops.len() - 1].1;
205 }
206 let (x0, y0) = stops[upper - 1];
207 let (x1, y1) = stops[upper];
208 if x0 == x1 {
209 y1
210 } else {
211 y0 + (y1 - y0) * ((progress - x0) / (x1 - x0))
212 }
213 }
214 Self::Custom(easing) => easing(progress),
215 }
216 }
217}
218
219#[cfg(test)]
220mod error_tests {
221 use super::*;
222
223 #[test]
224 fn bezier_errors_name_the_invalid_control_point() {
225 let error = Easing::cubic_bezier(0.2, f32::NAN, 0.8, 1.0).unwrap_err();
226
227 assert_eq!(error, EasingError::InvalidBezierControlPoint);
228 assert_eq!(
229 error.to_string(),
230 "cubic Bézier control points must be finite and x must be within 0..=1"
231 );
232 let _: &dyn std::error::Error = &error;
233 }
234}