armas_basic/animation/
easing.rs1#[derive(Debug, Clone, Copy, PartialEq, Default)]
18pub enum EasingFunction {
19 Linear,
21 EaseIn,
23 EaseOut,
25 #[default]
27 EaseInOut,
28 QuadIn,
30 QuadOut,
32 QuadInOut,
34 CubicIn,
36 CubicOut,
38 CubicInOut,
40 ExpoIn,
42 ExpoOut,
44 ExpoInOut,
46 ElasticIn,
48 ElasticOut,
50 BounceOut,
52 Cubic {
54 x1: f32,
56 y1: f32,
58 x2: f32,
60 y2: f32,
62 },
63}
64
65impl EasingFunction {
66 #[must_use]
68 pub fn apply(&self, t: f32) -> f32 {
69 let t = t.clamp(0.0, 1.0);
70
71 match self {
72 Self::Linear => t,
73 Self::EaseIn | Self::QuadIn => quad_in(t),
74 Self::EaseOut | Self::QuadOut => quad_out(t),
75 Self::EaseInOut | Self::QuadInOut => quad_in_out(t),
76 Self::CubicIn => cubic_in(t),
77 Self::CubicOut => cubic_out(t),
78 Self::CubicInOut => cubic_in_out(t),
79 Self::ExpoIn => expo_in(t),
80 Self::ExpoOut => expo_out(t),
81 Self::ExpoInOut => expo_in_out(t),
82 Self::ElasticIn => elastic_in(t),
83 Self::ElasticOut => elastic_out(t),
84 Self::BounceOut => bounce_out(t),
85 Self::Cubic { x1, y1, x2, y2 } => cubic_bezier(t, *x1, *y1, *x2, *y2),
86 }
87 }
88}
89
90fn quad_in(t: f32) -> f32 {
92 t * t
93}
94
95fn quad_out(t: f32) -> f32 {
96 t * (2.0 - t)
97}
98
99fn quad_in_out(t: f32) -> f32 {
100 if t < 0.5 {
101 2.0 * t * t
102 } else {
103 -1.0 + (4.0 - 2.0 * t) * t
104 }
105}
106
107fn cubic_in(t: f32) -> f32 {
109 t * t * t
110}
111
112fn cubic_out(t: f32) -> f32 {
113 let t = t - 1.0;
114 t * t * t + 1.0
115}
116
117fn cubic_in_out(t: f32) -> f32 {
118 if t < 0.5 {
119 4.0 * t * t * t
120 } else {
121 let t = 2.0 * t - 2.0;
122 1.0 + t * t * t / 2.0
123 }
124}
125
126fn expo_in(t: f32) -> f32 {
128 if t == 0.0 {
129 0.0
130 } else {
131 2.0f32.powf(10.0 * (t - 1.0))
132 }
133}
134
135fn expo_out(t: f32) -> f32 {
136 if t == 1.0 {
137 1.0
138 } else {
139 1.0 - 2.0f32.powf(-10.0 * t)
140 }
141}
142
143fn expo_in_out(t: f32) -> f32 {
144 if t == 0.0 {
145 return 0.0;
146 }
147 if t == 1.0 {
148 return 1.0;
149 }
150
151 if t < 0.5 {
152 2.0f32.powf(20.0 * t - 10.0) / 2.0
153 } else {
154 (2.0 - 2.0f32.powf(-20.0 * t + 10.0)) / 2.0
155 }
156}
157
158fn elastic_in(t: f32) -> f32 {
160 if t == 0.0 || t == 1.0 {
161 return t;
162 }
163
164 let p = 0.3;
165 let s = p / 4.0;
166 let t = t - 1.0;
167
168 -(2.0f32.powf(10.0 * t) * ((t - s) * (2.0 * std::f32::consts::PI) / p).sin())
169}
170
171fn elastic_out(t: f32) -> f32 {
172 if t == 0.0 || t == 1.0 {
173 return t;
174 }
175
176 let p = 0.3;
177 let s = p / 4.0;
178
179 2.0f32.powf(-10.0 * t) * ((t - s) * (2.0 * std::f32::consts::PI) / p).sin() + 1.0
180}
181
182fn bounce_out(t: f32) -> f32 {
184 if t < 1.0 / 2.75 {
185 7.5625 * t * t
186 } else if t < 2.0 / 2.75 {
187 let t = t - 1.5 / 2.75;
188 7.5625 * t * t + 0.75
189 } else if t < 2.5 / 2.75 {
190 let t = t - 2.25 / 2.75;
191 7.5625 * t * t + 0.9375
192 } else {
193 let t = t - 2.625 / 2.75;
194 7.5625 * t * t + 0.984_375
195 }
196}
197
198fn cubic_bezier(t: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
200 let x1 = x1.clamp(0.0, 1.0);
205 let x2 = x2.clamp(0.0, 1.0);
206
207 if x1 == 0.0 && x2 == 1.0 {
209 return t;
210 }
211
212 let mut guess = t;
214 for _ in 0..8 {
215 let guess_x = cubic_bezier_x(guess, x1, x2);
217 let error = guess_x - t;
218
219 if error.abs() < 0.001 {
220 break;
221 }
222
223 let slope = cubic_bezier_x_derivative(guess, x1, x2);
225 if slope.abs() < 0.000_001 {
226 break;
227 }
228
229 guess -= error / slope;
231 guess = guess.clamp(0.0, 1.0);
232 }
233
234 cubic_bezier_y(guess, y1, y2)
236}
237
238fn cubic_bezier_x(t: f32, x1: f32, x2: f32) -> f32 {
242 let t2 = t * t;
243 let t3 = t2 * t;
244 let mt = 1.0 - t;
245 let mt2 = mt * mt;
246
247 3.0 * mt2 * t * x1 + 3.0 * mt * t2 * x2 + t3
248}
249
250fn cubic_bezier_y(t: f32, y1: f32, y2: f32) -> f32 {
252 let t2 = t * t;
253 let t3 = t2 * t;
254 let mt = 1.0 - t;
255 let mt2 = mt * mt;
256
257 3.0 * mt2 * t * y1 + 3.0 * mt * t2 * y2 + t3
258}
259
260fn cubic_bezier_x_derivative(t: f32, x1: f32, x2: f32) -> f32 {
262 let mt = 1.0 - t;
263 let mt2 = mt * mt;
264 let t2 = t * t;
265
266 3.0 * mt2 * x1 + 6.0 * mt * t * (x2 - x1) + 3.0 * t2 * (1.0 - x2)
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn test_linear() {
275 assert_eq!(EasingFunction::Linear.apply(0.0), 0.0);
276 assert_eq!(EasingFunction::Linear.apply(0.5), 0.5);
277 assert_eq!(EasingFunction::Linear.apply(1.0), 1.0);
278 }
279
280 #[test]
281 fn test_ease_in_out_bounds() {
282 let result = EasingFunction::EaseInOut.apply(0.0);
283 assert!((0.0..=1.0).contains(&result));
284
285 let result = EasingFunction::EaseInOut.apply(1.0);
286 assert!((0.0..=1.0).contains(&result));
287 }
288
289 #[test]
290 fn test_all_easing_functions() {
291 let functions = [
292 EasingFunction::Linear,
293 EasingFunction::EaseIn,
294 EasingFunction::EaseOut,
295 EasingFunction::EaseInOut,
296 EasingFunction::QuadIn,
297 EasingFunction::QuadOut,
298 EasingFunction::QuadInOut,
299 EasingFunction::CubicIn,
300 EasingFunction::CubicOut,
301 EasingFunction::CubicInOut,
302 ];
303
304 for func in &functions {
305 assert!((func.apply(0.0) - 0.0).abs() < 0.001);
307 assert!((func.apply(1.0) - 1.0).abs() < 0.001);
308
309 for i in 0..=10 {
311 let t = i as f32 / 10.0;
312 let result = func.apply(t);
313 assert!(
314 (-0.1..=1.1).contains(&result),
315 "Easing {func:?} at t={t} gave {result}"
316 );
317 }
318 }
319 }
320}