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();
219 let zeta = damping / (2.0 * omega).max(f32::EPSILON);
220 if zeta >= 1.0 {
221 1.0 - (1.0 + omega * t) * (-omega * t).exp()
222 } else {
223 let omega_d = omega * (1.0 - zeta * zeta).sqrt();
224 let decay = (-zeta * omega * t).exp();
225 1.0 - decay * ((omega_d * t).cos() +
226 (zeta / (1.0 - zeta * zeta).sqrt()) * (omega_d * t).sin())
227 }
228 }
229
230 Easing::Hermite { p0, m0, p1, m1 } => {
231 let t2 = t * t;
232 let t3 = t2 * t;
233 let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
234 let h10 = t3 - 2.0 * t2 + t;
235 let h01 = -2.0 * t3 + 3.0 * t2;
236 let h11 = t3 - t2;
237 h00 * p0 + h10 * m0 + h01 * p1 + h11 * m1
238 }
239 }
240 }
241
242 pub fn derivative(&self, t: f32) -> f32 {
244 let eps = 1e-4_f32;
245 let hi = self.apply((t + eps).min(1.0));
246 let lo = self.apply((t - eps).max(0.0));
247 (hi - lo) / (2.0 * eps)
248 }
249
250 pub fn name(&self) -> &'static str {
252 match self {
253 Easing::Linear => "Linear",
254 Easing::EaseInQuad => "EaseInQuad",
255 Easing::EaseOutQuad => "EaseOutQuad",
256 Easing::EaseInOutQuad => "EaseInOutQuad",
257 Easing::EaseInCubic => "EaseInCubic",
258 Easing::EaseOutCubic => "EaseOutCubic",
259 Easing::EaseInOutCubic => "EaseInOutCubic",
260 Easing::EaseInQuart => "EaseInQuart",
261 Easing::EaseOutQuart => "EaseOutQuart",
262 Easing::EaseInOutQuart => "EaseInOutQuart",
263 Easing::EaseInQuint => "EaseInQuint",
264 Easing::EaseOutQuint => "EaseOutQuint",
265 Easing::EaseInOutQuint => "EaseInOutQuint",
266 Easing::EaseInSine => "EaseInSine",
267 Easing::EaseOutSine => "EaseOutSine",
268 Easing::EaseInOutSine => "EaseInOutSine",
269 Easing::EaseInExpo => "EaseInExpo",
270 Easing::EaseOutExpo => "EaseOutExpo",
271 Easing::EaseInOutExpo => "EaseInOutExpo",
272 Easing::EaseInCirc => "EaseInCirc",
273 Easing::EaseOutCirc => "EaseOutCirc",
274 Easing::EaseInOutCirc => "EaseInOutCirc",
275 Easing::EaseInBack => "EaseInBack",
276 Easing::EaseOutBack => "EaseOutBack",
277 Easing::EaseInOutBack => "EaseInOutBack",
278 Easing::EaseInElastic => "EaseInElastic",
279 Easing::EaseOutElastic => "EaseOutElastic",
280 Easing::EaseInOutElastic => "EaseInOutElastic",
281 Easing::EaseInBounce => "EaseInBounce",
282 Easing::EaseOutBounce => "EaseOutBounce",
283 Easing::EaseInOutBounce => "EaseInOutBounce",
284 Easing::SmoothStep => "SmoothStep",
285 Easing::SmootherStep => "SmootherStep",
286 Easing::Step => "Step",
287 Easing::EaseOutLinear => "EaseOutLinear",
288 Easing::Parabola => "Parabola",
289 Easing::Flash => "Flash",
290 Easing::Power(_) => "Power",
291 Easing::Sigmoid { .. } => "Sigmoid",
292 Easing::Spring { .. } => "Spring",
293 Easing::Hermite { .. } => "Hermite",
294 }
295 }
296
297 pub fn all_named() -> &'static [Easing] {
299 &[
300 Easing::Linear,
301 Easing::EaseInQuad, Easing::EaseOutQuad, Easing::EaseInOutQuad,
302 Easing::EaseInCubic, Easing::EaseOutCubic, Easing::EaseInOutCubic,
303 Easing::EaseInQuart, Easing::EaseOutQuart, Easing::EaseInOutQuart,
304 Easing::EaseInQuint, Easing::EaseOutQuint, Easing::EaseInOutQuint,
305 Easing::EaseInSine, Easing::EaseOutSine, Easing::EaseInOutSine,
306 Easing::EaseInExpo, Easing::EaseOutExpo, Easing::EaseInOutExpo,
307 Easing::EaseInCirc, Easing::EaseOutCirc, Easing::EaseInOutCirc,
308 Easing::EaseInBack, Easing::EaseOutBack, Easing::EaseInOutBack,
309 Easing::EaseInElastic, Easing::EaseOutElastic, Easing::EaseInOutElastic,
310 Easing::EaseInBounce, Easing::EaseOutBounce, Easing::EaseInOutBounce,
311 Easing::SmoothStep, Easing::SmootherStep, Easing::Step,
312 ]
314 }
315}
316
317fn bounce_out(t: f32) -> f32 {
319 const N1: f32 = 7.5625;
320 const D1: f32 = 2.75;
321 if t < 1.0 / D1 {
322 N1 * t * t
323 } else if t < 2.0 / D1 {
324 let t2 = t - 1.5 / D1;
325 N1 * t2 * t2 + 0.75
326 } else if t < 2.5 / D1 {
327 let t2 = t - 2.25 / D1;
328 N1 * t2 * t2 + 0.9375
329 } else {
330 let t2 = t - 2.625 / D1;
331 N1 * t2 * t2 + 0.984375
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn linear_endpoints() {
341 assert!((Easing::Linear.apply(0.0)).abs() < 1e-6);
342 assert!((Easing::Linear.apply(1.0) - 1.0).abs() < 1e-6);
343 }
344
345 #[test]
346 fn all_easings_start_at_zero_end_at_one() {
347 let parameterized = [
348 Easing::Power(2.5),
349 Easing::Sigmoid { k: 5.0 },
350 Easing::Spring { stiffness: 100.0, damping: 10.0 },
351 Easing::SmoothStep,
352 Easing::SmootherStep,
353 ];
354 for e in Easing::all_named().iter().chain(parameterized.iter()) {
355 let start = e.apply(0.0);
356 let end = e.apply(1.0);
357 assert!((start).abs() < 1e-4, "{} start={}", e.name(), start);
358 assert!((end - 1.0).abs() < 1e-4, "{} end={}", e.name(), end);
359 }
360 }
361
362 #[test]
363 fn bounce_is_monotonic_at_end() {
364 let prev = Easing::EaseOutBounce.apply(0.99);
365 let curr = Easing::EaseOutBounce.apply(1.00);
366 assert!(curr >= prev - 1e-4);
367 }
368
369 #[test]
370 fn spring_overshoots() {
371 let e = Easing::Spring { stiffness: 200.0, damping: 5.0 };
372 let max = (0..200).map(|i| (e.apply(i as f32 / 100.0) * 1000.0) as i32).max().unwrap();
373 assert!(max > 1000, "spring should overshoot past 1.0 (max={})", max);
374 }
375
376 #[test]
377 fn hermite_through_endpoints() {
378 let e = Easing::Hermite { p0: 0.0, m0: 1.0, p1: 1.0, m1: 1.0 };
379 assert!((e.apply(0.0) - 0.0).abs() < 1e-5);
380 assert!((e.apply(1.0) - 1.0).abs() < 1e-5);
381 }
382
383 #[test]
384 fn parabola_peaks_at_half() {
385 let e = Easing::Parabola;
386 assert!((e.apply(0.5) - 1.0).abs() < 1e-5);
387 assert!(e.apply(0.0).abs() < 1e-5);
388 assert!(e.apply(1.0).abs() < 1e-5);
389 }
390}