use core::fmt::{self, Debug, Formatter};
use core::marker::PhantomData;
use core::time::Duration;
use crate::animation::posed::{Fading, Posed};
use crate::animation::{AnimationStates, Running, Timed, Transition};
use crate::mesh::Animation;
const WHOLE: f32 = 1.0;
const CROSSINGS: u32 = 14;
pub struct Animator<M, S: AnimationStates> {
state: S,
motion: Timed,
fading: Option<Fading>,
changed: Option<Change<S>>,
holding: Holding,
seen: Duration,
mesh: PhantomData<fn() -> M>,
}
impl<M, S: AnimationStates> Animator<M, S> {
pub fn new() -> Self {
let state = S::entry();
let motion = Timed::new(&state.motion(&S::Input::default()), None, 0.0);
Self {
state,
motion,
fading: None,
changed: None,
holding: Holding::Running,
seen: Duration::ZERO,
mesh: PhantomData,
}
}
pub fn state(&self) -> S {
self.state
}
pub fn entered(&self, state: S) -> bool {
self.changed.is_some_and(|changed| changed.entered == state)
}
pub fn left(&self, state: S) -> bool {
self.changed.is_some_and(|changed| changed.left == state)
}
pub fn transitioning(&self) -> bool {
self.fading.is_some()
}
pub fn hold(&mut self) {
self.holding = Holding::Held;
}
pub fn resume(&mut self) {
if self.holding == Holding::Held {
self.holding = Holding::Resumed;
}
}
pub(crate) fn animate(&mut self, input: &S::Input, now: Duration, clips: &[Animation]) {
self.changed = None;
let now = now.max(self.seen);
let held = now.saturating_sub(self.seen);
let mut since = self.seen;
self.seen = now;
if self.holding != Holding::Running {
self.shift(held);
if self.holding == Holding::Held {
return;
}
self.holding = Holding::Running;
since = now;
}
self.settle(now);
self.motion = self.motion.started_at(now);
let at = self.motion.progress(now, clips);
if let Some(transition) = self.state.next(input, at) {
let crossing = self.crossing(transition.into, input, since, now, clips);
self.enter(transition, crossing, input, clips);
}
let motion = self.state.motion(input);
self.motion = self.motion.kept(&motion, now, clips);
}
pub(crate) fn running(&self) -> Running {
Running {
posed: Posed::Playing(self.motion),
fading: self.fading,
held: self.holding.stands().then_some(self.seen),
}
}
fn enter(
&mut self,
transition: Transition<S>,
crossing: Duration,
input: &S::Input,
clips: &[Animation],
) {
if transition.into == self.state && !transition.restarted {
return;
}
self.fading = match transition.fade.is_zero() {
true => None,
false => Some(Fading {
under: match self.fading {
Some(_) => Posed::Stopped(self.running().posing(crossing, clips)),
None => Posed::Playing(self.motion),
},
from: crossing,
over: transition.fade,
}),
};
self.motion = Timed::new(
&transition.into.motion(input),
Some(crossing),
transition.entering_at,
);
self.changed = Some(Change {
left: self.state,
entered: transition.into,
});
self.state = transition.into;
}
fn crossing(
&self,
into: S,
input: &S::Input,
since: Duration,
now: Duration,
clips: &[Animation],
) -> Duration {
let moves = |at: Duration| {
self.state
.next(input, self.motion.progress(at, clips))
.is_some_and(|transition| transition.into == into)
};
if since >= now || moves(since) {
return now;
}
let mut before = since;
let mut after = now;
for _ in 0..CROSSINGS {
let between = before + (after - before) / 2;
match moves(between) {
true => after = between,
false => before = between,
}
}
after
}
fn settle(&mut self, now: Duration) {
if self
.fading
.is_some_and(|fading| fading.weight(now) >= WHOLE)
{
self.fading = None;
}
}
fn shift(&mut self, span: Duration) {
self.motion = self.motion.shifted(span);
self.fading = self.fading.map(|fading| fading.shifted(span));
}
}
impl<M, S: AnimationStates> Clone for Animator<M, S> {
fn clone(&self) -> Self {
Self {
state: self.state,
motion: self.motion,
fading: self.fading,
changed: self.changed,
holding: self.holding,
seen: self.seen,
mesh: PhantomData,
}
}
}
impl<M, S: AnimationStates> Debug for Animator<M, S> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Animator")
.field("state", &self.state)
.field("transitioning", &self.transitioning())
.finish()
}
}
impl<M, S: AnimationStates> Default for Animator<M, S> {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Holding {
Running,
Held,
Resumed,
}
impl Holding {
fn stands(self) -> bool {
self != Self::Running
}
}
#[derive(Clone, Copy, Debug)]
struct Change<S> {
left: S,
entered: S,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::animation::{Motion, Progress};
use crate::math::Vec3;
use crate::mesh::{Clip, Keys, Moves, Posing, Track};
const FADE: Duration = Duration::from_millis(200);
const TICK: Duration = Duration::from_nanos(16_666_667);
const LONG: f32 = 0.458_333_34;
const SPANS: [f32; 4] = [1.0, 0.25, LONG, 0.01];
struct Puppet;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Step {
Slow,
Quick,
Long,
Brief,
}
impl Clip for Step {
fn from_name(_name: &str) -> Option<Self> {
None
}
fn all() -> Vec<Self> {
vec![Self::Slow, Self::Quick, Self::Long, Self::Brief]
}
fn index(&self) -> u32 {
*self as u32
}
}
fn clips() -> Vec<Animation> {
SPANS
.iter()
.map(|&span| {
Animation::new(vec![Track {
joint: 0,
moves: Moves::Position(Keys::Linear(vec![(0.0, Vec3::ZERO), (span, Vec3::X)])),
}])
})
.collect()
}
#[derive(Clone, Copy, Debug)]
struct Told {
go: bool,
restart: bool,
entering_at: f32,
step: u32,
weight: f32,
pace: f32,
openness: f32,
}
impl Default for Told {
fn default() -> Self {
Self {
go: false,
restart: false,
entering_at: 0.0,
step: 0,
weight: 0.0,
pace: 1.0,
openness: 0.0,
}
}
}
fn go() -> Told {
Told {
go: true,
..Told::default()
}
}
fn restart(at: f32) -> Told {
Told {
restart: true,
entering_at: at,
..Told::default()
}
}
fn mix(weight: f32, pace: f32) -> Told {
Told {
weight,
pace,
..Told::default()
}
}
fn open(openness: f32) -> Told {
Told {
openness,
..Told::default()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Walk {
Still,
Going,
Done,
}
impl AnimationStates for Walk {
type Clip = Step;
type Input = Told;
fn entry() -> Self {
Self::Still
}
fn motion(&self, input: &Told) -> Motion<Step> {
match self {
Self::Still => Motion::looping(Step::Slow),
Self::Going => Motion::once(Step::Long),
Self::Done => Motion::scrubbed(Step::Slow, input.openness),
}
}
fn next(&self, input: &Told, at: Progress) -> Option<Transition<Self>> {
match self {
Self::Still if input.go => Some(Self::Going.fade(FADE)),
Self::Going if at.ended() => Some(Self::Done.fade(FADE)),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Round {
Turning,
Past,
}
impl AnimationStates for Round {
type Clip = Step;
type Input = Told;
fn entry() -> Self {
Self::Turning
}
fn motion(&self, _input: &Told) -> Motion<Step> {
match self {
Self::Turning => Motion::looping(Step::Brief),
Self::Past => Motion::looping(Step::Slow),
}
}
fn next(&self, _input: &Told, at: Progress) -> Option<Transition<Self>> {
match self {
Self::Turning if at.past(0.5) => Some(Self::Past.fade(FADE)),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Steps {
First,
Second,
Third,
}
impl AnimationStates for Steps {
type Clip = Step;
type Input = Told;
fn entry() -> Self {
Self::First
}
fn motion(&self, _input: &Told) -> Motion<Step> {
match self {
Self::First => Motion::looping(Step::Slow),
Self::Second => Motion::looping(Step::Quick),
Self::Third => Motion::looping(Step::Long),
}
}
fn next(&self, input: &Told, _at: Progress) -> Option<Transition<Self>> {
let wanted = match input.step {
0 => Self::First,
1 => Self::Second,
_ => Self::Third,
};
(wanted != *self).then(|| wanted.fade(FADE))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Only {
It,
}
impl AnimationStates for Only {
type Clip = Step;
type Input = Told;
fn entry() -> Self {
Self::It
}
fn motion(&self, _input: &Told) -> Motion<Step> {
Motion::looping(Step::Slow)
}
fn next(&self, input: &Told, _at: Progress) -> Option<Transition<Self>> {
match (input.restart, input.go) {
(true, _) => Some(Self::It.restarted().entering_at(input.entering_at)),
(_, true) => Some(Self::It.fade(FADE)),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Into {
One,
Pair,
}
impl AnimationStates for Into {
type Clip = Step;
type Input = Told;
fn entry() -> Self {
Self::One
}
fn motion(&self, _input: &Told) -> Motion<Step> {
match self {
Self::One => Motion::looping(Step::Slow),
Self::Pair => Motion::blend(Step::Quick, Step::Long, 0.5),
}
}
fn next(&self, input: &Told, _at: Progress) -> Option<Transition<Self>> {
match input.go {
true => Some(Self::Pair.fade(FADE)),
false => None,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Opening {
It,
}
impl AnimationStates for Opening {
type Clip = Step;
type Input = Told;
fn entry() -> Self {
Self::It
}
fn motion(&self, input: &Told) -> Motion<Step> {
Motion::scrubbed(Step::Slow, input.openness)
}
fn next(&self, _input: &Told, _at: Progress) -> Option<Transition<Self>> {
None
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Mixing {
It,
}
impl AnimationStates for Mixing {
type Clip = Step;
type Input = Told;
fn entry() -> Self {
Self::It
}
fn motion(&self, input: &Told) -> Motion<Step> {
Motion::blend(Step::Slow, Step::Quick, input.weight).paced(input.pace)
}
fn next(&self, _input: &Told, _at: Progress) -> Option<Transition<Self>> {
None
}
}
fn machine<S: AnimationStates<Input = Told>>() -> Animator<Puppet, S> {
Animator::new()
}
fn animate<S: AnimationStates<Input = Told>>(
animator: &mut Animator<Puppet, S>,
input: Told,
at: f32,
) {
animator.animate(&input, seconds(at), &clips());
}
fn posing<S: AnimationStates<Input = Told>>(animator: &Animator<Puppet, S>, at: f32) -> Posing {
animator.running().posing(seconds(at), &clips())
}
fn seconds(at: f32) -> Duration {
Duration::from_secs_f32(at)
}
fn started(posing: Posing) -> f32 {
posing.from.at
}
fn weight(posing: Posing) -> Option<f32> {
posing.over[0].map(|faded| faded.weight)
}
fn blended(posing: Posing) -> Option<f32> {
posing.over[0].map(|faded| faded.sampled.at)
}
#[test]
fn a_machine_starts_in_its_entry_state_at_the_start_of_what_that_state_plays() {
let animator = machine::<Walk>();
assert_eq!(animator.state(), Walk::Still);
assert!(!animator.transitioning());
assert_eq!(
started(posing(&animator, 3.0)),
0.0,
"no clock has reached it yet"
);
}
#[test]
fn a_transition_starts_a_fade_of_the_length_it_states_and_is_entered_for_one_tick() {
let mut animator = machine::<Walk>();
animate(&mut animator, Told::default(), 0.0);
assert!(!animator.entered(Walk::Going));
animate(&mut animator, go(), 0.1);
assert_eq!(animator.state(), Walk::Going);
assert!(animator.entered(Walk::Going) && animator.left(Walk::Still));
assert!(animator.transitioning());
assert_eq!(weight(posing(&animator, 0.1)), Some(0.0));
assert_eq!(weight(posing(&animator, 0.2)), Some(0.5), "half a fade in");
assert_eq!(
weight(posing(&animator, 0.3)),
Some(1.0),
"and whole at its end"
);
animate(&mut animator, go(), 0.15);
assert!(
!animator.entered(Walk::Going) && !animator.left(Walk::Still),
"which reads for the tick the transition started in alone"
);
}
#[test]
fn the_motion_a_fade_leaves_runs_on_under_it() {
let mut animator = machine::<Walk>();
animate(&mut animator, Told::default(), 0.0);
animate(&mut animator, go(), 0.1);
assert!(
(started(posing(&animator, 0.1)) - 0.1).abs() < 1e-6,
"the loop it left is read where it had reached"
);
assert!(
(started(posing(&animator, 0.25)) - 0.25).abs() < 1e-6,
"and runs on while the fade does"
);
assert_eq!(
blended(posing(&animator, 0.1)),
Some(0.0),
"as the one it entered starts"
);
}
#[test]
fn a_transition_that_enters_part_way_in_starts_what_it_plays_there() {
let mut animator = machine::<Only>();
animate(&mut animator, Told::default(), 0.0);
animate(&mut animator, restart(0.25), 0.5);
assert_eq!(
blended(posing(&animator, 0.5)),
Some(0.25),
"a quarter of the way along a clip a second long"
);
}
#[test]
fn a_transition_to_the_state_it_is_in_moves_nothing_unless_it_starts_again() {
let mut running = machine::<Only>();
animate(&mut running, Told::default(), 0.0);
animate(&mut running, go(), 0.5);
assert!(!running.transitioning(), "there is nothing to fade between");
assert!(
(started(posing(&running, 0.5)) - 0.5).abs() < 1e-6,
"and what it plays runs on"
);
let mut again = machine::<Only>();
animate(&mut again, Told::default(), 0.0);
animate(&mut again, restart(0.0), 0.5);
assert!(again.transitioning());
assert!(again.entered(Only::It) && again.left(Only::It));
assert_eq!(blended(posing(&again, 0.5)), Some(0.0), "from the start");
}
#[test]
fn a_motion_that_ends_fades_from_the_instant_it_ended_and_not_from_the_tick_that_read_it() {
let mut animator = machine::<Walk>();
animate(&mut animator, go(), 0.0);
let mut read = Duration::ZERO;
while !animator.entered(Walk::Done) && read.as_secs_f32() < 1.0 {
read += TICK;
animator.animate(&go(), read, &clips());
}
let read = read.as_secs_f32();
assert_eq!(animator.state(), Walk::Done);
assert!(read > LONG, "the tick that read the end landed past it");
let crossed = (read - LONG) / FADE.as_secs_f32();
let weight = weight(posing(&animator, read)).expect("the fade is running");
assert!(
(weight - crossed).abs() < 2e-5,
"the fade reads {weight} of the way in at the tick, not the {crossed} that a fade \
from the end of the clip reads"
);
}
#[test]
fn a_clip_shorter_than_a_tick_fades_from_the_first_crossing_inside_the_tick() {
let mut animator = machine::<Round>();
animate(&mut animator, Told::default(), 0.0);
animator.animate(&Told::default(), TICK, &clips());
let read = TICK.as_secs_f32();
assert_eq!(animator.state(), Round::Past);
let crossed = (read - 0.005) / FADE.as_secs_f32();
let weight = weight(posing(&animator, read)).expect("the fade is running");
assert!(
(weight - crossed).abs() < 2e-5,
"the fade reads {weight} of the way in, not the {crossed} of a fade from the first \
crossing"
);
}
#[test]
fn interrupting_a_fade_stops_what_it_drew_at_the_instant_it_was_interrupted() {
let mut animator = machine::<Steps>();
animate(&mut animator, Told::default(), 0.0);
animate(
&mut animator,
Told {
step: 1,
..Told::default()
},
0.1,
);
animate(
&mut animator,
Told {
step: 2,
..Told::default()
},
0.2,
);
let interrupted = posing(&animator, 0.2);
assert!(
(started(interrupted) - 0.2).abs() < 1e-6,
"the first clip is read where it stood at the interruption"
);
let over = interrupted.over[0].expect("the fade it interrupted is stopped under it");
assert!(
(over.sampled.at - 0.1).abs() < 1e-6 && (over.weight - 0.5).abs() < 1e-5,
"{over:?} is not the second clip at the weight the fade had reached"
);
assert_eq!(interrupted.over[1].map(|faded| faded.weight), Some(0.0));
let later = posing(&animator, 0.3);
assert_eq!(
(started(later), later.over[0]),
(started(interrupted), interrupted.over[0]),
"and what was stopped is read the same a fade later"
);
let entering = later.over[1].expect("the state it entered fades in over that");
assert!(
(entering.weight - 0.5).abs() < 1e-5,
"{entering:?} is not half of the way into the fade that interrupted"
);
}
#[test]
fn a_fade_into_a_blend_takes_two_places_of_the_pose_and_reads_as_one_fade() {
let mut animator = machine::<Into>();
animate(&mut animator, Told::default(), 0.0);
animate(&mut animator, go(), 0.0);
let half = posing(&animator, 0.1);
let over = |at: usize| half.over[at].map(|faded| faded.weight);
assert!(
over(0).is_some_and(|weight| (weight - 1.0 / 3.0).abs() < 1e-5),
"{half:?} does not read as half of what it left"
);
assert!(
over(1).is_some_and(|weight| (weight - 0.25).abs() < 1e-5),
"{half:?} does not read as a quarter of the second of the pair"
);
let whole = posing(&animator, 0.2);
assert_eq!(
(
whole.over[0].map(|faded| faded.weight),
whole.over[1].map(|faded| faded.weight)
),
(Some(1.0), Some(0.5)),
"and at the end of the fade the clip it left is gone and the pair \
is read at its own weight"
);
}
#[test]
fn a_scrubbed_motion_follows_the_value_it_is_given_and_no_clock() {
let mut animator = machine::<Opening>();
animate(&mut animator, open(0.25), 0.0);
assert_eq!(started(posing(&animator, 0.0)), 0.25);
assert_eq!(
started(posing(&animator, 9.0)),
0.25,
"which no clock moves"
);
animate(&mut animator, open(1.0), 1.0);
assert_eq!(started(posing(&animator, 1.0)), 1.0);
animate(&mut animator, open(4.0), 2.0);
assert_eq!(
started(posing(&animator, 2.0)),
1.0,
"a fraction past the end is held inside the clip"
);
}
#[test]
fn a_blend_keeps_its_place_across_a_change_of_weight() {
let mut animator = machine::<Mixing>();
animate(&mut animator, mix(0.0, 1.0), 0.0);
let before = posing(&animator, 0.3);
animate(&mut animator, mix(1.0, 1.0), 0.3);
let after = posing(&animator, 0.3);
assert_eq!(
(started(before), blended(before)),
(started(after), blended(after)),
"both clips are read where they already were"
);
assert_eq!(weight(after), Some(1.0), "at the weight it was given");
assert!(
(started(posing(&animator, 0.55)) - 0.3).abs() < 1e-6,
"and a cycle of the clip it now reads runs on from there"
);
}
#[test]
fn a_paced_motion_keeps_its_position_across_a_change_of_pace() {
let mut animator = machine::<Mixing>();
animate(&mut animator, mix(0.0, 1.0), 0.0);
let before = started(posing(&animator, 0.5));
animate(&mut animator, mix(0.0, 2.0), 0.5);
assert_eq!(started(posing(&animator, 0.5)), before);
assert!(
(started(posing(&animator, 0.6)) - 0.7).abs() < 1e-6,
"and runs on at twice the pace"
);
}
#[test]
fn a_weight_or_a_pace_outside_what_a_motion_takes_is_held_inside_it() {
let mut animator = machine::<Mixing>();
animate(&mut animator, mix(4.0, 1.0), 0.0);
assert_eq!(weight(posing(&animator, 0.0)), Some(1.0));
animate(&mut animator, mix(f32::NAN, 1.0), 0.1);
assert_eq!(
weight(posing(&animator, 0.1)),
Some(0.0),
"and a weight that is no number reads as none of the second clip"
);
let mut held = machine::<Mixing>();
animate(&mut held, mix(0.0, -2.0), 0.0);
let standing = started(posing(&held, 0.0));
animate(&mut held, mix(0.0, -2.0), 1.0);
assert_eq!(
started(posing(&held, 1.0)),
standing,
"a pace of nothing or less holds a motion where it is"
);
}
#[test]
fn holding_a_machine_leaves_what_it_plays_where_it_is_until_it_resumes() {
let mut animator = machine::<Only>();
animate(&mut animator, Told::default(), 0.0);
animate(&mut animator, Told::default(), 0.5);
let held = started(posing(&animator, 0.5));
animator.hold();
animate(&mut animator, Told::default(), 2.0);
assert_eq!(
started(posing(&animator, 2.0)),
held,
"the span it was held over counts for nothing"
);
animator.resume();
animate(&mut animator, Told::default(), 2.25);
assert_eq!(
started(posing(&animator, 2.25)),
held,
"it plays on from there"
);
animate(&mut animator, Told::default(), 2.5);
assert!((started(posing(&animator, 2.5)) - 0.75).abs() < 1e-6);
}
#[test]
fn a_held_machine_is_drawn_at_the_instant_it_was_held_whatever_instant_the_frame_draws_at() {
let mut animator = machine::<Walk>();
animate(&mut animator, Told::default(), 0.0);
animate(&mut animator, go(), 0.1);
let before = posing(&animator, 0.1);
animator.hold();
animate(&mut animator, go(), 0.15);
assert_eq!(
posing(&animator, 0.15),
before,
"the tick that holds it leaves the pose where the tick before found it"
);
assert_eq!(
posing(&animator, 0.158),
before,
"a frame half a tick past that tick reads the same pose"
);
assert_eq!(
posing(&animator, 0.166),
before,
"and so does one a whole tick past it, mid-fade"
);
}
#[test]
fn a_clock_that_goes_back_is_held_to_the_last_instant_the_machine_read() {
let mut animator = machine::<Only>();
animate(&mut animator, Told::default(), 0.0);
animate(&mut animator, Told::default(), 0.75);
animate(&mut animator, Told::default(), 0.25);
animator.hold();
animate(&mut animator, Told::default(), 1.0);
assert!(
(started(posing(&animator, 1.0)) - 0.75).abs() < 1e-6,
"the hold counted the span since the instant it had read, not one before it"
);
}
#[test]
fn two_machines_run_alike_read_alike() {
let mut one = machine::<Walk>();
let mut two = machine::<Walk>();
for tick in 0..48 {
let at = tick as f32 * TICK.as_secs_f32();
let told = Told {
go: tick > 3,
openness: 0.3,
..Told::default()
};
animate(&mut one, told, at);
animate(&mut two, told, at);
assert_eq!(one.state(), two.state());
assert_eq!(posing(&one, at), posing(&two, at));
assert_eq!(one.transitioning(), two.transitioning());
}
assert_eq!(
one.state(),
Walk::Done,
"and both ran the whole way through"
);
}
}