use core::time::Duration;
use crate::animation::Progress;
use crate::mesh::{Animation, Clip, Posing, Sampled, Timeline};
const PACED: f32 = 1.0;
const WHOLE: f32 = 1.0;
#[derive(Clone, Debug, PartialEq)]
pub struct Motion<C: Clip> {
plays: Plays<C>,
pace: f32,
}
impl<C: Clip> Motion<C> {
pub fn looping(clip: C) -> Self {
Self::of(Plays::Looping(clip))
}
pub fn once(clip: C) -> Self {
Self::of(Plays::Once(clip))
}
pub fn blend(from: C, to: C, weight: f32) -> Self {
Self::of(Plays::Blended {
from,
to,
weight: within(weight),
})
}
pub fn scrubbed(clip: C, fraction: f32) -> Self {
Self::of(Plays::Scrubbed {
clip,
fraction: within(fraction),
})
}
pub fn paced(mut self, rate: f32) -> Self {
self.pace = paced(rate);
self
}
fn of(plays: Plays<C>) -> Self {
Self { plays, pace: PACED }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum Plays<C> {
Looping(C),
Once(C),
Blended { from: C, to: C, weight: f32 },
Scrubbed { clip: C, fraction: f32 },
}
impl<C: Clip> Plays<C> {
fn indexed(&self) -> Plays<u32> {
match self {
Self::Looping(clip) => Plays::Looping(clip.index()),
Self::Once(clip) => Plays::Once(clip.index()),
Self::Blended { from, to, weight } => Plays::Blended {
from: from.index(),
to: to.index(),
weight: *weight,
},
Self::Scrubbed { clip, fraction } => Plays::Scrubbed {
clip: clip.index(),
fraction: *fraction,
},
}
}
}
impl<C: PartialEq> Plays<C> {
fn same_clips(&self, other: &Self) -> bool {
match (self, other) {
(Self::Looping(one), Self::Looping(two)) | (Self::Once(one), Self::Once(two)) => {
one == two
}
(
Self::Blended { from, to, .. },
Self::Blended {
from: other_from,
to: other_to,
..
},
) => from == other_from && to == other_to,
(Self::Scrubbed { clip, .. }, Self::Scrubbed { clip: other, .. }) => clip == other,
_ => false,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Timed {
plays: Plays<u32>,
pace: f32,
started: Option<Duration>,
from: f32,
}
impl Timed {
pub(crate) fn new<C: Clip>(motion: &Motion<C>, started: Option<Duration>, from: f32) -> Self {
Self {
plays: motion.plays.indexed(),
pace: motion.pace,
started,
from,
}
}
pub(crate) fn progress(&self, now: Duration, clips: &[Animation]) -> Progress {
Progress::new(self.run(now, clips), self.ends())
}
pub(crate) fn posing(&self, now: Duration, clips: &[Animation]) -> Posing {
self.reading(now, clips).posing()
}
pub(crate) fn blended_over(
&self,
base: Posing,
weight: f32,
now: Duration,
clips: &[Animation],
) -> Posing {
self.reading(now, clips)
.blended_over(base, weight.clamp(0.0, WHOLE))
}
pub(crate) fn kept<C: Clip>(
self,
motion: &Motion<C>,
now: Duration,
clips: &[Animation],
) -> Self {
let plays = motion.plays.indexed();
if plays == self.plays && motion.pace == self.pace {
return self;
}
if !plays.same_clips(&self.plays) {
return Self::new(motion, Some(now), 0.0);
}
Self {
plays,
pace: motion.pace,
started: Some(now),
from: self.run(now, clips),
}
}
pub(crate) fn started_at(mut self, now: Duration) -> Self {
self.started = self.started.or(Some(now));
self
}
pub(crate) fn shifted(mut self, span: Duration) -> Self {
self.started = self
.started
.map(|started| started.checked_add(span).unwrap_or(started));
self
}
fn run(&self, now: Duration, clips: &[Animation]) -> f32 {
if let Plays::Scrubbed { fraction, .. } = self.plays {
return fraction;
}
let cycle = self.cycle(clips);
if cycle <= 0.0 {
return match self.ends() {
true => WHOLE,
false => self.from,
};
}
let Some(started) = self.started else {
return self.from;
};
self.from + now.saturating_sub(started).as_secs_f32() * self.pace / cycle
}
fn cycle(&self, clips: &[Animation]) -> f32 {
match self.plays {
Plays::Looping(clip) | Plays::Once(clip) => timeline(clip, clips).span(),
Plays::Blended { from, to, weight } => {
let together = rate(timeline(from, clips).span(), WHOLE - weight)
+ rate(timeline(to, clips).span(), weight);
match together.is_finite() && together > 0.0 {
true => WHOLE / together,
false => 0.0,
}
}
Plays::Scrubbed { .. } => 0.0,
}
}
fn ends(&self) -> bool {
matches!(self.plays, Plays::Once(_) | Plays::Scrubbed { .. })
}
fn reading(&self, now: Duration, clips: &[Animation]) -> Reading {
let run = self.run(now, clips);
match self.plays {
Plays::Looping(clip) => Reading::One(sampled(clip, run.rem_euclid(WHOLE), clips)),
Plays::Once(clip) => Reading::One(sampled(clip, run.clamp(0.0, WHOLE), clips)),
Plays::Scrubbed { clip, fraction } => Reading::One(sampled(clip, fraction, clips)),
Plays::Blended { from, to, weight } => {
let phase = run.rem_euclid(WHOLE);
Reading::Pair {
from: sampled(from, phase, clips),
to: sampled(to, phase, clips),
weight,
}
}
}
}
}
enum Reading {
One(Sampled),
Pair {
from: Sampled,
to: Sampled,
weight: f32,
},
}
impl Reading {
fn posing(self) -> Posing {
match self {
Self::One(sampled) => Posing::clip(sampled.clip, sampled.at),
Self::Pair { from, to, weight } => Posing::clip(from.clip, from.at).blended(to, weight),
}
}
fn blended_over(self, base: Posing, weight: f32) -> Posing {
match self {
Self::One(sampled) => base.blended(sampled, weight),
Self::Pair {
from,
to,
weight: between,
} => {
let taken = weight * between;
let first = match taken < WHOLE {
true => weight * (WHOLE - between) / (WHOLE - taken),
false => 0.0,
};
base.blended(from, first).blended(to, taken)
}
}
}
}
fn timeline(clip: u32, clips: &[Animation]) -> Timeline {
clips
.get(clip as usize)
.map(Animation::timeline)
.unwrap_or_default()
}
fn sampled(clip: u32, fraction: f32, clips: &[Animation]) -> Sampled {
Sampled {
clip,
at: timeline(clip, clips).at(fraction),
}
}
fn rate(span: f32, share: f32) -> f32 {
if share <= 0.0 {
return 0.0;
}
match span > 0.0 {
true => share / span,
false => f32::INFINITY,
}
}
fn within(value: f32) -> f32 {
match value.is_nan() {
true => 0.0,
false => value.clamp(0.0, WHOLE),
}
}
fn paced(rate: f32) -> f32 {
match rate.is_nan() {
true => PACED,
false => rate.max(0.0),
}
}