1use core::time::Duration;
2
3use crate::animation::Progress;
4use crate::mesh::{Animation, Clip, Posing, Sampled, Timeline};
5
6const PACED: f32 = 1.0;
8
9const WHOLE: f32 = 1.0;
11
12#[derive(Clone, Debug, PartialEq)]
18pub struct Motion<C: Clip> {
19 plays: Plays<C>,
20 pace: f32,
21}
22
23impl<C: Clip> Motion<C> {
24 pub fn looping(clip: C) -> Self {
26 Self::of(Plays::Looping(clip))
27 }
28
29 pub fn once(clip: C) -> Self {
31 Self::of(Plays::Once(clip))
32 }
33
34 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 pub fn scrubbed(clip: C, fraction: f32) -> Self {
56 Self::of(Plays::Scrubbed {
57 clip,
58 fraction: within(fraction),
59 })
60 }
61
62 pub fn paced(mut self, rate: f32) -> Self {
68 self.pace = paced(rate);
69 self
70 }
71
72 fn of(plays: Plays<C>) -> Self {
74 Self { plays, pace: PACED }
75 }
76}
77
78#[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 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 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#[derive(Clone, Copy, Debug, PartialEq)]
133pub(crate) struct Timed {
134 plays: Plays<u32>,
135 pace: f32,
136 started: Option<Duration>,
139 from: f32,
142}
143
144impl Timed {
145 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 pub(crate) fn progress(&self, now: Duration, clips: &[Animation]) -> Progress {
158 Progress::new(self.run(now, clips), self.ends())
159 }
160
161 pub(crate) fn posing(&self, now: Duration, clips: &[Animation]) -> Posing {
163 self.reading(now, clips).posing()
164 }
165
166 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 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 pub(crate) fn started_at(mut self, now: Duration) -> Self {
208 self.started = self.started.or(Some(now));
209 self
210 }
211
212 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 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 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 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 fn ends(&self) -> bool {
261 matches!(self.plays, Plays::Once(_) | Plays::Scrubbed { .. })
262 }
263
264 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
283enum Reading {
286 One(Sampled),
287 Pair {
288 from: Sampled,
289 to: Sampled,
290 weight: f32,
291 },
292}
293
294impl Reading {
295 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 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
328fn timeline(clip: u32, clips: &[Animation]) -> Timeline {
331 clips
332 .get(clip as usize)
333 .map(Animation::timeline)
334 .unwrap_or_default()
335}
336
337fn sampled(clip: u32, fraction: f32, clips: &[Animation]) -> Sampled {
339 Sampled {
340 clip,
341 at: timeline(clip, clips).at(fraction),
342 }
343}
344
345fn 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
358fn within(value: f32) -> f32 {
360 match value.is_nan() {
361 true => 0.0,
362 false => value.clamp(0.0, WHOLE),
363 }
364}
365
366fn paced(rate: f32) -> f32 {
369 match rate.is_nan() {
370 true => PACED,
371 false => rate.max(0.0),
372 }
373}