1#![warn(clippy::all)]
2#![forbid(unsafe_code)]
3
4mod animation;
5mod animator;
6mod bezier;
7mod builder;
8mod choreography;
9mod import;
10mod machine;
11mod mixer;
12mod spring_machine;
13mod spring_value;
14mod value;
15
16pub use animation::{Animation, AnimationFrame, Interpolation, Keyframe, LoopMode, Track};
17pub use animator::Animator;
18pub use bezier::CubicBezier;
19pub use builder::{
20 Animate, AnimateBuilder, AnimateFrame, SpringBuilder, TweenBuilder, animate, animation,
21 lerp_sample, spring,
22};
23pub use choreography::Choreography;
24pub use import::{ImportError, animation_from_json, bundle_from_json, import_lottie};
25pub use machine::{
26 CmpOp, Condition, FromState, MachineError, MachineInputs, MachineState, StateMachine, ToState,
27 Transition,
28};
29pub use mixer::Mixer;
30use sequoia_core::{Color, Point, Size};
31pub use spring_machine::{AnimValue, SpringMachine, StateDef, TransitionDef, Trigger};
32pub use spring_value::{SpringPoint, SpringValue};
33use std::any::{Any, TypeId};
34use std::collections::HashMap;
35use std::collections::hash_map::DefaultHasher;
36use std::hash::{Hash, Hasher};
37use std::panic::Location;
38use std::sync::{Mutex, OnceLock};
39use std::time::Instant;
40pub use value::Value;
41
42#[derive(Clone, Copy, Debug, PartialEq)]
43#[non_exhaustive]
44pub enum Easing {
45 Linear,
46 EaseIn,
47 EaseOut,
48 EaseInOut,
49 EaseInQuad,
50 EaseOutQuad,
51 EaseInOutQuad,
52 EaseInCubic,
53 EaseOutCubic,
54 EaseInOutCubic,
55 EaseInQuart,
56 EaseOutQuart,
57 EaseInOutQuart,
58 EaseInQuint,
59 EaseOutQuint,
60 EaseInOutQuint,
61 EaseInExpo,
62 EaseOutExpo,
63 EaseInOutExpo,
64 EaseInCirc,
65 EaseOutCirc,
66 EaseInOutCirc,
67 EaseInSine,
68 EaseOutSine,
69 EaseInOutSine,
70 EaseInBack,
71 EaseOutBack,
72 EaseInOutBack,
73 BounceOut,
74 BounceIn,
75 ElasticOut,
76
77 CubicBezier(f32, f32, f32, f32),
78}
79
80impl Easing {
81 pub fn apply(self, t: f32) -> f32 {
82 let t = t.clamp(0.0, 1.0);
83 match self {
84 Easing::Linear => t,
85 Easing::EaseIn => t * t * t,
86 Easing::EaseOut => 1.0 - (1.0 - t).powi(3),
87 Easing::EaseInOut => {
88 if t < 0.5 {
89 4.0 * t * t * t
90 } else {
91 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
92 }
93 }
94 Easing::EaseInQuad => t * t,
95 Easing::EaseOutQuad => t * (2.0 - t),
96 Easing::EaseInOutQuad => {
97 if t < 0.5 {
98 2.0 * t * t
99 } else {
100 -1.0 + (4.0 - 2.0 * t) * t
101 }
102 }
103 Easing::EaseInCubic => t * t * t,
104 Easing::EaseOutCubic => {
105 let t = t - 1.0;
106 t * t * t + 1.0
107 }
108 Easing::EaseInOutCubic => {
109 if t < 0.5 {
110 4.0 * t * t * t
111 } else {
112 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
113 }
114 }
115 Easing::EaseInQuart => t * t * t * t,
116 Easing::EaseOutQuart => {
117 let t = t - 1.0;
118 -(t * t * t * t) + 1.0
119 }
120 Easing::EaseInOutQuart => {
121 if t < 0.5 {
122 8.0 * t * t * t * t
123 } else {
124 1.0 - (-2.0 * t + 2.0).powi(4) / 2.0
125 }
126 }
127 Easing::EaseInQuint => t * t * t * t * t,
128 Easing::EaseOutQuint => {
129 let t = t - 1.0;
130 t * t * t * t * t + 1.0
131 }
132 Easing::EaseInOutQuint => {
133 if t < 0.5 {
134 16.0 * t * t * t * t * t
135 } else {
136 1.0 - (-2.0 * t + 2.0).powi(5) / 2.0
137 }
138 }
139 Easing::EaseInExpo => {
140 if t == 0.0 {
141 0.0
142 } else {
143 (2.0_f32).powf(10.0 * (t - 1.0))
144 }
145 }
146 Easing::EaseOutExpo => {
147 if t == 1.0 {
148 1.0
149 } else {
150 1.0 - (2.0_f32).powf(-10.0 * t)
151 }
152 }
153 Easing::EaseInOutExpo => {
154 if t == 0.0 || t == 1.0 {
155 t
156 } else if t < 0.5 {
157 (2.0_f32).powf(10.0 * (2.0 * t - 1.0)) / 2.0
158 } else {
159 (2.0 - (2.0_f32).powf(-10.0 * (2.0 * t - 1.0))) / 2.0
160 }
161 }
162 Easing::EaseInCirc => 1.0 - (1.0 - t * t).sqrt(),
163 Easing::EaseOutCirc => (1.0 - (t - 1.0) * (t - 1.0)).sqrt(),
164 Easing::EaseInOutCirc => {
165 if t < 0.5 {
166 (1.0 - (1.0 - 4.0 * t * t).sqrt()) / 2.0
167 } else {
168 ((1.0 - (-2.0 * t + 2.0).powi(2)).sqrt() + 1.0) / 2.0
169 }
170 }
171 Easing::EaseInSine => 1.0 - (t * std::f32::consts::FRAC_PI_2).cos(),
172 Easing::EaseOutSine => (t * std::f32::consts::FRAC_PI_2).sin(),
173 Easing::EaseInOutSine => -(std::f32::consts::PI * t).cos() / 2.0 + 0.5,
174 Easing::EaseInBack => {
175 const C1: f32 = 1.70158;
176 const C3: f32 = C1 + 1.0;
177 C3 * t * t * t - C1 * t * t
178 }
179 Easing::EaseOutBack => {
180 const C1: f32 = 1.70158;
181 const C3: f32 = C1 + 1.0;
182 let t = t - 1.0;
183 C3 * t * t * t + C1 * t * t + 1.0
184 }
185 Easing::EaseInOutBack => {
186 const C1: f32 = 1.70158;
187 const C2: f32 = C1 * 1.525;
188 if t < 0.5 {
189 let t = 2.0 * t;
190 (t * t * (C2 + 1.0) * t - C2 * t * t) / 2.0
191 } else {
192 let t = 2.0 * t - 2.0;
193 (t * t * (C2 + 1.0) * t + C2 * t * t + 2.0) / 2.0
194 }
195 }
196 Easing::BounceOut => {
197 if t < 1.0 / 2.75 {
198 7.5625 * t * t
199 } else if t < 2.0 / 2.75 {
200 let t = t - 1.5 / 2.75;
201 7.5625 * t * t + 0.75
202 } else if t < 2.5 / 2.75 {
203 let t = t - 2.25 / 2.75;
204 7.5625 * t * t + 0.9375
205 } else {
206 let t = t - 2.625 / 2.75;
207 7.5625 * t * t + 0.984375
208 }
209 }
210 Easing::BounceIn => 1.0 - Easing::BounceOut.apply(1.0 - t),
211 Easing::CubicBezier(x1, y1, x2, y2) => CubicBezier::new(x1, y1, x2, y2).solve(t),
212 Easing::ElasticOut => {
213 const C4: f32 = (2.0 * std::f32::consts::PI) / 3.0;
214 if t == 0.0 || t == 1.0 {
215 t
216 } else {
217 (2.0_f32).powf(-10.0 * t) * ((t * 10.0 - 0.75) * C4).sin() + 1.0
218 }
219 }
220 }
221 }
222}
223
224pub trait Lerp: Copy {
225 fn lerp(self, other: Self, t: f32) -> Self;
226}
227
228impl Lerp for f32 {
229 fn lerp(self, other: Self, t: f32) -> Self {
230 self + (other - self) * t
231 }
232}
233
234impl Lerp for f64 {
235 fn lerp(self, other: Self, t: f32) -> Self {
236 self + (other - self) * t as f64
237 }
238}
239
240impl Lerp for Color {
241 fn lerp(self, other: Self, t: f32) -> Self {
242 Color::rgba(
243 self.r + (other.r - self.r) * t,
244 self.g + (other.g - self.g) * t,
245 self.b + (other.b - self.b) * t,
246 self.a + (other.a - self.a) * t,
247 )
248 }
249}
250
251impl Lerp for Point {
252 fn lerp(self, other: Self, t: f32) -> Self {
253 Point::new(self.x.lerp(other.x, t), self.y.lerp(other.y, t))
254 }
255}
256
257impl Lerp for Size {
258 fn lerp(self, other: Self, t: f32) -> Self {
259 Size::new(
260 self.width.lerp(other.width, t),
261 self.height.lerp(other.height, t),
262 )
263 }
264}
265
266#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
267pub struct SessionKey(u64, u32, u32);
268
269impl SessionKey {
270 #[track_caller]
271 pub fn caller() -> Self {
272 let loc = Location::caller();
273 let mut h = DefaultHasher::new();
274 loc.file().hash(&mut h);
275 SessionKey(h.finish(), loc.line(), loc.column())
276 }
277}
278
279type SessionCellRegistry = HashMap<(SessionKey, TypeId), Box<dyn Any + Send>>;
280
281static SESSION_CELLS: OnceLock<Mutex<SessionCellRegistry>> = OnceLock::new();
282
283pub fn session_cell<T: Default + Send + 'static>(key: SessionKey) -> &'static Mutex<T> {
284 let map = SESSION_CELLS.get_or_init(|| Mutex::new(HashMap::new()));
285 {
286 let map = map.lock().unwrap_or_else(|p| p.into_inner());
287 if let Some(cell) = map.get(&(key, TypeId::of::<T>())) {
288 return cell
289 .downcast_ref::<&'static Mutex<T>>()
290 .copied()
291 .expect("session cell stored under a different type");
292 }
293 }
294
295 let cell: &'static Mutex<T> = Box::leak(Box::new(Mutex::new(T::default())));
296 let mut map = map.lock().unwrap_or_else(|p| p.into_inner());
297 map.entry((key, TypeId::of::<T>()))
298 .or_insert_with(|| Box::new(cell))
299 .downcast_ref::<&'static Mutex<T>>()
300 .copied()
301 .expect("session cell stored under a different type")
302}
303
304pub fn clear_sessions() {
305 if let Some(map) = SESSION_CELLS.get() {
306 map.lock().unwrap_or_else(|p| p.into_inner()).clear();
307 }
308}
309
310pub const MAX_FRAME_DT: f32 = 1.0 / 20.0;
311
312#[derive(Default)]
313struct Session {
314 started: Option<Instant>,
315
316 last: Option<Instant>,
317
318 elapsed: f64,
319
320 running: bool,
321
322 from: f32,
323 to: f32,
324
325 run_started: f64,
326}
327
328#[derive(Clone)]
329pub struct AnimationController {
330 started: bool,
331
332 last: Option<Instant>,
333
334 elapsed: f64,
335
336 step_elapsed: f64,
337 duration: f64,
338 easing: Easing,
339 pub value: f32,
340 pub running: bool,
341 from: f32,
342 to: f32,
343
344 session: Option<SessionKey>,
345}
346
347impl AnimationController {
348 pub fn new(duration_secs: f64, easing: Easing) -> Self {
349 Self {
350 started: false,
351 last: None,
352 elapsed: 0.0,
353 step_elapsed: 0.0,
354 duration: duration_secs,
355 easing,
356 value: 0.0,
357 running: false,
358 from: 0.0,
359 to: 1.0,
360 session: None,
361 }
362 }
363
364 pub fn with_session(mut self, key: SessionKey) -> Self {
365 self.session = Some(key);
366 self
367 }
368
369 fn anchor(&mut self) {
370 let Some(key) = self.session else {
371 self.last = Some(Instant::now());
372 self.elapsed = 0.0;
373 return;
374 };
375 let mut s = session_cell::<Session>(key)
376 .lock()
377 .unwrap_or_else(|p| p.into_inner());
378 if s.started.is_none() {
379 s.started = Some(Instant::now());
380 s.last = Some(Instant::now());
381 s.elapsed = 0.0;
382 s.from = self.from;
383 s.to = self.to;
384 s.run_started = 0.0;
385 s.running = true;
386 } else if self.from != s.from || self.to != s.to {
387 s.from = self.from;
388 s.to = self.to;
389 s.run_started = s.elapsed;
390 s.running = true;
391 }
392 }
393
394 pub fn start(&mut self) {
395 self.started = true;
396 self.running = true;
397 self.from = 0.0;
398 self.to = 1.0;
399 self.value = 0.0;
400 self.step_elapsed = 0.0;
401 self.anchor();
402 }
403
404 pub fn animate_to(&mut self, target: f32) {
405 let target = target.clamp(0.0, 1.0);
406 if (target - self.value).abs() < 0.001 {
407 self.running = false;
408 self.value = target;
409 return;
410 }
411 self.started = true;
412 self.from = self.value;
413 self.to = target;
414 self.running = true;
415 self.step_elapsed = 0.0;
416 self.anchor();
417 }
418
419 pub fn advance(&mut self) -> bool {
420 if !self.running {
421 return false;
422 }
423 let now = Instant::now();
424
425 let (elapsed, run_started) = if let Some(key) = self.session {
426 let mut s = session_cell::<Session>(key)
427 .lock()
428 .unwrap_or_else(|p| p.into_inner());
429 let dt = now
430 .duration_since(s.last.unwrap_or(now))
431 .as_secs_f64()
432 .min(f64::from(MAX_FRAME_DT));
433 s.elapsed += dt;
434 s.last = Some(now);
435 (s.elapsed, s.run_started)
436 } else {
437 let dt = now
438 .duration_since(self.last.unwrap_or(now))
439 .as_secs_f64()
440 .min(f64::from(MAX_FRAME_DT));
441 self.elapsed += dt;
442 self.last = Some(now);
443 (self.elapsed, 0.0)
444 };
445 let t = ((elapsed - run_started) / self.duration) as f32;
446 if t >= 1.0 {
447 self.value = self.to;
448 self.running = false;
449 if let Some(key) = self.session {
450 session_cell::<Session>(key)
451 .lock()
452 .unwrap_or_else(|p| p.into_inner())
453 .running = false;
454 }
455 true
456 } else {
457 let eased = self.easing.apply(t);
458 self.value = self.from + (self.to - self.from) * eased;
459 false
460 }
461 }
462
463 pub fn reset(&mut self) {
464 self.started = false;
465 self.last = None;
466 self.elapsed = 0.0;
467 self.step_elapsed = 0.0;
468 self.value = 0.0;
469 self.running = false;
470 self.from = 0.0;
471 self.to = 1.0;
472 if let Some(key) = self.session {
473 let mut s = session_cell::<Session>(key)
474 .lock()
475 .unwrap_or_else(|p| p.into_inner());
476 s.started = None;
477 s.last = None;
478 s.elapsed = 0.0;
479 s.running = false;
480 }
481 }
482
483 pub fn advance_by(&mut self, dt: f64) -> bool {
484 if !self.running {
485 return false;
486 }
487 self.step_elapsed += dt.max(0.0);
488 let t = (self.step_elapsed / self.duration) as f32;
489 if t >= 1.0 {
490 self.value = self.to;
491 self.running = false;
492 true
493 } else {
494 self.value = self.from + (self.to - self.from) * self.easing.apply(t);
495 false
496 }
497 }
498
499 pub fn progress(&self) -> f32 {
500 ((self.value - self.from) / (self.to - self.from)).clamp(0.0, 1.0)
501 }
502}
503
504#[derive(Clone)]
505pub struct Tween<T: Lerp> {
506 start: Option<Instant>,
507
508 elapsed: f64,
509 duration: f64,
510 delay: f64,
511 easing: Easing,
512 from: T,
513 to: T,
514 pub value: T,
515 pub running: bool,
516 pub finished: bool,
517}
518
519impl<T: Lerp> Tween<T> {
520 pub fn new(duration_secs: f64, from: T, to: T, easing: Easing) -> Self {
521 Self {
522 start: None,
523 elapsed: 0.0,
524 duration: duration_secs,
525 delay: 0.0,
526 easing,
527 from,
528 to,
529 value: from,
530 running: false,
531 finished: false,
532 }
533 }
534
535 pub fn with_delay(mut self, secs: f64) -> Self {
536 self.delay = secs;
537 self
538 }
539
540 pub fn start(&mut self) {
541 self.start = Some(Instant::now());
542 self.elapsed = 0.0;
543 self.running = true;
544 self.finished = false;
545 self.value = self.from;
546 }
547
548 pub fn advance(&mut self) -> bool {
549 if !self.running || self.finished {
550 return false;
551 }
552 let elapsed = self.start.map(|s| s.elapsed().as_secs_f64()).unwrap_or(0.0);
553 if elapsed < self.delay {
554 return false;
555 }
556 let t = ((elapsed - self.delay) / self.duration) as f32;
557 if t >= 1.0 {
558 self.value = self.to;
559 self.finished = true;
560 self.running = false;
561 true
562 } else {
563 self.value = self.from.lerp(self.to, self.easing.apply(t));
564 false
565 }
566 }
567
568 pub fn advance_by(&mut self, dt: f64) -> bool {
569 if !self.running || self.finished {
570 return false;
571 }
572 self.elapsed += dt.max(0.0);
573 if self.elapsed < self.delay {
574 return false;
575 }
576 let t = ((self.elapsed - self.delay) / self.duration) as f32;
577 if t >= 1.0 {
578 self.value = self.to;
579 self.finished = true;
580 self.running = false;
581 true
582 } else {
583 self.value = self.from.lerp(self.to, self.easing.apply(t));
584 false
585 }
586 }
587
588 pub fn reset(&mut self) {
589 self.start = None;
590 self.elapsed = 0.0;
591 self.value = self.from;
592 self.running = false;
593 self.finished = false;
594 }
595}
596
597pub trait AnimationClip: Send {
598 fn advance(&mut self) -> bool;
599 fn running(&self) -> bool;
600 fn finished(&self) -> bool;
601}
602
603macro_rules! impl_clip_for_tween {
604 ($t:ty) => {
605 impl AnimationClip for Tween<$t> {
606 fn advance(&mut self) -> bool {
607 self.advance()
608 }
609 fn running(&self) -> bool {
610 self.running
611 }
612 fn finished(&self) -> bool {
613 self.finished
614 }
615 }
616 };
617}
618impl_clip_for_tween!(f32);
619impl_clip_for_tween!(f64);
620impl_clip_for_tween!(Color);
621impl_clip_for_tween!(Point);
622impl_clip_for_tween!(Size);
623
624impl AnimationClip for AnimationController {
625 fn advance(&mut self) -> bool {
626 self.advance()
627 }
628 fn running(&self) -> bool {
629 self.running
630 }
631 fn finished(&self) -> bool {
632 !self.running && self.started
633 }
634}
635
636pub struct Parallel {
637 clips: Vec<Box<dyn AnimationClip>>,
638}
639
640impl Parallel {
641 pub fn new(clips: Vec<Box<dyn AnimationClip>>) -> Self {
642 Self { clips }
643 }
644
645 pub fn advance(&mut self) -> bool {
646 let mut any_finished = false;
647 for clip in &mut self.clips {
648 if clip.advance() {
649 any_finished = true;
650 }
651 }
652 any_finished
653 }
654
655 pub fn running(&self) -> bool {
656 self.clips.iter().any(|c| c.running())
657 }
658
659 pub fn finished(&self) -> bool {
660 self.clips.iter().all(|c| c.finished())
661 }
662}
663
664pub struct Sequence {
665 clips: Vec<Box<dyn AnimationClip>>,
666 index: usize,
667}
668
669impl Sequence {
670 pub fn new(clips: Vec<Box<dyn AnimationClip>>) -> Self {
671 Self { clips, index: 0 }
672 }
673
674 pub fn advance(&mut self) -> bool {
675 if self.index >= self.clips.len() {
676 return true;
677 }
678 let finished = self.clips[self.index].advance();
679 if finished && self.clips[self.index].finished() {
680 self.index += 1;
681 }
682 self.index >= self.clips.len()
683 }
684
685 pub fn running(&self) -> bool {
686 self.index < self.clips.len()
687 }
688
689 pub fn finished(&self) -> bool {
690 self.index >= self.clips.len()
691 }
692}
693
694pub struct Timeline<T: Lerp> {
695 keyframes: Vec<(f64, T)>,
696 duration: f64,
697 elapsed: f64,
698 pub value: T,
699 pub finished: bool,
700 easing: Easing,
701}
702
703impl<T: Lerp + Copy> Timeline<T> {
704 pub fn try_new(keyframes: Vec<(f64, T)>, easing: Easing) -> Option<Self> {
705 if keyframes.is_empty() {
706 return None;
707 }
708 if keyframes.iter().any(|&(t, _)| !t.is_finite() || t < 0.0) {
709 return None;
710 }
711 if keyframes.windows(2).any(|w| w[0].0 > w[1].0) {
712 return None;
713 }
714 let duration = keyframes.last().map(|&(t, _)| t)?;
715 let value = keyframes.first().map(|&(_, v)| v)?;
716 Some(Self {
717 keyframes,
718 duration,
719 elapsed: 0.0,
720 value,
721 finished: false,
722 easing,
723 })
724 }
725
726 pub fn advance(&mut self, dt: f64) -> bool {
727 if self.finished {
728 return true;
729 }
730 self.elapsed = (self.elapsed + dt).min(self.duration);
731 self.update_value();
732 if self.elapsed >= self.duration {
733 self.finished = true;
734 }
735 self.finished
736 }
737
738 pub fn reset(&mut self) {
739 self.elapsed = 0.0;
740 self.finished = false;
741 if let Some(&(_, v)) = self.keyframes.first() {
742 self.value = v;
743 }
744 }
745
746 fn update_value(&mut self) {
747 let t = self.elapsed;
748 let Some(&(_, first_value)) = self.keyframes.first() else {
749 return;
750 };
751 if t <= 0.0 {
752 self.value = first_value;
753 return;
754 }
755 let Some(&(last_t, last_value)) = self.keyframes.last() else {
756 return;
757 };
758 if t >= last_t {
759 self.value = last_value;
760 return;
761 }
762
763 for pair in self.keyframes.windows(2) {
764 let (t0, v0) = pair[0];
765 let (t1, v1) = pair[1];
766 if t >= t0 && t <= t1 {
767 let seg_t = ((t - t0) / (t1 - t0)) as f32;
768 self.value = v0.lerp(v1, self.easing.apply(seg_t));
769 return;
770 }
771 }
772 }
773}
774
775#[derive(Clone, Copy, Debug, PartialEq)]
776pub struct Spring {
777 pub damping_ratio: f32,
778
779 pub frequency: f32,
780}
781
782impl Spring {
783 pub const fn new(damping_ratio: f32, frequency: f32) -> Self {
784 Self {
785 damping_ratio,
786 frequency,
787 }
788 }
789
790 pub fn value(&self, t: f32) -> f32 {
791 let t = t.max(0.0);
792 let w0 = std::f32::consts::TAU * self.frequency.max(0.0001);
793 let z = self.damping_ratio;
794 if z < 1.0 {
795 let wd = w0 * (1.0 - z * z).sqrt();
796 let c = z / (1.0 - z * z).sqrt();
797 let decay = (-z * w0 * t).exp();
798 1.0 - decay * ((wd * t).cos() + c * (wd * t).sin())
799 } else if z > 1.0 {
800 let wd = w0 * (z * z - 1.0).sqrt();
801 let c = z / (z * z - 1.0).sqrt();
802 let decay = (-z * w0 * t).exp();
803 1.0 - decay * ((wd * t).cosh() + c * (wd * t).sinh())
804 } else {
805 let decay = (-w0 * t).exp();
806 1.0 - decay * (1.0 + w0 * t)
807 }
808 }
809
810 pub fn settle_time(&self, eps: f32) -> f32 {
811 let w0 = std::f32::consts::TAU * self.frequency.max(0.0001);
812 let z = self.damping_ratio.max(0.0001);
813 let envelope = (-eps.ln() / (z * w0)).max(0.0);
814
815 envelope * 1.5
816 }
817
818 pub const fn snappy() -> Self {
819 Self::new(0.7, 6.0)
820 }
821
822 pub const fn bouncy() -> Self {
823 Self::new(0.45, 4.0)
824 }
825
826 pub const fn gentle() -> Self {
827 Self::new(1.0, 2.5)
828 }
829
830 pub fn from_stiffness_damping(stiffness: f32, damping: f32, mass: f32) -> Self {
831 let m = mass.max(0.0001);
832 let k = stiffness.max(0.0);
833 let c = damping.max(0.0);
834 let omega0 = (k / m).sqrt();
835 let freq = omega0 / std::f32::consts::TAU;
836 let zeta = if k <= 0.0 || m <= 0.0 {
837 1.0
838 } else {
839 c / (2.0 * (k * m).sqrt())
840 };
841 Self::new(zeta, freq.max(0.0001))
842 }
843}
844
845pub trait DurationExt {
846 fn ms(self) -> std::time::Duration;
847 fn s(self) -> std::time::Duration;
848}
849
850macro_rules! impl_duration_ext_int {
851 ($($t:ty),*) => {
852 $(
853 impl DurationExt for $t {
854 fn ms(self) -> std::time::Duration {
855 let v = (self as i64).max(0) as u64;
856 std::time::Duration::from_millis(v)
857 }
858 fn s(self) -> std::time::Duration {
859 let v = (self as f64).max(0.0);
860 std::time::Duration::from_secs_f64(v)
861 }
862 }
863 )*
864 };
865}
866impl_duration_ext_int!(u64, u32, u16, i64, i32, usize);
867
868macro_rules! impl_duration_ext_float {
869 ($($t:ty),*) => {
870 $(
871 impl DurationExt for $t {
872 fn ms(self) -> std::time::Duration {
873 let v = (self as f64 / 1000.0).max(0.0);
874 std::time::Duration::from_secs_f64(v)
875 }
876 fn s(self) -> std::time::Duration {
877 let v = (self as f64).max(0.0);
878 std::time::Duration::from_secs_f64(v)
879 }
880 }
881 )*
882 };
883}
884impl_duration_ext_float!(f32, f64);
885
886pub use Easing::*;
887
888#[cfg(test)]
889mod tests {
890 use super::*;
891
892 #[test]
893 fn timeline_requires_at_least_one_keyframe() {
894 let empty: Vec<(f64, f32)> = Vec::new();
895 assert!(Timeline::try_new(empty, Linear).is_none());
896 }
897
898 #[test]
899 fn timeline_rejects_non_monotonic_times() {
900 assert!(Timeline::try_new(vec![(0.0, 0.0), (0.5, 1.0), (0.2, 2.0)], Linear).is_none());
901 assert!(Timeline::try_new(vec![(1.0, 0.0), (0.5, 1.0)], Linear).is_none());
902 }
903
904 #[test]
905 fn timeline_rejects_negative_or_non_finite_times() {
906 assert!(Timeline::try_new(vec![(-1.0, 0.0), (1.0, 1.0)], Linear).is_none());
907 assert!(
908 Timeline::try_new(vec![(0.0, 0.0), (f64::NAN, 1.0)], Linear).is_none(),
909 "NaN times are rejected"
910 );
911 }
912
913 #[test]
914 fn timeline_accepts_valid_input() {
915 let mut t = Timeline::try_new(vec![(0.0, 0.0f32), (0.5, 50.0f32), (1.0, 100.0f32)], Linear)
916 .expect("valid timeline");
917 assert_eq!(t.value, 0.0);
918 t.advance(0.25);
919 assert!(!t.finished);
920 assert!(t.value > 0.0 && t.value < 50.0);
921 t.advance(1.0);
922 assert!(t.finished);
923 assert_eq!(t.value, 100.0);
924 }
925
926 #[test]
927 fn timeline_single_keyframe_is_stable() {
928 let mut t = Timeline::try_new(vec![(0.0, 7.0f32)], Linear).expect("single keyframe");
929 t.advance(10.0);
930 assert!(t.finished);
931 assert_eq!(t.value, 7.0);
932 t.reset();
933 assert!(!t.finished);
934 assert_eq!(t.value, 7.0);
935 }
936
937 #[test]
938 fn timeline_reset_after_advance_recovers_start() {
939 let mut t = Timeline::try_new(vec![(0.0, 0.0f32), (1.0, 100.0f32)], Linear)
940 .expect("valid timeline");
941 t.advance(1.0);
942 assert!(t.finished);
943 t.reset();
944 assert!(!t.finished);
945 assert_eq!(t.value, 0.0);
946
947 let flat = Timeline::try_new(vec![(0.0, 1.0f32), (0.0, 2.0f32)], Linear)
948 .expect("duplicate times are non-decreasing");
949 assert_eq!(flat.value, 1.0);
950 }
951
952 #[test]
953 fn spring_starts_at_zero_and_settles_at_one() {
954 let s = Spring::new(1.0, 4.0);
955 assert_eq!(s.value(0.0), 0.0);
956 let settled = s.settle_time(0.001);
957 assert!(settled > 0.0);
958 assert!((s.value(settled) - 1.0).abs() < 0.005);
959 assert!((s.value(settled + 1.0) - 1.0).abs() < 0.005);
960 }
961
962 #[test]
963 fn underdamped_spring_overshoots() {
964 let s = Spring::new(0.5, 4.0);
965 let mut peak = 0.0f32;
966 let mut t = 0.0;
967 while t < 2.0 {
968 peak = peak.max(s.value(t));
969 t += 0.001;
970 }
971 assert!(peak > 1.05, "under-damped spring should overshoot: {peak}");
972 assert!(peak < 1.6, "overshoot should stay bounded: {peak}");
973 }
974
975 #[test]
976 fn overdamped_spring_never_overshoots() {
977 let s = Spring::new(2.0, 4.0);
978 let mut peak = 0.0f32;
979 let mut t = 0.0;
980 while t < 2.0 {
981 peak = peak.max(s.value(t));
982 t += 0.001;
983 }
984 assert!(
985 peak <= 1.0 + 1e-4,
986 "over-damped spring must not overshoot: {peak}"
987 );
988 }
989
990 #[test]
991 fn session_continues_animation_across_rebuilds() {
992 let key = SessionKey::caller();
993 let mut first = AnimationController::new(0.01, Linear).with_session(key);
994 first.start();
995 std::thread::sleep(std::time::Duration::from_millis(20));
996 assert!(first.advance(), "20 ms must finish a 10 ms animation");
997
998 let mut rebuilt = AnimationController::new(0.01, Linear).with_session(key);
999 rebuilt.start();
1000 assert!(rebuilt.advance());
1001 assert_eq!(rebuilt.value, 1.0);
1002 assert!(!rebuilt.running);
1003 }
1004
1005 #[test]
1006 fn session_retarget_animates_without_restarting_or_jumping() {
1007 let key = SessionKey::caller();
1008 let mut c = AnimationController::new(0.1, Linear).with_session(key);
1009 c.start();
1010 std::thread::sleep(std::time::Duration::from_millis(30));
1011 c.advance();
1012 let v_before = c.value;
1013 assert!(v_before > 0.2 && v_before < 0.5, "value = {v_before}");
1014
1015 let mut rebuilt = AnimationController::new(0.1, Linear).with_session(key);
1016 rebuilt.value = v_before;
1017 rebuilt.animate_to(0.0);
1018 assert!(rebuilt.running, "re-target must start a new run");
1019 std::thread::sleep(std::time::Duration::from_millis(20));
1020 assert!(!rebuilt.advance());
1021 assert!(
1022 rebuilt.running,
1023 "re-target must animate, not settle instantly"
1024 );
1025 assert!(
1026 rebuilt.value < v_before,
1027 "must head back toward 0 (got {})",
1028 rebuilt.value
1029 );
1030
1031 for _ in 0..8 {
1032 std::thread::sleep(std::time::Duration::from_millis(30));
1033 rebuilt.advance();
1034 }
1035 assert_eq!(rebuilt.value, 0.0);
1036 assert!(!rebuilt.running);
1037 let mut settled = AnimationController::new(0.1, Linear).with_session(key);
1038 settled.animate_to(0.0);
1039 assert!(!settled.running, "stray redraw must not replay the run");
1040 }
1041
1042 #[test]
1043 fn session_reset_reanchors_the_clock() {
1044 let key = SessionKey::caller();
1045 let mut first = AnimationController::new(0.01, Linear).with_session(key);
1046 first.start();
1047 std::thread::sleep(std::time::Duration::from_millis(20));
1048 first.advance();
1049 first.reset();
1050
1051 let mut fresh = AnimationController::new(10.0, Linear).with_session(key);
1052 fresh.start();
1053 assert!(!fresh.advance());
1054 assert!(fresh.running);
1055 assert!(fresh.value < 0.5);
1056 }
1057
1058 #[test]
1059 fn sessions_are_isolated_per_declaration_site() {
1060 let key_a = SessionKey::caller();
1061 let key_b = SessionKey::caller();
1062 let mut a = AnimationController::new(0.01, Linear).with_session(key_a);
1063 a.start();
1064 std::thread::sleep(std::time::Duration::from_millis(20));
1065 assert!(a.advance());
1066
1067 let mut b = AnimationController::new(10.0, Linear).with_session(key_b);
1068 b.start();
1069 assert!(!b.advance());
1070 assert!(b.running);
1071 }
1072
1073 #[test]
1074 fn controllers_without_session_keep_legacy_behavior() {
1075 let mut c = AnimationController::new(10.0, Linear);
1076 c.start();
1077 assert!(!c.advance());
1078 assert!(c.running);
1079 assert!(c.value < 0.001, "animation should barely have started");
1080 }
1081
1082 #[test]
1083 fn stalled_frame_does_not_teleport_session_animation() {
1084 let key = SessionKey::caller();
1085 let mut first = AnimationController::new(1.0, Linear).with_session(key);
1086 first.start();
1087
1088 std::thread::sleep(std::time::Duration::from_millis(300));
1089
1090 let mut rebuilt = AnimationController::new(1.0, Linear).with_session(key);
1091 rebuilt.start();
1092 assert!(
1093 !rebuilt.advance(),
1094 "a stalled frame must not finish the animation"
1095 );
1096 assert!(rebuilt.running);
1097 assert!(
1098 rebuilt.value < 0.5,
1099 "stalled frame advanced too far: {}",
1100 rebuilt.value
1101 );
1102 }
1103}