Skip to main content

mirage_engine/animation/
motion.rs

1use core::time::Duration;
2
3use crate::animation::Progress;
4use crate::mesh::{Animation, Clip, Posing, Sampled, Timeline};
5
6/// The pace a motion runs at until one is set.
7const PACED: f32 = 1.0;
8
9/// The whole of one cycle.
10const WHOLE: f32 = 1.0;
11
12/// What a state plays: one clip, two of them blended, or one a value of the
13/// game's own scrubs.
14///
15/// Every clip runs on its own timeline, so a pace of `1.0` plays it as long
16/// as its source states.
17#[derive(Clone, Debug, PartialEq)]
18pub struct Motion<C: Clip> {
19    plays: Plays<C>,
20    pace: f32,
21}
22
23impl<C: Clip> Motion<C> {
24    /// Plays `clip` over and over, wrapping from its last key to its first.
25    pub fn looping(clip: C) -> Self {
26        Self::of(Plays::Looping(clip))
27    }
28
29    /// Plays `clip` once and holds its last key.
30    pub fn once(clip: C) -> Self {
31        Self::of(Plays::Once(clip))
32    }
33
34    /// Plays `from` and `to` together, `weight` of the way to `to`, as a
35    /// fraction held inside `0.0..=1.0` and read as `0.0` where it is not a
36    /// number.
37    ///
38    /// Each is read at the same fraction of its own timeline and both count
39    /// cycles together, so a walk and a run of different lengths keep their
40    /// steps together.
41    pub fn blend(from: C, to: C, weight: f32) -> Self {
42        Self::of(Plays::Blended {
43            from,
44            to,
45            weight: within(weight),
46        })
47    }
48
49    /// Holds `clip` at `fraction` of the way along it, a fraction held
50    /// inside `0.0..=1.0` and read as `0.0` where it is not a number.
51    ///
52    /// Required if you want a value of the game's own to pose a mesh: no
53    /// clock moves it, so a door opens as far as the game states and no
54    /// further.
55    pub fn scrubbed(clip: C, fraction: f32) -> Self {
56        Self::of(Plays::Scrubbed {
57            clip,
58            fraction: within(fraction),
59        })
60    }
61
62    /// Runs at `rate` of the pace its source states, a fraction that is `1.0`
63    /// until set.
64    ///
65    /// A rate of nothing or less holds the motion where it is, and one
66    /// that is not a number reads as `1.0`. The last call is the one used.
67    pub fn paced(mut self, rate: f32) -> Self {
68        self.pace = paced(rate);
69        self
70    }
71
72    /// The motion that reads `plays` at the pace its source states.
73    fn of(plays: Plays<C>) -> Self {
74        Self { plays, pace: PACED }
75    }
76}
77
78/// What a motion reads, by whatever names its clips: a game's own
79/// vocabulary where a state states it, their places in that vocabulary
80/// where a machine runs it.
81#[derive(Clone, Copy, Debug, PartialEq)]
82enum Plays<C> {
83    Looping(C),
84    Once(C),
85    Blended { from: C, to: C, weight: f32 },
86    Scrubbed { clip: C, fraction: f32 },
87}
88
89impl<C: Clip> Plays<C> {
90    /// The same, with every clip by its place in its vocabulary.
91    fn indexed(&self) -> Plays<u32> {
92        match self {
93            Self::Looping(clip) => Plays::Looping(clip.index()),
94            Self::Once(clip) => Plays::Once(clip.index()),
95            Self::Blended { from, to, weight } => Plays::Blended {
96                from: from.index(),
97                to: to.index(),
98                weight: *weight,
99            },
100            Self::Scrubbed { clip, fraction } => Plays::Scrubbed {
101                clip: clip.index(),
102                fraction: *fraction,
103            },
104        }
105    }
106}
107
108impl<C: PartialEq> Plays<C> {
109    /// Whether both read the same clips the same way, whatever weight or
110    /// fraction each of them holds.
111    fn same_clips(&self, other: &Self) -> bool {
112        match (self, other) {
113            (Self::Looping(one), Self::Looping(two)) | (Self::Once(one), Self::Once(two)) => {
114                one == two
115            }
116            (
117                Self::Blended { from, to, .. },
118                Self::Blended {
119                    from: other_from,
120                    to: other_to,
121                    ..
122                },
123            ) => from == other_from && to == other_to,
124            (Self::Scrubbed { clip, .. }, Self::Scrubbed { clip: other, .. }) => clip == other,
125            _ => false,
126        }
127    }
128}
129
130/// A motion and where a machine is in it: the clips by their places, the
131/// pace it runs at, the instant it started and the cycles it started at.
132#[derive(Clone, Copy, Debug, PartialEq)]
133pub(crate) struct Timed {
134    plays: Plays<u32>,
135    pace: f32,
136    /// The instant it started, absent until `animate` first runs the
137    /// machine, which leaves it where it starts.
138    started: Option<Duration>,
139    /// The cycles it had run when it started, which entering part way in
140    /// sets.
141    from: f32,
142}
143
144impl Timed {
145    /// The motion `motion` states, started at `started` and `from` cycles
146    /// into it.
147    pub(crate) fn new<C: Clip>(motion: &Motion<C>, started: Option<Duration>, from: f32) -> Self {
148        Self {
149            plays: motion.plays.indexed(),
150            pace: motion.pace,
151            started,
152            from,
153        }
154    }
155
156    /// How far along the motion the machine is at `now`.
157    pub(crate) fn progress(&self, now: Duration, clips: &[Animation]) -> Progress {
158        Progress::new(self.run(now, clips), self.ends())
159    }
160
161    /// The pose it holds at `now`.
162    pub(crate) fn posing(&self, now: Duration, clips: &[Animation]) -> Posing {
163        self.reading(now, clips).posing()
164    }
165
166    /// `base` blended `weight` of the way to the pose it holds at `now`.
167    pub(crate) fn blended_over(
168        &self,
169        base: Posing,
170        weight: f32,
171        now: Duration,
172        clips: &[Animation],
173    ) -> Posing {
174        self.reading(now, clips)
175            .blended_over(base, weight.clamp(0.0, WHOLE))
176    }
177
178    /// The motion `motion` states, kept where the machine already is in
179    /// it where it reads the same clips and started afresh where it does
180    /// not.
181    ///
182    /// A change of pace or of weight alone moves the start instant, so a
183    /// loop keeps the position it is at and a blend the cycle it is in.
184    pub(crate) fn kept<C: Clip>(
185        self,
186        motion: &Motion<C>,
187        now: Duration,
188        clips: &[Animation],
189    ) -> Self {
190        let plays = motion.plays.indexed();
191        if plays == self.plays && motion.pace == self.pace {
192            return self;
193        }
194        if !plays.same_clips(&self.plays) {
195            return Self::new(motion, Some(now), 0.0);
196        }
197
198        Self {
199            plays,
200            pace: motion.pace,
201            started: Some(now),
202            from: self.run(now, clips),
203        }
204    }
205
206    /// The same motion, started at `now` where no clock has run it yet.
207    pub(crate) fn started_at(mut self, now: Duration) -> Self {
208        self.started = self.started.or(Some(now));
209        self
210    }
211
212    /// The same motion started `span` later, which leaves it where it is
213    /// while the machine is held.
214    pub(crate) fn shifted(mut self, span: Duration) -> Self {
215        self.started = self
216            .started
217            .map(|started| started.checked_add(span).unwrap_or(started));
218        self
219    }
220
221    /// The cycles of it that have run by `now`.
222    fn run(&self, now: Duration, clips: &[Animation]) -> f32 {
223        if let Plays::Scrubbed { fraction, .. } = self.plays {
224            return fraction;
225        }
226        let cycle = self.cycle(clips);
227        if cycle <= 0.0 {
228            // A clip with no keys holds every joint at rest, so a motion
229            // that ends has ended the instant it starts.
230            return match self.ends() {
231                true => WHOLE,
232                false => self.from,
233            };
234        }
235        let Some(started) = self.started else {
236            return self.from;
237        };
238
239        self.from + now.saturating_sub(started).as_secs_f32() * self.pace / cycle
240    }
241
242    /// How long one cycle of it runs for, in seconds; nothing at all where
243    /// its clips hold no keys.
244    fn cycle(&self, clips: &[Animation]) -> f32 {
245        match self.plays {
246            Plays::Looping(clip) | Plays::Once(clip) => timeline(clip, clips).span(),
247            Plays::Blended { from, to, weight } => {
248                let together = rate(timeline(from, clips).span(), WHOLE - weight)
249                    + rate(timeline(to, clips).span(), weight);
250                match together.is_finite() && together > 0.0 {
251                    true => WHOLE / together,
252                    false => 0.0,
253                }
254            }
255            Plays::Scrubbed { .. } => 0.0,
256        }
257    }
258
259    /// Whether it holds at the end of one cycle rather than wrapping.
260    fn ends(&self) -> bool {
261        matches!(self.plays, Plays::Once(_) | Plays::Scrubbed { .. })
262    }
263
264    /// What it reads at `now`.
265    fn reading(&self, now: Duration, clips: &[Animation]) -> Reading {
266        let run = self.run(now, clips);
267        match self.plays {
268            Plays::Looping(clip) => Reading::One(sampled(clip, run.rem_euclid(WHOLE), clips)),
269            Plays::Once(clip) => Reading::One(sampled(clip, run.clamp(0.0, WHOLE), clips)),
270            Plays::Scrubbed { clip, fraction } => Reading::One(sampled(clip, fraction, clips)),
271            Plays::Blended { from, to, weight } => {
272                let phase = run.rem_euclid(WHOLE);
273                Reading::Pair {
274                    from: sampled(from, phase, clips),
275                    to: sampled(to, phase, clips),
276                    weight,
277                }
278            }
279        }
280    }
281}
282
283/// What a motion reads at one instant: one clip, or the pair a blend
284/// reads, each at the same fraction of its own timeline.
285enum Reading {
286    One(Sampled),
287    Pair {
288        from: Sampled,
289        to: Sampled,
290        weight: f32,
291    },
292}
293
294impl Reading {
295    /// The pose it holds on its own.
296    fn posing(self) -> Posing {
297        match self {
298            Self::One(sampled) => Posing::clip(sampled.clip, sampled.at),
299            Self::Pair { from, to, weight } => Posing::clip(from.clip, from.at).blended(to, weight),
300        }
301    }
302
303    /// `base` blended `weight` of the way to it.
304    ///
305    /// A pair takes two places of the pose; the first is scaled so that the
306    /// two of them together read as one fade from what lies under them to
307    /// the pair, which two fades blended one over the other are not on
308    /// their own.
309    fn blended_over(self, base: Posing, weight: f32) -> Posing {
310        match self {
311            Self::One(sampled) => base.blended(sampled, weight),
312            Self::Pair {
313                from,
314                to,
315                weight: between,
316            } => {
317                let taken = weight * between;
318                let first = match taken < WHOLE {
319                    true => weight * (WHOLE - between) / (WHOLE - taken),
320                    false => 0.0,
321                };
322                base.blended(from, first).blended(to, taken)
323            }
324        }
325    }
326}
327
328/// The timeline of the clip at `clip`, which is empty where the mesh drawn
329/// holds no such clip.
330fn timeline(clip: u32, clips: &[Animation]) -> Timeline {
331    clips
332        .get(clip as usize)
333        .map(Animation::timeline)
334        .unwrap_or_default()
335}
336
337/// The clip at `clip` read `fraction` of the way along its own timeline.
338fn sampled(clip: u32, fraction: f32, clips: &[Animation]) -> Sampled {
339    Sampled {
340        clip,
341        at: timeline(clip, clips).at(fraction),
342    }
343}
344
345/// The cycles a second a clip `span` seconds long runs at for the `share`
346/// of a blend it takes: nothing where it takes none of the blend, and
347/// without end where the clip holds no keys at all.
348fn rate(span: f32, share: f32) -> f32 {
349    if share <= 0.0 {
350        return 0.0;
351    }
352    match span > 0.0 {
353        true => share / span,
354        false => f32::INFINITY,
355    }
356}
357
358/// `value` inside `0.0..=1.0`, and nothing at all where it is not a number.
359fn within(value: f32) -> f32 {
360    match value.is_nan() {
361        true => 0.0,
362        false => value.clamp(0.0, WHOLE),
363    }
364}
365
366/// `rate` from nothing up, and the pace its source states where it is not a
367/// number.
368fn paced(rate: f32) -> f32 {
369    match rate.is_nan() {
370        true => PACED,
371        false => rate.max(0.0),
372    }
373}