Skip to main content

mirage_engine/animation/
animator.rs

1use core::fmt::{self, Debug, Formatter};
2use core::marker::PhantomData;
3use core::time::Duration;
4
5use crate::animation::posed::{Fading, Posed};
6use crate::animation::{AnimationStates, Running, Timed, Transition};
7use crate::mesh::Animation;
8
9/// The weight a fade has run out at.
10const WHOLE: f32 = 1.0;
11
12/// Times the machine halves a tick's own span to reach the instant inside
13/// it a transition started at, which leaves that instant known to within
14/// `0.0001` of the span.
15const CROSSINGS: u32 = 14;
16
17/// The machine a game poses a draw with: the state it is in, what that
18/// state plays, and the fade out of the state it left.
19///
20/// Typed by the mesh it poses, so a draw of another mesh does not take it.
21/// [`ctx.animate`](crate::TickContext::animate) runs one and
22/// [`Instance::posed`](crate::mesh::Instance::posed) draws in the pose it
23/// holds; no clip length and no time of the game's own crosses either call.
24pub struct Animator<M, S: AnimationStates> {
25    state: S,
26    motion: Timed,
27    fading: Option<Fading>,
28    changed: Option<Change<S>>,
29    holding: Holding,
30    /// The last instant `animate` ran it at, which a clock that goes back
31    /// is held to.
32    seen: Duration,
33    mesh: PhantomData<fn() -> M>,
34}
35
36impl<M, S: AnimationStates> Animator<M, S> {
37    /// A machine in its entry state, at the start of what that state plays
38    /// reading the default input.
39    pub fn new() -> Self {
40        let state = S::entry();
41        let motion = Timed::new(&state.motion(&S::Input::default()), None, 0.0);
42
43        Self {
44            state,
45            motion,
46            fading: None,
47            changed: None,
48            holding: Holding::Running,
49            seen: Duration::ZERO,
50            mesh: PhantomData,
51        }
52    }
53
54    /// The state it is in, which is the state being faded into while a fade
55    /// runs.
56    pub fn state(&self) -> S {
57        self.state
58    }
59
60    /// Whether a transition into `state` started the last time `animate`
61    /// ran it.
62    pub fn entered(&self, state: S) -> bool {
63        self.changed.is_some_and(|changed| changed.entered == state)
64    }
65
66    /// Whether a transition out of `state` started the last time `animate`
67    /// ran it.
68    pub fn left(&self, state: S) -> bool {
69        self.changed.is_some_and(|changed| changed.left == state)
70    }
71
72    /// Whether a fade out of the state it left was still running the last
73    /// time `animate` ran it.
74    pub fn transitioning(&self) -> bool {
75        self.fading.is_some()
76    }
77
78    /// Holds what it plays where it is, until [`resume`](Self::resume).
79    ///
80    /// Required if you want a paused game to leave every mesh in the pose
81    /// it is drawn in: the span between this call and the resume counts for
82    /// nothing, so each motion runs on from where it was held.
83    pub fn hold(&mut self) {
84        self.holding = Holding::Held;
85    }
86
87    /// Plays on from where [`hold`](Self::hold) left it.
88    pub fn resume(&mut self) {
89        if self.holding == Holding::Held {
90            self.holding = Holding::Resumed;
91        }
92    }
93
94    /// Runs the machine up to `now`: where the state it is in goes from
95    /// there, and what the state it lands in plays.
96    pub(crate) fn animate(&mut self, input: &S::Input, now: Duration, clips: &[Animation]) {
97        self.changed = None;
98        let now = now.max(self.seen);
99        let held = now.saturating_sub(self.seen);
100        let mut since = self.seen;
101        self.seen = now;
102        if self.holding != Holding::Running {
103            self.shift(held);
104            if self.holding == Holding::Held {
105                return;
106            }
107            self.holding = Holding::Running;
108            // The hold moved every start by the span it covered, so no
109            // progress lies between the tick before this one and this one.
110            since = now;
111        }
112        self.settle(now);
113        self.motion = self.motion.started_at(now);
114
115        let at = self.motion.progress(now, clips);
116        if let Some(transition) = self.state.next(input, at) {
117            let crossing = self.crossing(transition.into, input, since, now, clips);
118            self.enter(transition, crossing, input, clips);
119        }
120        let motion = self.state.motion(input);
121        self.motion = self.motion.kept(&motion, now, clips);
122    }
123
124    /// What it is running, for the draw that poses a mesh by it, read at
125    /// the instant it was held at while a hold stands.
126    pub(crate) fn running(&self) -> Running {
127        Running {
128            posed: Posed::Playing(self.motion),
129            fading: self.fading,
130            held: self.holding.stands().then_some(self.seen),
131        }
132    }
133
134    /// Moves into what `transition` states, fading out of what it plays now
135    /// from `crossing`.
136    ///
137    /// A fade already running is interrupted, which stops what it drew at
138    /// that instant; one that takes no time at all drops what it
139    /// leaves.
140    fn enter(
141        &mut self,
142        transition: Transition<S>,
143        crossing: Duration,
144        input: &S::Input,
145        clips: &[Animation],
146    ) {
147        if transition.into == self.state && !transition.restarted {
148            return;
149        }
150        self.fading = match transition.fade.is_zero() {
151            true => None,
152            false => Some(Fading {
153                under: match self.fading {
154                    Some(_) => Posed::Stopped(self.running().posing(crossing, clips)),
155                    None => Posed::Playing(self.motion),
156                },
157                from: crossing,
158                over: transition.fade,
159            }),
160        };
161        self.motion = Timed::new(
162            &transition.into.motion(input),
163            Some(crossing),
164            transition.entering_at,
165        );
166        self.changed = Some(Change {
167            left: self.state,
168            entered: transition.into,
169        });
170        self.state = transition.into;
171    }
172
173    /// The first instant between `since` and `now` at which how far the
174    /// motion has run moves the state it is in into `into`, to within
175    /// `0.0001` of that span.
176    ///
177    /// A tick reads the state once, and what moved it — a fraction of a
178    /// cycle passed — crosses inside the tick. The state itself is the only
179    /// thing that states where, so the machine reads it again over the
180    /// span, at the progress each instant in it holds and at this tick's
181    /// input, halving the span until the two ends of it read the same. The
182    /// fade then starts at that crossing rather than at the tick, so a clip
183    /// shorter than a tick fades from the first crossing inside it and no
184    /// fade is paced by the frame rate. Where the state already moves at
185    /// the start of the span, the input and not the progress moved it, and
186    /// the transition starts at the tick that read it. The state is taken
187    /// to move once across one tick.
188    fn crossing(
189        &self,
190        into: S,
191        input: &S::Input,
192        since: Duration,
193        now: Duration,
194        clips: &[Animation],
195    ) -> Duration {
196        let moves = |at: Duration| {
197            self.state
198                .next(input, self.motion.progress(at, clips))
199                .is_some_and(|transition| transition.into == into)
200        };
201        if since >= now || moves(since) {
202            return now;
203        }
204
205        let mut before = since;
206        let mut after = now;
207        for _ in 0..CROSSINGS {
208            let between = before + (after - before) / 2;
209            match moves(between) {
210                true => after = between,
211                false => before = between,
212            }
213        }
214
215        after
216    }
217
218    /// Drops a fade that has run out by `now`.
219    fn settle(&mut self, now: Duration) {
220        if self
221            .fading
222            .is_some_and(|fading| fading.weight(now) >= WHOLE)
223        {
224            self.fading = None;
225        }
226    }
227
228    /// Moves every instant it holds `span` later, which leaves what it
229    /// plays where it is.
230    fn shift(&mut self, span: Duration) {
231        self.motion = self.motion.shifted(span);
232        self.fading = self.fading.map(|fading| fading.shifted(span));
233    }
234}
235
236impl<M, S: AnimationStates> Clone for Animator<M, S> {
237    fn clone(&self) -> Self {
238        Self {
239            state: self.state,
240            motion: self.motion,
241            fading: self.fading,
242            changed: self.changed,
243            holding: self.holding,
244            seen: self.seen,
245            mesh: PhantomData,
246        }
247    }
248}
249
250impl<M, S: AnimationStates> Debug for Animator<M, S> {
251    /// The state it is in and whether it is still fading out of the one it
252    /// left.
253    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
254        formatter
255            .debug_struct("Animator")
256            .field("state", &self.state)
257            .field("transitioning", &self.transitioning())
258            .finish()
259    }
260}
261
262impl<M, S: AnimationStates> Default for Animator<M, S> {
263    /// A machine in its entry state; see [`Animator::new`].
264    fn default() -> Self {
265        Self::new()
266    }
267}
268
269/// Whether a machine's clock runs: on, held where it is, or running again
270/// from where it was held.
271#[derive(Clone, Copy, Debug, Eq, PartialEq)]
272enum Holding {
273    Running,
274    Held,
275    Resumed,
276}
277
278impl Holding {
279    /// Whether the machine stands at the last instant it read: held, or
280    /// resumed and not yet run on from there.
281    fn stands(self) -> bool {
282        self != Self::Running
283    }
284}
285
286/// The transition the last run of `animate` started: the state it left and
287/// the state it entered.
288#[derive(Clone, Copy, Debug)]
289struct Change<S> {
290    left: S,
291    entered: S,
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::animation::{Motion, Progress};
298    use crate::math::Vec3;
299    use crate::mesh::{Clip, Keys, Moves, Posing, Track};
300
301    /// The fade every transition below takes.
302    const FADE: Duration = Duration::from_millis(200);
303
304    /// A tick of a sixtieth of a second, which no clip below runs for a
305    /// whole number of.
306    const TICK: Duration = Duration::from_nanos(16_666_667);
307
308    /// How long `Step::Long` runs for, in seconds: a length no tick lands
309    /// on.
310    const LONG: f32 = 0.458_333_34;
311
312    /// How long each clip of `Step` runs for, in seconds.
313    const SPANS: [f32; 4] = [1.0, 0.25, LONG, 0.01];
314
315    /// The mesh every machine below poses, which no test of them draws.
316    struct Puppet;
317
318    /// The clips every machine below plays.
319    #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
320    enum Step {
321        Slow,
322        Quick,
323        Long,
324        Brief,
325    }
326
327    impl Clip for Step {
328        fn from_name(_name: &str) -> Option<Self> {
329            None
330        }
331
332        fn all() -> Vec<Self> {
333            vec![Self::Slow, Self::Quick, Self::Long, Self::Brief]
334        }
335
336        fn index(&self) -> u32 {
337            *self as u32
338        }
339    }
340
341    /// What those clips hold: one joint moved out along `X` over the clip's
342    /// own span, so the time a pose is read at is what a test reads back.
343    fn clips() -> Vec<Animation> {
344        SPANS
345            .iter()
346            .map(|&span| {
347                Animation::new(vec![Track {
348                    joint: 0,
349                    moves: Moves::Position(Keys::Linear(vec![(0.0, Vec3::ZERO), (span, Vec3::X)])),
350                }])
351            })
352            .collect()
353    }
354
355    /// What every machine below reads: where the game moves it, and the
356    /// values two of them are posed by.
357    #[derive(Clone, Copy, Debug)]
358    struct Told {
359        go: bool,
360        restart: bool,
361        entering_at: f32,
362        step: u32,
363        weight: f32,
364        pace: f32,
365        openness: f32,
366    }
367
368    impl Default for Told {
369        /// Moves nothing, at the pace every clip states.
370        fn default() -> Self {
371            Self {
372                go: false,
373                restart: false,
374                entering_at: 0.0,
375                step: 0,
376                weight: 0.0,
377                pace: 1.0,
378                openness: 0.0,
379            }
380        }
381    }
382
383    /// What moves a machine on.
384    fn go() -> Told {
385        Told {
386            go: true,
387            ..Told::default()
388        }
389    }
390
391    /// What moves a machine into what it already plays, entering `at` of
392    /// the way in.
393    fn restart(at: f32) -> Told {
394        Told {
395            restart: true,
396            entering_at: at,
397            ..Told::default()
398        }
399    }
400
401    /// A blend at `weight`, paced by `pace`.
402    fn mix(weight: f32, pace: f32) -> Told {
403        Told {
404            weight,
405            pace,
406            ..Told::default()
407        }
408    }
409
410    /// A value that scrubs a clip.
411    fn open(openness: f32) -> Told {
412        Told {
413            openness,
414            ..Told::default()
415        }
416    }
417
418    /// A machine that walks a clip once where the game moves it on, and
419    /// then holds a pose the game states.
420    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
421    enum Walk {
422        Still,
423        Going,
424        Done,
425    }
426
427    impl AnimationStates for Walk {
428        type Clip = Step;
429        type Input = Told;
430
431        fn entry() -> Self {
432            Self::Still
433        }
434
435        fn motion(&self, input: &Told) -> Motion<Step> {
436            match self {
437                Self::Still => Motion::looping(Step::Slow),
438                Self::Going => Motion::once(Step::Long),
439                Self::Done => Motion::scrubbed(Step::Slow, input.openness),
440            }
441        }
442
443        fn next(&self, input: &Told, at: Progress) -> Option<Transition<Self>> {
444            match self {
445                Self::Still if input.go => Some(Self::Going.fade(FADE)),
446                Self::Going if at.ended() => Some(Self::Done.fade(FADE)),
447                _ => None,
448            }
449        }
450    }
451
452    /// A machine that moves on part way through a loop of its own.
453    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
454    enum Round {
455        Turning,
456        Past,
457    }
458
459    impl AnimationStates for Round {
460        type Clip = Step;
461        type Input = Told;
462
463        fn entry() -> Self {
464            Self::Turning
465        }
466
467        fn motion(&self, _input: &Told) -> Motion<Step> {
468            match self {
469                Self::Turning => Motion::looping(Step::Brief),
470                Self::Past => Motion::looping(Step::Slow),
471            }
472        }
473
474        fn next(&self, _input: &Told, at: Progress) -> Option<Transition<Self>> {
475            match self {
476                Self::Turning if at.past(0.5) => Some(Self::Past.fade(FADE)),
477                _ => None,
478            }
479        }
480    }
481
482    /// A machine that moves to whatever state the game states, so that a
483    /// fade of its own can be interrupted.
484    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
485    enum Steps {
486        First,
487        Second,
488        Third,
489    }
490
491    impl AnimationStates for Steps {
492        type Clip = Step;
493        type Input = Told;
494
495        fn entry() -> Self {
496            Self::First
497        }
498
499        fn motion(&self, _input: &Told) -> Motion<Step> {
500            match self {
501                Self::First => Motion::looping(Step::Slow),
502                Self::Second => Motion::looping(Step::Quick),
503                Self::Third => Motion::looping(Step::Long),
504            }
505        }
506
507        fn next(&self, input: &Told, _at: Progress) -> Option<Transition<Self>> {
508            let wanted = match input.step {
509                0 => Self::First,
510                1 => Self::Second,
511                _ => Self::Third,
512            };
513
514            (wanted != *self).then(|| wanted.fade(FADE))
515        }
516    }
517
518    /// A machine of one state, which the game moves to that same state.
519    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
520    enum Only {
521        It,
522    }
523
524    impl AnimationStates for Only {
525        type Clip = Step;
526        type Input = Told;
527
528        fn entry() -> Self {
529            Self::It
530        }
531
532        fn motion(&self, _input: &Told) -> Motion<Step> {
533            Motion::looping(Step::Slow)
534        }
535
536        fn next(&self, input: &Told, _at: Progress) -> Option<Transition<Self>> {
537            match (input.restart, input.go) {
538                (true, _) => Some(Self::It.restarted().entering_at(input.entering_at)),
539                (_, true) => Some(Self::It.fade(FADE)),
540                _ => None,
541            }
542        }
543    }
544
545    /// A machine that fades from one clip into a pair of them blended.
546    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
547    enum Into {
548        One,
549        Pair,
550    }
551
552    impl AnimationStates for Into {
553        type Clip = Step;
554        type Input = Told;
555
556        fn entry() -> Self {
557            Self::One
558        }
559
560        fn motion(&self, _input: &Told) -> Motion<Step> {
561            match self {
562                Self::One => Motion::looping(Step::Slow),
563                Self::Pair => Motion::blend(Step::Quick, Step::Long, 0.5),
564            }
565        }
566
567        fn next(&self, input: &Told, _at: Progress) -> Option<Transition<Self>> {
568            match input.go {
569                true => Some(Self::Pair.fade(FADE)),
570                false => None,
571            }
572        }
573    }
574
575    /// A machine of one state, posed by a value of the game's own.
576    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
577    enum Opening {
578        It,
579    }
580
581    impl AnimationStates for Opening {
582        type Clip = Step;
583        type Input = Told;
584
585        fn entry() -> Self {
586            Self::It
587        }
588
589        fn motion(&self, input: &Told) -> Motion<Step> {
590            Motion::scrubbed(Step::Slow, input.openness)
591        }
592
593        fn next(&self, _input: &Told, _at: Progress) -> Option<Transition<Self>> {
594            None
595        }
596    }
597
598    /// A machine of one state, blending two clips at a weight and a pace the
599    /// game states.
600    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
601    enum Mixing {
602        It,
603    }
604
605    impl AnimationStates for Mixing {
606        type Clip = Step;
607        type Input = Told;
608
609        fn entry() -> Self {
610            Self::It
611        }
612
613        fn motion(&self, input: &Told) -> Motion<Step> {
614            Motion::blend(Step::Slow, Step::Quick, input.weight).paced(input.pace)
615        }
616
617        fn next(&self, _input: &Told, _at: Progress) -> Option<Transition<Self>> {
618            None
619        }
620    }
621
622    /// A machine of `S` over the test clips.
623    fn machine<S: AnimationStates<Input = Told>>() -> Animator<Puppet, S> {
624        Animator::new()
625    }
626
627    /// Runs `animator` up to `at` seconds on the run clock.
628    fn animate<S: AnimationStates<Input = Told>>(
629        animator: &mut Animator<Puppet, S>,
630        input: Told,
631        at: f32,
632    ) {
633        animator.animate(&input, seconds(at), &clips());
634    }
635
636    /// The pose `animator` holds `at` seconds on the run clock.
637    fn posing<S: AnimationStates<Input = Told>>(animator: &Animator<Puppet, S>, at: f32) -> Posing {
638        animator.running().posing(seconds(at), &clips())
639    }
640
641    /// `at` seconds on the run clock.
642    fn seconds(at: f32) -> Duration {
643        Duration::from_secs_f32(at)
644    }
645
646    /// The time along its own clip the pose starts at.
647    fn started(posing: Posing) -> f32 {
648        posing.from.at
649    }
650
651    /// How far of the way to the clip blended over that one the pose lies,
652    /// and nothing where none is blended over it.
653    fn weight(posing: Posing) -> Option<f32> {
654        posing.over[0].map(|faded| faded.weight)
655    }
656
657    /// The time along its own clip the clip blended over the first is read
658    /// at.
659    fn blended(posing: Posing) -> Option<f32> {
660        posing.over[0].map(|faded| faded.sampled.at)
661    }
662
663    #[test]
664    fn a_machine_starts_in_its_entry_state_at_the_start_of_what_that_state_plays() {
665        let animator = machine::<Walk>();
666
667        assert_eq!(animator.state(), Walk::Still);
668        assert!(!animator.transitioning());
669        assert_eq!(
670            started(posing(&animator, 3.0)),
671            0.0,
672            "no clock has reached it yet"
673        );
674    }
675
676    #[test]
677    fn a_transition_starts_a_fade_of_the_length_it_states_and_is_entered_for_one_tick() {
678        let mut animator = machine::<Walk>();
679        animate(&mut animator, Told::default(), 0.0);
680        assert!(!animator.entered(Walk::Going));
681
682        animate(&mut animator, go(), 0.1);
683        assert_eq!(animator.state(), Walk::Going);
684        assert!(animator.entered(Walk::Going) && animator.left(Walk::Still));
685        assert!(animator.transitioning());
686        assert_eq!(weight(posing(&animator, 0.1)), Some(0.0));
687        assert_eq!(weight(posing(&animator, 0.2)), Some(0.5), "half a fade in");
688        assert_eq!(
689            weight(posing(&animator, 0.3)),
690            Some(1.0),
691            "and whole at its end"
692        );
693
694        animate(&mut animator, go(), 0.15);
695        assert!(
696            !animator.entered(Walk::Going) && !animator.left(Walk::Still),
697            "which reads for the tick the transition started in alone"
698        );
699    }
700
701    #[test]
702    fn the_motion_a_fade_leaves_runs_on_under_it() {
703        let mut animator = machine::<Walk>();
704        animate(&mut animator, Told::default(), 0.0);
705        animate(&mut animator, go(), 0.1);
706
707        assert!(
708            (started(posing(&animator, 0.1)) - 0.1).abs() < 1e-6,
709            "the loop it left is read where it had reached"
710        );
711        assert!(
712            (started(posing(&animator, 0.25)) - 0.25).abs() < 1e-6,
713            "and runs on while the fade does"
714        );
715        assert_eq!(
716            blended(posing(&animator, 0.1)),
717            Some(0.0),
718            "as the one it entered starts"
719        );
720    }
721
722    #[test]
723    fn a_transition_that_enters_part_way_in_starts_what_it_plays_there() {
724        let mut animator = machine::<Only>();
725        animate(&mut animator, Told::default(), 0.0);
726        animate(&mut animator, restart(0.25), 0.5);
727
728        assert_eq!(
729            blended(posing(&animator, 0.5)),
730            Some(0.25),
731            "a quarter of the way along a clip a second long"
732        );
733    }
734
735    #[test]
736    fn a_transition_to_the_state_it_is_in_moves_nothing_unless_it_starts_again() {
737        let mut running = machine::<Only>();
738        animate(&mut running, Told::default(), 0.0);
739        animate(&mut running, go(), 0.5);
740
741        assert!(!running.transitioning(), "there is nothing to fade between");
742        assert!(
743            (started(posing(&running, 0.5)) - 0.5).abs() < 1e-6,
744            "and what it plays runs on"
745        );
746
747        let mut again = machine::<Only>();
748        animate(&mut again, Told::default(), 0.0);
749        animate(&mut again, restart(0.0), 0.5);
750
751        assert!(again.transitioning());
752        assert!(again.entered(Only::It) && again.left(Only::It));
753        assert_eq!(blended(posing(&again, 0.5)), Some(0.0), "from the start");
754    }
755
756    #[test]
757    fn a_motion_that_ends_fades_from_the_instant_it_ended_and_not_from_the_tick_that_read_it() {
758        let mut animator = machine::<Walk>();
759        animate(&mut animator, go(), 0.0);
760        let mut read = Duration::ZERO;
761        while !animator.entered(Walk::Done) && read.as_secs_f32() < 1.0 {
762            read += TICK;
763            animator.animate(&go(), read, &clips());
764        }
765        let read = read.as_secs_f32();
766
767        assert_eq!(animator.state(), Walk::Done);
768        assert!(read > LONG, "the tick that read the end landed past it");
769        let crossed = (read - LONG) / FADE.as_secs_f32();
770        let weight = weight(posing(&animator, read)).expect("the fade is running");
771        assert!(
772            // The crossing is read to within `0.0001` of the tick, which
773            // leaves the fade within `1e-5` of the way in.
774            (weight - crossed).abs() < 2e-5,
775            "the fade reads {weight} of the way in at the tick, not the {crossed} that a fade \
776             from the end of the clip reads"
777        );
778    }
779
780    #[test]
781    fn a_clip_shorter_than_a_tick_fades_from_the_first_crossing_inside_the_tick() {
782        let mut animator = machine::<Round>();
783        animate(&mut animator, Told::default(), 0.0);
784        animator.animate(&Told::default(), TICK, &clips());
785        let read = TICK.as_secs_f32();
786
787        assert_eq!(animator.state(), Round::Past);
788        // A loop `0.01` seconds long passes half of a cycle at `0.005` and
789        // every `0.01` after, and the machine moves at the first instant
790        // inside the tick that its state would have moved at.
791        let crossed = (read - 0.005) / FADE.as_secs_f32();
792        let weight = weight(posing(&animator, read)).expect("the fade is running");
793        assert!(
794            // The crossing is read to within `0.0001` of the tick, which
795            // leaves the fade within `1e-5` of the way in.
796            (weight - crossed).abs() < 2e-5,
797            "the fade reads {weight} of the way in, not the {crossed} of a fade from the first \
798             crossing"
799        );
800    }
801
802    #[test]
803    fn interrupting_a_fade_stops_what_it_drew_at_the_instant_it_was_interrupted() {
804        let mut animator = machine::<Steps>();
805        animate(&mut animator, Told::default(), 0.0);
806        animate(
807            &mut animator,
808            Told {
809                step: 1,
810                ..Told::default()
811            },
812            0.1,
813        );
814        animate(
815            &mut animator,
816            Told {
817                step: 2,
818                ..Told::default()
819            },
820            0.2,
821        );
822
823        let interrupted = posing(&animator, 0.2);
824        assert!(
825            (started(interrupted) - 0.2).abs() < 1e-6,
826            "the first clip is read where it stood at the interruption"
827        );
828        let over = interrupted.over[0].expect("the fade it interrupted is stopped under it");
829        assert!(
830            (over.sampled.at - 0.1).abs() < 1e-6 && (over.weight - 0.5).abs() < 1e-5,
831            "{over:?} is not the second clip at the weight the fade had reached"
832        );
833        assert_eq!(interrupted.over[1].map(|faded| faded.weight), Some(0.0));
834
835        let later = posing(&animator, 0.3);
836        assert_eq!(
837            (started(later), later.over[0]),
838            (started(interrupted), interrupted.over[0]),
839            "and what was stopped is read the same a fade later"
840        );
841        let entering = later.over[1].expect("the state it entered fades in over that");
842        assert!(
843            (entering.weight - 0.5).abs() < 1e-5,
844            "{entering:?} is not half of the way into the fade that interrupted"
845        );
846    }
847
848    #[test]
849    fn a_fade_into_a_blend_takes_two_places_of_the_pose_and_reads_as_one_fade() {
850        let mut animator = machine::<Into>();
851        animate(&mut animator, Told::default(), 0.0);
852        animate(&mut animator, go(), 0.0);
853
854        // Half of the way into the fade, the pose is half the clip it left
855        // and a quarter of each of the pair, which the pose states as a
856        // third of the way to the first of the pair and then a quarter of
857        // the way to the second.
858        let half = posing(&animator, 0.1);
859        let over = |at: usize| half.over[at].map(|faded| faded.weight);
860
861        assert!(
862            over(0).is_some_and(|weight| (weight - 1.0 / 3.0).abs() < 1e-5),
863            "{half:?} does not read as half of what it left"
864        );
865        assert!(
866            over(1).is_some_and(|weight| (weight - 0.25).abs() < 1e-5),
867            "{half:?} does not read as a quarter of the second of the pair"
868        );
869
870        let whole = posing(&animator, 0.2);
871        assert_eq!(
872            (
873                whole.over[0].map(|faded| faded.weight),
874                whole.over[1].map(|faded| faded.weight)
875            ),
876            (Some(1.0), Some(0.5)),
877            "and at the end of the fade the clip it left is gone and the pair \
878             is read at its own weight"
879        );
880    }
881
882    #[test]
883    fn a_scrubbed_motion_follows_the_value_it_is_given_and_no_clock() {
884        let mut animator = machine::<Opening>();
885        animate(&mut animator, open(0.25), 0.0);
886
887        assert_eq!(started(posing(&animator, 0.0)), 0.25);
888        assert_eq!(
889            started(posing(&animator, 9.0)),
890            0.25,
891            "which no clock moves"
892        );
893
894        animate(&mut animator, open(1.0), 1.0);
895        assert_eq!(started(posing(&animator, 1.0)), 1.0);
896
897        animate(&mut animator, open(4.0), 2.0);
898        assert_eq!(
899            started(posing(&animator, 2.0)),
900            1.0,
901            "a fraction past the end is held inside the clip"
902        );
903    }
904
905    #[test]
906    fn a_blend_keeps_its_place_across_a_change_of_weight() {
907        let mut animator = machine::<Mixing>();
908        animate(&mut animator, mix(0.0, 1.0), 0.0);
909        let before = posing(&animator, 0.3);
910        animate(&mut animator, mix(1.0, 1.0), 0.3);
911        let after = posing(&animator, 0.3);
912
913        assert_eq!(
914            (started(before), blended(before)),
915            (started(after), blended(after)),
916            "both clips are read where they already were"
917        );
918        assert_eq!(weight(after), Some(1.0), "at the weight it was given");
919        assert!(
920            (started(posing(&animator, 0.55)) - 0.3).abs() < 1e-6,
921            "and a cycle of the clip it now reads runs on from there"
922        );
923    }
924
925    #[test]
926    fn a_paced_motion_keeps_its_position_across_a_change_of_pace() {
927        let mut animator = machine::<Mixing>();
928        animate(&mut animator, mix(0.0, 1.0), 0.0);
929        let before = started(posing(&animator, 0.5));
930        animate(&mut animator, mix(0.0, 2.0), 0.5);
931
932        assert_eq!(started(posing(&animator, 0.5)), before);
933        assert!(
934            (started(posing(&animator, 0.6)) - 0.7).abs() < 1e-6,
935            "and runs on at twice the pace"
936        );
937    }
938
939    #[test]
940    fn a_weight_or_a_pace_outside_what_a_motion_takes_is_held_inside_it() {
941        let mut animator = machine::<Mixing>();
942        animate(&mut animator, mix(4.0, 1.0), 0.0);
943        assert_eq!(weight(posing(&animator, 0.0)), Some(1.0));
944
945        animate(&mut animator, mix(f32::NAN, 1.0), 0.1);
946        assert_eq!(
947            weight(posing(&animator, 0.1)),
948            Some(0.0),
949            "and a weight that is no number reads as none of the second clip"
950        );
951
952        let mut held = machine::<Mixing>();
953        animate(&mut held, mix(0.0, -2.0), 0.0);
954        let standing = started(posing(&held, 0.0));
955        animate(&mut held, mix(0.0, -2.0), 1.0);
956
957        assert_eq!(
958            started(posing(&held, 1.0)),
959            standing,
960            "a pace of nothing or less holds a motion where it is"
961        );
962    }
963
964    #[test]
965    fn holding_a_machine_leaves_what_it_plays_where_it_is_until_it_resumes() {
966        let mut animator = machine::<Only>();
967        animate(&mut animator, Told::default(), 0.0);
968        animate(&mut animator, Told::default(), 0.5);
969        let held = started(posing(&animator, 0.5));
970
971        animator.hold();
972        animate(&mut animator, Told::default(), 2.0);
973        assert_eq!(
974            started(posing(&animator, 2.0)),
975            held,
976            "the span it was held over counts for nothing"
977        );
978
979        animator.resume();
980        animate(&mut animator, Told::default(), 2.25);
981        assert_eq!(
982            started(posing(&animator, 2.25)),
983            held,
984            "it plays on from there"
985        );
986
987        animate(&mut animator, Told::default(), 2.5);
988        assert!((started(posing(&animator, 2.5)) - 0.75).abs() < 1e-6);
989    }
990
991    #[test]
992    fn a_held_machine_is_drawn_at_the_instant_it_was_held_whatever_instant_the_frame_draws_at() {
993        let mut animator = machine::<Walk>();
994        animate(&mut animator, Told::default(), 0.0);
995        animate(&mut animator, go(), 0.1);
996        let before = posing(&animator, 0.1);
997        animator.hold();
998        animate(&mut animator, go(), 0.15);
999
1000        assert_eq!(
1001            posing(&animator, 0.15),
1002            before,
1003            "the tick that holds it leaves the pose where the tick before found it"
1004        );
1005        assert_eq!(
1006            posing(&animator, 0.158),
1007            before,
1008            "a frame half a tick past that tick reads the same pose"
1009        );
1010        assert_eq!(
1011            posing(&animator, 0.166),
1012            before,
1013            "and so does one a whole tick past it, mid-fade"
1014        );
1015    }
1016
1017    #[test]
1018    fn a_clock_that_goes_back_is_held_to_the_last_instant_the_machine_read() {
1019        let mut animator = machine::<Only>();
1020        animate(&mut animator, Told::default(), 0.0);
1021        animate(&mut animator, Told::default(), 0.75);
1022        animate(&mut animator, Told::default(), 0.25);
1023
1024        animator.hold();
1025        animate(&mut animator, Told::default(), 1.0);
1026        assert!(
1027            (started(posing(&animator, 1.0)) - 0.75).abs() < 1e-6,
1028            "the hold counted the span since the instant it had read, not one before it"
1029        );
1030    }
1031
1032    #[test]
1033    fn two_machines_run_alike_read_alike() {
1034        let mut one = machine::<Walk>();
1035        let mut two = machine::<Walk>();
1036
1037        for tick in 0..48 {
1038            let at = tick as f32 * TICK.as_secs_f32();
1039            let told = Told {
1040                go: tick > 3,
1041                openness: 0.3,
1042                ..Told::default()
1043            };
1044            animate(&mut one, told, at);
1045            animate(&mut two, told, at);
1046
1047            assert_eq!(one.state(), two.state());
1048            assert_eq!(posing(&one, at), posing(&two, at));
1049            assert_eq!(one.transitioning(), two.transitioning());
1050        }
1051        assert_eq!(
1052            one.state(),
1053            Walk::Done,
1054            "and both ran the whole way through"
1055        );
1056    }
1057}