1use crate::{Easing, SpringConfig};
4
5pub type EasingFn = fn(f64) -> f64;
7
8impl Easing {
9 pub fn evaluate(&self, t: f64) -> f64 {
11 let t = t.clamp(0.0, 1.0);
12
13 match self {
14 Easing::Linear => linear(t),
15 Easing::EaseIn => ease_in_quad(t),
16 Easing::EaseOut => ease_out_quad(t),
17 Easing::EaseInOut => ease_in_out_quad(t),
18 Easing::CircIn => circ_in(t),
19 Easing::CircOut => circ_out(t),
20 Easing::CircInOut => circ_in_out(t),
21 Easing::BackIn => back_in(t),
22 Easing::BackOut => back_out(t),
23 Easing::BackInOut => back_in_out(t),
24 Easing::Spring(config) => {
25 let factor = (config.stiffness / 100.0).clamp(0.5, 2.0);
28 ease_out_expo(t).powf(1.0 / factor)
29 }
30 Easing::Bezier(x1, y1, x2, y2) => cubic_bezier(*x1, *y1, *x2, *y2, t),
31 Easing::CubicBezier(cb) => cubic_bezier(cb.0, cb.1, cb.2, cb.3, t),
32 }
33 }
34
35 pub fn as_fn(&self) -> Box<dyn Fn(f64) -> f64> {
37 match self {
38 Easing::Linear => Box::new(linear),
39 Easing::EaseIn => Box::new(ease_in_quad),
40 Easing::EaseOut => Box::new(ease_out_quad),
41 Easing::EaseInOut => Box::new(ease_in_out_quad),
42 Easing::CircIn => Box::new(circ_in),
43 Easing::CircOut => Box::new(circ_out),
44 Easing::CircInOut => Box::new(circ_in_out),
45 Easing::BackIn => Box::new(back_in),
46 Easing::BackOut => Box::new(back_out),
47 Easing::BackInOut => Box::new(back_in_out),
48 Easing::Spring(config) => {
49 let factor = (config.stiffness / 100.0).clamp(0.5, 2.0);
50 Box::new(move |t| ease_out_expo(t).powf(1.0 / factor))
51 }
52 Easing::Bezier(x1, y1, x2, y2) => {
53 let x1 = *x1;
54 let y1 = *y1;
55 let x2 = *x2;
56 let y2 = *y2;
57 Box::new(move |t| cubic_bezier(x1, y1, x2, y2, t))
58 }
59 Easing::CubicBezier(cb) => {
60 let x1 = cb.0;
61 let y1 = cb.1;
62 let x2 = cb.2;
63 let y2 = cb.3;
64 Box::new(move |t| cubic_bezier(x1, y1, x2, y2, t))
65 }
66 }
67 }
68}
69
70pub fn linear(t: f64) -> f64 {
74 t
75}
76
77pub fn ease_in_quad(t: f64) -> f64 {
79 t * t
80}
81
82pub fn ease_out_quad(t: f64) -> f64 {
84 t * (2.0 - t)
85}
86
87pub fn ease_in_out_quad(t: f64) -> f64 {
89 if t < 0.5 {
90 2.0 * t * t
91 } else {
92 -1.0 + (4.0 - 2.0 * t) * t
93 }
94}
95
96pub fn ease_in_cubic(t: f64) -> f64 {
98 t * t * t
99}
100
101pub fn ease_out_cubic(t: f64) -> f64 {
103 let t = t - 1.0;
104 t * t * t + 1.0
105}
106
107pub fn ease_in_out_cubic(t: f64) -> f64 {
109 if t < 0.5 {
110 4.0 * t * t * t
111 } else {
112 let t = 2.0 * t - 2.0;
113 1.0 + t * t * t / 2.0
114 }
115}
116
117pub fn ease_out_expo(t: f64) -> f64 {
119 if t == 1.0 {
120 1.0
121 } else {
122 1.0 - 2.0_f64.powf(-10.0 * t)
123 }
124}
125
126pub fn circ_in(t: f64) -> f64 {
130 1.0 - (1.0 - t * t).sqrt()
131}
132
133pub fn circ_out(t: f64) -> f64 {
135 let t = t - 1.0;
136 (1.0 - t * t).sqrt()
137}
138
139pub fn circ_in_out(t: f64) -> f64 {
141 if t < 0.5 {
142 (1.0 - (1.0 - 4.0 * t * t).sqrt()) / 2.0
143 } else {
144 let t = -2.0 * t + 2.0;
145 ((1.0 - t * t).sqrt() + 1.0) / 2.0
146 }
147}
148
149const BACK_CONSTANT: f64 = 1.70158;
152
153pub fn back_in(t: f64) -> f64 {
155 let c1 = BACK_CONSTANT;
156 let c3 = c1 + 1.0;
157 c3 * t * t * t - c1 * t * t
158}
159
160pub fn back_out(t: f64) -> f64 {
162 let c1 = BACK_CONSTANT;
163 let c3 = c1 + 1.0;
164 let t = t - 1.0;
165 1.0 + c3 * t * t * t + c1 * t * t
166}
167
168pub fn back_in_out(t: f64) -> f64 {
170 let c1 = BACK_CONSTANT;
171 let c2 = c1 * 1.525;
172
173 if t < 0.5 {
174 let t = 2.0 * t;
175 (t * t * ((c2 + 1.0) * t - c2)) / 2.0
176 } else {
177 let t = 2.0 * t - 2.0;
178 (t * t * ((c2 + 1.0) * t + c2) + 2.0) / 2.0
179 }
180}
181
182pub fn cubic_bezier(_x1: f64, y1: f64, _x2: f64, y2: f64, t: f64) -> f64 {
184 let t2 = t * t;
187 let t3 = t2 * t;
188 let mt = 1.0 - t;
189 let mt2 = mt * mt;
190 let _mt3 = mt2 * mt;
191
192 3.0 * mt2 * t * y1 + 3.0 * mt * t2 * y2 + t3
195}
196
197pub mod presets {
199 use super::*;
200
201 pub const EASE: Easing = Easing::Bezier(0.4, 0.0, 0.2, 1.0);
203
204 pub const EASE_IN: Easing = Easing::Bezier(0.4, 0.0, 1.0, 1.0);
206
207 pub const EASE_OUT: Easing = Easing::Bezier(0.0, 0.0, 0.2, 1.0);
209
210 pub const EASE_IN_OUT: Easing = Easing::Bezier(0.4, 0.0, 0.2, 1.0);
212
213 pub const SPRING_SMOOTH: Easing = Easing::Spring(SpringConfig {
215 stiffness: 100.0,
216 damping: 20.0,
217 mass: 1.0,
218 velocity: 0.0,
219 rest_delta: 0.01,
220 rest_speed: 0.01,
221 });
222
223 pub const SPRING_BOUNCY: Easing = Easing::Spring(SpringConfig {
225 stiffness: 200.0,
226 damping: 10.0,
227 mass: 1.0,
228 velocity: 0.0,
229 rest_delta: 0.01,
230 rest_speed: 0.01,
231 });
232
233 pub const SPRING_GENTLE: Easing = Easing::Spring(SpringConfig {
235 stiffness: 50.0,
236 damping: 15.0,
237 mass: 1.0,
238 velocity: 0.0,
239 rest_delta: 0.01,
240 rest_speed: 0.01,
241 });
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 #[cfg(feature = "approx")]
248 use approx::assert_relative_eq;
249
250 #[test]
251 fn test_linear_easing() {
252 assert_eq!(linear(0.0), 0.0);
253 assert_eq!(linear(0.5), 0.5);
254 assert_eq!(linear(1.0), 1.0);
255 }
256
257 #[cfg(feature = "approx")]
258 #[test]
259 fn test_ease_bounds() {
260 let easings = vec![
261 ease_in_quad,
262 ease_out_quad,
263 ease_in_out_quad,
264 ease_in_cubic,
265 ease_out_cubic,
266 ease_in_out_cubic,
267 circ_in,
268 circ_out,
269 circ_in_out,
270 ];
271
272 for easing in easings {
273 assert_relative_eq!(easing(0.0), 0.0, epsilon = 1e-10);
275 assert_relative_eq!(easing(1.0), 1.0, epsilon = 1e-10);
276
277 let mut prev = easing(0.0);
279 for i in 1..=10 {
280 let t = i as f64 / 10.0;
281 let current = easing(t);
282 assert!(current >= prev - 1e-10, "Easing function not monotonic");
283 prev = current;
284 }
285 }
286 }
287
288 #[test]
289 fn test_back_easing_overshoot() {
290 let mut max: f64 = 0.0;
292 for i in 0..=100 {
293 let t = i as f64 / 100.0;
294 let value = back_out(t);
295 max = max.max(value);
296 }
297 assert!(max > 1.0, "Back easing should overshoot");
298 }
299
300 #[cfg(feature = "approx")]
301 #[test]
302 fn test_cubic_bezier() {
303 let ease = |t| cubic_bezier(0.25, 0.1, 0.25, 1.0, t);
305 assert_relative_eq!(ease(0.0), 0.0, epsilon = 1e-3);
306 assert_relative_eq!(ease(1.0), 1.0, epsilon = 1e-3);
307 }
308
309 #[test]
310 fn test_easing_evaluate() {
311 let easing = Easing::EaseInOut;
312 assert_eq!(easing.evaluate(0.0), 0.0);
313 assert_eq!(easing.evaluate(1.0), 1.0);
314
315 assert_eq!(easing.evaluate(-0.1), 0.0);
317 assert_eq!(easing.evaluate(1.1), 1.0);
318 }
319}