1use std::{error::Error, fmt, time::Duration};
2
3#[derive(Clone, Copy, Debug, Default, PartialEq)]
5#[non_exhaustive]
6pub enum Easing {
7 Linear,
9 EaseIn,
11 Custom(CustomEasing),
13 Bezier(Bezier),
15 CircIn,
17 CircOut,
19 CircInOut,
21 BackIn,
23 BackOut,
25 BackInOut,
27 Anticipate,
29 #[default]
31 EaseOut,
32 EaseInOut,
34}
35
36impl Easing {
37 pub(crate) fn sample(self, t: f64) -> (f64, f64) {
38 match self {
39 Self::Linear => (t, 1.0),
40 Self::Custom(curve) => {
41 let result = (curve.sample)(t);
42
43 assert!(
44 result.0.is_finite() && result.1.is_finite(),
45 "custom easing must return finite value/derivative"
46 );
47
48 result
49 }
50 Self::EaseIn => (t * t * t, 3.0 * t * t),
51 Self::Bezier(curve) => curve.sample(t),
52 Self::CircIn => {
53 let root = (1.0 - t * t).max(1e-12).sqrt();
54
55 (1.0 - root, t / root)
56 }
57 Self::CircOut => {
58 let root = (1.0 - (t - 1.0).powi(2)).max(1e-12).sqrt();
59
60 (root, (1.0 - t) / root)
61 }
62 Self::CircInOut => {
63 if t < 0.5 {
64 let (v, d) = Self::CircIn.sample(t * 2.);
65
66 (v * 0.5, d)
67 } else {
68 let (v, d) = Self::CircOut.sample(t * 2. - 1.);
69
70 (0.5 + v * 0.5, d)
71 }
72 }
73 Self::BackIn => {
74 let (v, d) = Self::BackOut.sample(1. - t);
75
76 (1. - v, d)
77 }
78 Self::BackInOut => {
79 if t < 0.5 {
80 let (v, d) = Self::BackIn.sample(t * 2.);
81
82 (v * 0.5, d)
83 } else {
84 let (v, d) = Self::BackOut.sample(t * 2. - 1.);
85
86 (0.5 + v * 0.5, d)
87 }
88 }
89 Self::Anticipate => {
90 if t < 0.5 {
91 let (v, d) = Self::BackIn.sample(t * 2.);
92
93 (v * 0.5, d)
94 } else {
95 let decay = 2_f64.powf(-20. * (t - 0.5));
96
97 (1. - 0.5 * decay, 10. * std::f64::consts::LN_2 * decay)
98 }
99 }
100 Self::BackOut => {
101 let u = t - 1.0;
102
103 (
104 1.0 + 2.70158 * u.powi(3) + 1.70158 * u * u,
105 8.10474 * u * u + 3.40316 * u,
106 )
107 }
108 Self::EaseOut => (1.0 - (1.0 - t).powi(3), 3.0 * (1.0 - t).powi(2)),
109 Self::EaseInOut => (t * t * (3.0 - 2.0 * t), 6.0 * t * (1.0 - t)),
110 }
111 }
112}
113
114#[derive(Clone, Copy, Debug)]
118pub struct CustomEasing {
119 sample: fn(f64) -> (f64, f64),
120 identity: u64,
121}
122
123impl PartialEq for CustomEasing {
124 fn eq(&self, other: &Self) -> bool {
125 self.identity == other.identity
126 }
127}
128
129impl CustomEasing {
130 pub fn new(sample: fn(f64) -> (f64, f64)) -> Result<Self, InvalidEasing> {
134 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
135 let start = sample(0.);
136 let end = sample(1.);
137
138 if start.0 != 0. || end.0 != 1. || ![start.1, end.1].iter().all(|v| v.is_finite()) {
139 return Err(InvalidEasing);
140 }
141
142 Ok(Self {
143 sample,
144 identity: NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
145 })
146 }
147}
148
149#[derive(Clone, Copy, Debug, PartialEq)]
153pub struct Spring {
154 stiffness: f64,
155 damping: f64,
156 mass: f64,
157 rest: Option<[f64; 2]>,
158}
159
160impl Spring {
161 pub fn new(stiffness: f32, damping: f32, mass: f32) -> Result<Self, InvalidSpring> {
163 if [stiffness, damping, mass]
164 .iter()
165 .any(|value| !value.is_finite() || *value <= 0.0)
166 {
167 return Err(InvalidSpring);
168 }
169
170 Ok(Self {
171 stiffness: stiffness.into(),
172 damping: damping.into(),
173 mass: mass.into(),
174 rest: None,
175 })
176 }
177
178 pub fn visual(duration: Duration, bounce: f32) -> Result<Self, InvalidSpring> {
183 if duration.is_zero() || !bounce.is_finite() || !(-1.0..1.0).contains(&bounce) {
184 return Err(InvalidSpring);
185 }
186
187 let ratio = 1.0 - f64::from(bounce);
188
189 let unit = Self {
190 stiffness: 1.0,
191 damping: 2.0 * ratio,
192 mass: 1.0,
193 rest: None,
194 };
195
196 let mut high = 1.0;
197
198 while unit.matrix(high)[0] > 0.1 {
199 high *= 2.0;
200 }
201
202 let mut low = 0.0;
203
204 for _ in 0..64 {
205 let mid = (low + high) * 0.5;
206
207 if unit.matrix(mid)[0] > 0.1 {
208 low = mid;
209 } else {
210 high = mid;
211 }
212 }
213
214 let omega = high / duration.as_secs_f64();
215
216 Ok(Self {
217 stiffness: omega * omega,
218 damping: 2.0 * ratio * omega,
219 mass: 1.0,
220 rest: None,
221 })
222 }
223
224 pub fn rest(mut self, distance: f64, speed: f64) -> Result<Self, InvalidSpring> {
227 if ![distance, speed].iter().all(|v| v.is_finite() && *v > 0.0) {
228 return Err(InvalidSpring);
229 }
230
231 self.rest = Some([distance, speed]);
232
233 Ok(self)
234 }
235
236 pub(crate) fn matrix(self, seconds: f64) -> [f64; 4] {
238 let omega = (self.stiffness / self.mass).sqrt();
239 let decay = self.damping / (2.0 * self.mass);
240 let discriminant = decay * decay - omega * omega;
241
242 let (a, b) = if discriminant.abs() <= omega * omega * 1e-10 {
243 let exponential = (-decay * seconds).exp();
244
245 (exponential, exponential * seconds)
246 } else if discriminant < 0.0 {
247 let frequency = (-discriminant).sqrt();
248 let (sine, cosine) = (frequency * seconds).sin_cos();
249 let exponential = (-decay * seconds).exp();
250
251 (exponential * cosine, exponential * sine / frequency)
252 } else {
253 let root = discriminant.sqrt();
254 let slow = -omega * omega / (decay + root);
256 let fast = -decay - root;
257 let slow_exp = (slow * seconds).exp();
258 let fast_exp = (fast * seconds).exp();
259
260 (
261 (slow_exp + fast_exp) * 0.5,
262 (slow_exp - fast_exp) / (2.0 * root),
263 )
264 };
265
266 [a + decay * b, b, -omega * omega * b, a - decay * b]
267 }
268
269 pub(crate) fn is_settled(
270 self,
271 position: f64,
272 velocity: f64,
273 target: f64,
274 epsilon: f64,
275 ) -> bool {
276 let [distance, speed] = self
277 .rest
278 .unwrap_or([epsilon, epsilon * (self.stiffness / self.mass).sqrt()]);
279
280 (position - target).abs() <= distance && velocity.abs() <= speed
281 }
282}
283
284impl Default for Spring {
285 fn default() -> Self {
286 Self {
287 stiffness: 240.0,
288 damping: 26.0,
289 mass: 1.0,
290 rest: None,
291 }
292 }
293}
294
295#[derive(Clone, Copy, Debug, PartialEq, Eq)]
297pub struct InvalidSpring;
298
299impl fmt::Display for InvalidSpring {
300 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301 f.write_str("invalid spring parameters, timing, bounce, or settling tolerances")
302 }
303}
304
305impl Error for InvalidSpring {}
306
307#[derive(Clone, Copy, Debug, PartialEq)]
309#[non_exhaustive]
310pub enum Transition {
311 Instant,
313 Tween(Duration, Easing),
315 Spring(Spring),
317}
318
319impl Default for Transition {
320 fn default() -> Self {
321 Self::Spring(Spring::default())
322 }
323}
324
325#[derive(Clone, Copy, Debug, PartialEq)]
327pub struct Bezier {
328 points: [f64; 4],
329}
330
331impl Bezier {
332 pub fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Result<Self, InvalidEasing> {
334 if ![x1, y1, x2, y2].iter().all(|v| v.is_finite())
335 || !(0.0..=1.0).contains(&x1)
336 || !(0.0..=1.0).contains(&x2)
337 {
338 return Err(InvalidEasing);
339 }
340
341 Ok(Self {
342 points: [x1, y1, x2, y2],
343 })
344 }
345
346 fn sample(self, x: f64) -> (f64, f64) {
347 let [x1, y1, x2, y2] = self.points;
348
349 let evaluate = |u: f64, a: f64, b: f64| {
350 3.0 * (1.0 - u).powi(2) * u * a + 3.0 * (1.0 - u) * u * u * b + u * u * u
351 };
352
353 let derivative = |u: f64, a: f64, b: f64| {
354 3.0 * (1.0 - u).powi(2) * a + 6.0 * (1.0 - u) * u * (b - a) + 3.0 * u * u * (1.0 - b)
355 };
356
357 let mut low = 0.0;
358 let mut high = 1.0;
359 let mut u = x;
360
361 for _ in 0..32 {
362 let actual = evaluate(u, x1, x2);
363
364 if (actual - x).abs() < 1e-9 {
365 break;
366 }
367
368 if actual < x {
369 low = u;
370 } else {
371 high = u;
372 }
373
374 u = (low + high) * 0.5;
375 }
376
377 (
378 evaluate(u, y1, y2),
379 derivative(u, y1, y2) / derivative(u, x1, x2).max(1e-12),
380 )
381 }
382}
383
384#[derive(Clone, Copy, Debug, PartialEq, Eq)]
386pub struct InvalidEasing;
387
388impl fmt::Display for InvalidEasing {
389 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390 f.write_str("invalid Bezier controls or custom easing endpoints/derivatives")
391 }
392}
393
394impl Error for InvalidEasing {}