1use std::f32::consts::{PI, TAU};
7
8#[derive(Clone, Copy, Debug, PartialEq)]
12pub enum Easing {
13 Linear,
15 EaseInQuad,
16 EaseOutQuad,
17 EaseInOutQuad,
18 EaseInCubic,
19 EaseOutCubic,
20 EaseInOutCubic,
21 EaseInQuart,
22 EaseOutQuart,
23 EaseInOutQuart,
24 EaseInQuint,
25 EaseOutQuint,
26 EaseInOutQuint,
27
28 EaseInSine,
30 EaseOutSine,
31 EaseInOutSine,
32
33 EaseInExpo,
35 EaseOutExpo,
36 EaseInOutExpo,
37
38 EaseInCirc,
40 EaseOutCirc,
41 EaseInOutCirc,
42
43 EaseInBack,
45 EaseOutBack,
46 EaseInOutBack,
47
48 EaseInElastic,
50 EaseOutElastic,
51 EaseInOutElastic,
52
53 EaseInBounce,
55 EaseOutBounce,
56 EaseInOutBounce,
57
58 SmoothStep,
61 SmootherStep,
63 Step,
65 EaseOutLinear,
67 Hermite { p0: f32, m0: f32, p1: f32, m1: f32 },
69 Power(f32),
71 Sigmoid { k: f32 },
73 Spring { stiffness: f32, damping: f32 },
75 Parabola,
77 Flash,
79}
80
81impl Easing {
82 pub fn apply(&self, t: f32) -> f32 {
84 let t = t.clamp(0.0, 1.0);
85 match *self {
86 Easing::Linear => t,
88
89 Easing::EaseInQuad => t * t,
91 Easing::EaseOutQuad => 1.0 - (1.0 - t) * (1.0 - t),
92 Easing::EaseInOutQuad => {
93 if t < 0.5 { 2.0 * t * t }
94 else { 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0 }
95 }
96
97 Easing::EaseInCubic => t * t * t,
99 Easing::EaseOutCubic => 1.0 - (1.0 - t).powi(3),
100 Easing::EaseInOutCubic => {
101 if t < 0.5 { 4.0 * t * t * t }
102 else { 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0 }
103 }
104
105 Easing::EaseInQuart => t * t * t * t,
107 Easing::EaseOutQuart => 1.0 - (1.0 - t).powi(4),
108 Easing::EaseInOutQuart => {
109 if t < 0.5 { 8.0 * t * t * t * t }
110 else { 1.0 - (-2.0 * t + 2.0).powi(4) / 2.0 }
111 }
112
113 Easing::EaseInQuint => t * t * t * t * t,
115 Easing::EaseOutQuint => 1.0 - (1.0 - t).powi(5),
116 Easing::EaseInOutQuint => {
117 if t < 0.5 { 16.0 * t * t * t * t * t }
118 else { 1.0 - (-2.0 * t + 2.0).powi(5) / 2.0 }
119 }
120
121 Easing::EaseInSine => 1.0 - (t * PI / 2.0).cos(),
123 Easing::EaseOutSine => (t * PI / 2.0).sin(),
124 Easing::EaseInOutSine => -((PI * t).cos() - 1.0) / 2.0,
125
126 Easing::EaseInExpo => {
128 if t == 0.0 { 0.0 } else { 2.0_f32.powf(10.0 * t - 10.0) }
129 }
130 Easing::EaseOutExpo => {
131 if t == 1.0 { 1.0 } else { 1.0 - 2.0_f32.powf(-10.0 * t) }
132 }
133 Easing::EaseInOutExpo => {
134 if t == 0.0 { return 0.0; }
135 if t == 1.0 { return 1.0; }
136 if t < 0.5 { 2.0_f32.powf(20.0 * t - 10.0) / 2.0 }
137 else { (2.0 - 2.0_f32.powf(-20.0 * t + 10.0)) / 2.0 }
138 }
139
140 Easing::EaseInCirc => 1.0 - (1.0 - t * t).sqrt(),
142 Easing::EaseOutCirc => (1.0 - (t - 1.0) * (t - 1.0)).sqrt(),
143 Easing::EaseInOutCirc => {
144 if t < 0.5 { (1.0 - (1.0 - (2.0 * t).powi(2)).sqrt()) / 2.0 }
145 else { ((1.0 - (-2.0 * t + 2.0).powi(2)).sqrt() + 1.0) / 2.0 }
146 }
147
148 Easing::EaseInBack => {
150 let c1 = 1.70158_f32;
151 let c3 = c1 + 1.0;
152 c3 * t * t * t - c1 * t * t
153 }
154 Easing::EaseOutBack => {
155 let c1 = 1.70158_f32;
156 let c3 = c1 + 1.0;
157 1.0 + c3 * (t - 1.0).powi(3) + c1 * (t - 1.0).powi(2)
158 }
159 Easing::EaseInOutBack => {
160 let c1 = 1.70158_f32;
161 let c2 = c1 * 1.525;
162 if t < 0.5 {
163 ((2.0 * t).powi(2) * ((c2 + 1.0) * 2.0 * t - c2)) / 2.0
164 } else {
165 ((2.0 * t - 2.0).powi(2) * ((c2 + 1.0) * (2.0 * t - 2.0) + c2) + 2.0) / 2.0
166 }
167 }
168
169 Easing::EaseInElastic => {
171 if t == 0.0 { return 0.0; }
172 if t == 1.0 { return 1.0; }
173 let c4 = TAU / 3.0;
174 -2.0_f32.powf(10.0 * t - 10.0) * ((t * 10.0 - 10.75) * c4).sin()
175 }
176 Easing::EaseOutElastic => {
177 if t == 0.0 { return 0.0; }
178 if t == 1.0 { return 1.0; }
179 let c4 = TAU / 3.0;
180 2.0_f32.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
181 }
182 Easing::EaseInOutElastic => {
183 if t == 0.0 { return 0.0; }
184 if t == 1.0 { return 1.0; }
185 let c5 = TAU / 4.5;
186 if t < 0.5 {
187 -(2.0_f32.powf(20.0 * t - 10.0) * ((20.0 * t - 11.125) * c5).sin()) / 2.0
188 } else {
189 (2.0_f32.powf(-20.0 * t + 10.0) * ((20.0 * t - 11.125) * c5).sin()) / 2.0 + 1.0
190 }
191 }
192
193 Easing::EaseOutBounce => bounce_out(t),
195 Easing::EaseInBounce => 1.0 - bounce_out(1.0 - t),
196 Easing::EaseInOutBounce => {
197 if t < 0.5 { (1.0 - bounce_out(1.0 - 2.0 * t)) / 2.0 }
198 else { (1.0 + bounce_out(2.0 * t - 1.0)) / 2.0 }
199 }
200
201 Easing::SmoothStep => t * t * (3.0 - 2.0 * t),
203 Easing::SmootherStep => t * t * t * (t * (t * 6.0 - 15.0) + 10.0),
204 Easing::Step => if t < 0.5 { 0.0 } else { 1.0 },
205 Easing::EaseOutLinear => t,
206 Easing::Parabola => 4.0 * t * (1.0 - t),
207 Easing::Flash => if t < f32::EPSILON { 1.0 } else { 1.0 - t },
208
209 Easing::Power(n) => t.powf(n),
210
211 Easing::Sigmoid { k } => {
212 let s = |x: f32| 1.0 / (1.0 + (-k * x).exp());
213 (s(t - 0.5) - s(-0.5)) / (s(0.5) - s(-0.5))
214 }
215
216 Easing::Spring { stiffness, damping } => {
217 let omega = stiffness.sqrt();
223 let zeta = damping / (2.0 * omega).max(f32::EPSILON);
224 let response = |t: f32| -> f32 {
225 if zeta >= 1.0 {
226 1.0 - (1.0 + omega * t) * (-omega * t).exp()
227 } else {
228 let omega_d = omega * (1.0 - zeta * zeta).sqrt();
229 let decay = (-zeta * omega * t).exp();
230 1.0 - decay * ((omega_d * t).cos() +
231 (zeta / (1.0 - zeta * zeta).sqrt()) * (omega_d * t).sin())
232 }
233 };
234 response(t) + (1.0 - response(1.0)) * t
235 }
236
237 Easing::Hermite { p0, m0, p1, m1 } => {
238 let t2 = t * t;
239 let t3 = t2 * t;
240 let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
241 let h10 = t3 - 2.0 * t2 + t;
242 let h01 = -2.0 * t3 + 3.0 * t2;
243 let h11 = t3 - t2;
244 h00 * p0 + h10 * m0 + h01 * p1 + h11 * m1
245 }
246 }
247 }
248
249 pub fn derivative(&self, t: f32) -> f32 {
251 let eps = 1e-4_f32;
252 let hi = self.apply((t + eps).min(1.0));
253 let lo = self.apply((t - eps).max(0.0));
254 (hi - lo) / (2.0 * eps)
255 }
256
257 pub fn name(&self) -> &'static str {
259 match self {
260 Easing::Linear => "Linear",
261 Easing::EaseInQuad => "EaseInQuad",
262 Easing::EaseOutQuad => "EaseOutQuad",
263 Easing::EaseInOutQuad => "EaseInOutQuad",
264 Easing::EaseInCubic => "EaseInCubic",
265 Easing::EaseOutCubic => "EaseOutCubic",
266 Easing::EaseInOutCubic => "EaseInOutCubic",
267 Easing::EaseInQuart => "EaseInQuart",
268 Easing::EaseOutQuart => "EaseOutQuart",
269 Easing::EaseInOutQuart => "EaseInOutQuart",
270 Easing::EaseInQuint => "EaseInQuint",
271 Easing::EaseOutQuint => "EaseOutQuint",
272 Easing::EaseInOutQuint => "EaseInOutQuint",
273 Easing::EaseInSine => "EaseInSine",
274 Easing::EaseOutSine => "EaseOutSine",
275 Easing::EaseInOutSine => "EaseInOutSine",
276 Easing::EaseInExpo => "EaseInExpo",
277 Easing::EaseOutExpo => "EaseOutExpo",
278 Easing::EaseInOutExpo => "EaseInOutExpo",
279 Easing::EaseInCirc => "EaseInCirc",
280 Easing::EaseOutCirc => "EaseOutCirc",
281 Easing::EaseInOutCirc => "EaseInOutCirc",
282 Easing::EaseInBack => "EaseInBack",
283 Easing::EaseOutBack => "EaseOutBack",
284 Easing::EaseInOutBack => "EaseInOutBack",
285 Easing::EaseInElastic => "EaseInElastic",
286 Easing::EaseOutElastic => "EaseOutElastic",
287 Easing::EaseInOutElastic => "EaseInOutElastic",
288 Easing::EaseInBounce => "EaseInBounce",
289 Easing::EaseOutBounce => "EaseOutBounce",
290 Easing::EaseInOutBounce => "EaseInOutBounce",
291 Easing::SmoothStep => "SmoothStep",
292 Easing::SmootherStep => "SmootherStep",
293 Easing::Step => "Step",
294 Easing::EaseOutLinear => "EaseOutLinear",
295 Easing::Parabola => "Parabola",
296 Easing::Flash => "Flash",
297 Easing::Power(_) => "Power",
298 Easing::Sigmoid { .. } => "Sigmoid",
299 Easing::Spring { .. } => "Spring",
300 Easing::Hermite { .. } => "Hermite",
301 }
302 }
303
304 pub fn all_named() -> &'static [Easing] {
306 &[
307 Easing::Linear,
308 Easing::EaseInQuad, Easing::EaseOutQuad, Easing::EaseInOutQuad,
309 Easing::EaseInCubic, Easing::EaseOutCubic, Easing::EaseInOutCubic,
310 Easing::EaseInQuart, Easing::EaseOutQuart, Easing::EaseInOutQuart,
311 Easing::EaseInQuint, Easing::EaseOutQuint, Easing::EaseInOutQuint,
312 Easing::EaseInSine, Easing::EaseOutSine, Easing::EaseInOutSine,
313 Easing::EaseInExpo, Easing::EaseOutExpo, Easing::EaseInOutExpo,
314 Easing::EaseInCirc, Easing::EaseOutCirc, Easing::EaseInOutCirc,
315 Easing::EaseInBack, Easing::EaseOutBack, Easing::EaseInOutBack,
316 Easing::EaseInElastic, Easing::EaseOutElastic, Easing::EaseInOutElastic,
317 Easing::EaseInBounce, Easing::EaseOutBounce, Easing::EaseInOutBounce,
318 Easing::SmoothStep, Easing::SmootherStep, Easing::Step,
319 ]
321 }
322}
323
324fn bounce_out(t: f32) -> f32 {
326 const N1: f32 = 7.5625;
327 const D1: f32 = 2.75;
328 if t < 1.0 / D1 {
329 N1 * t * t
330 } else if t < 2.0 / D1 {
331 let t2 = t - 1.5 / D1;
332 N1 * t2 * t2 + 0.75
333 } else if t < 2.5 / D1 {
334 let t2 = t - 2.25 / D1;
335 N1 * t2 * t2 + 0.9375
336 } else {
337 let t2 = t - 2.625 / D1;
338 N1 * t2 * t2 + 0.984375
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn linear_endpoints() {
348 assert!((Easing::Linear.apply(0.0)).abs() < 1e-6);
349 assert!((Easing::Linear.apply(1.0) - 1.0).abs() < 1e-6);
350 }
351
352 #[test]
353 fn all_easings_start_at_zero_end_at_one() {
354 let parameterized = [
355 Easing::Power(2.5),
356 Easing::Sigmoid { k: 5.0 },
357 Easing::Spring { stiffness: 100.0, damping: 10.0 },
358 Easing::SmoothStep,
359 Easing::SmootherStep,
360 ];
361 for e in Easing::all_named().iter().chain(parameterized.iter()) {
362 let start = e.apply(0.0);
363 let end = e.apply(1.0);
364 assert!((start).abs() < 1e-4, "{} start={}", e.name(), start);
365 assert!((end - 1.0).abs() < 1e-4, "{} end={}", e.name(), end);
366 }
367 }
368
369 #[test]
370 fn bounce_is_monotonic_at_end() {
371 let prev = Easing::EaseOutBounce.apply(0.99);
372 let curr = Easing::EaseOutBounce.apply(1.00);
373 assert!(curr >= prev - 1e-4);
374 }
375
376 #[test]
377 fn spring_overshoots() {
378 let e = Easing::Spring { stiffness: 200.0, damping: 5.0 };
379 let max = (0..200).map(|i| (e.apply(i as f32 / 100.0) * 1000.0) as i32).max().unwrap();
380 assert!(max > 1000, "spring should overshoot past 1.0 (max={})", max);
381 }
382
383 #[test]
384 fn hermite_through_endpoints() {
385 let e = Easing::Hermite { p0: 0.0, m0: 1.0, p1: 1.0, m1: 1.0 };
386 assert!((e.apply(0.0) - 0.0).abs() < 1e-5);
387 assert!((e.apply(1.0) - 1.0).abs() < 1e-5);
388 }
389
390 #[test]
391 fn parabola_peaks_at_half() {
392 let e = Easing::Parabola;
393 assert!((e.apply(0.5) - 1.0).abs() < 1e-5);
394 assert!(e.apply(0.0).abs() < 1e-5);
395 assert!(e.apply(1.0).abs() < 1e-5);
396 }
397}