Skip to main content

sequoia_animation/
lib.rs

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}