use crate::random::Rng;
#[derive(Debug, Clone, PartialEq)]
pub enum SequenceStep {
PlayFrames {
frames: Vec<usize>,
frame_duration: f32,
},
Pause(f32),
RandomPause {
min: f32,
max: f32,
},
Action(u32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SequenceLoop {
Once,
Repeat,
PingPongRepeat,
PingPongOnce,
}
#[derive(Debug, Clone)]
pub struct AnimationSequence {
steps: Vec<SequenceStep>,
cursors: Vec<usize>,
held_frame: Option<usize>,
loop_mode: SequenceLoop,
step: usize,
timer: f32,
step_duration: Option<f32>,
forward: bool,
actions: std::collections::VecDeque<u32>,
finished: bool,
just_completed: bool,
}
impl AnimationSequence {
#[must_use]
pub fn builder() -> SequenceBuilder {
SequenceBuilder::new()
}
pub fn tick(&mut self, dt: f32, rng: &mut Rng) -> bool {
self.just_completed = false;
self.actions.clear();
if self.finished {
return false;
}
self.timer += dt;
let mut idle_advances = 0;
loop {
let duration = self.resolved_duration(rng);
if self.timer < duration {
return false;
}
if duration > 0.0 {
idle_advances = 0;
self.timer -= duration;
} else {
idle_advances += 1;
if idle_advances > self.steps.len() {
self.timer = 0.0;
return false;
}
}
if self.advance() {
self.finished = true;
self.just_completed = true;
self.timer = 0.0;
return true;
}
}
}
#[must_use]
pub fn current_frame(&self) -> Option<usize> {
match self.steps.get(self.step) {
Some(SequenceStep::PlayFrames { frames, .. }) => {
frames.get(self.cursors[self.step]).copied()
}
_ => self.held_frame,
}
}
#[must_use]
pub fn just_completed(&self) -> bool {
self.just_completed
}
#[must_use]
pub fn is_finished(&self) -> bool {
self.finished
}
pub fn take_action(&mut self) -> Option<u32> {
self.actions.pop_front()
}
pub fn reset(&mut self) {
self.step = 0;
self.timer = 0.0;
self.step_duration = None;
self.forward = true;
self.finished = false;
self.just_completed = false;
self.actions.clear();
self.cursors.fill(0);
self.held_frame = None;
}
fn resolved_duration(&mut self, rng: &mut Rng) -> f32 {
if let Some(duration) = self.step_duration {
return duration;
}
let duration = match &self.steps[self.step] {
SequenceStep::PlayFrames { frame_duration, .. } => *frame_duration,
SequenceStep::Pause(duration) => *duration,
SequenceStep::RandomPause { min, max } => min + rng.f32() * (max - min),
SequenceStep::Action(_) => 0.0,
};
self.step_duration = Some(duration);
duration
}
fn advance(&mut self) -> bool {
let cursor = self.cursors[self.step];
let next_frame = match &self.steps[self.step] {
SequenceStep::PlayFrames { frames, .. } => {
self.held_frame = Some(frames[cursor]);
if self.forward {
(cursor + 1 < frames.len()).then_some(cursor + 1)
} else {
cursor.checked_sub(1)
}
}
_ => None,
};
if let Some(cursor) = next_frame {
self.cursors[self.step] = cursor;
self.step_duration = None;
return false;
}
if let SequenceStep::Action(id) = &self.steps[self.step] {
self.actions.push_back(*id);
}
self.next_step()
}
fn next_step(&mut self) -> bool {
let previous_step = self.step;
let previous_forward = self.forward;
match self.loop_mode {
SequenceLoop::Once => {
self.step += 1;
if self.step >= self.steps.len() {
return true;
}
}
SequenceLoop::Repeat => {
self.step += 1;
if self.step >= self.steps.len() {
self.step = 0;
self.cursors.fill(0);
}
}
SequenceLoop::PingPongRepeat | SequenceLoop::PingPongOnce => {
if self.forward {
self.step += 1;
if self.step >= self.steps.len() {
self.step = self.steps.len().saturating_sub(2);
self.forward = false;
}
} else if self.step > 0 {
self.step -= 1;
} else if self.loop_mode == SequenceLoop::PingPongOnce {
return true;
} else {
self.step = usize::from(self.steps.len() > 1);
self.forward = true;
self.cursors.fill(0);
}
}
}
if self.step != previous_step || self.forward != previous_forward {
self.enter_step(previous_forward);
}
self.step_duration = None;
false
}
fn enter_step(&mut self, previous_forward: bool) {
let single_step = self.steps.len() == 1;
let forward = self.forward;
let SequenceStep::PlayFrames { frames, .. } = &self.steps[self.step] else {
return;
};
let len = frames.len();
self.cursors[self.step] = if forward {
usize::from(!previous_forward && single_step && len > 1)
} else if previous_forward && single_step && len > 1 {
len - 2
} else {
len - 1
};
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SequenceError {
NoSteps,
EmptyFrames { step: usize },
BadDuration { step: usize },
BadRandomRange { step: usize },
}
impl std::fmt::Display for SequenceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoSteps => write!(f, "animation sequence has no steps"),
Self::EmptyFrames { step } => {
write!(f, "animation sequence step {step} plays no frames")
}
Self::BadDuration { step } => write!(
f,
"animation sequence step {step} needs a finite, non-negative duration"
),
Self::BadRandomRange { step } => write!(
f,
"animation sequence step {step} needs a finite random pause range with min <= max"
),
}
}
}
impl std::error::Error for SequenceError {}
#[derive(Debug, Clone)]
pub struct SequenceBuilder {
steps: Vec<SequenceStep>,
loop_mode: SequenceLoop,
}
impl SequenceBuilder {
fn new() -> Self {
Self {
steps: Vec::new(),
loop_mode: SequenceLoop::Once,
}
}
#[must_use]
pub fn loop_mode(mut self, loop_mode: SequenceLoop) -> Self {
self.loop_mode = loop_mode;
self
}
#[must_use]
pub fn play_frames(mut self, frames: impl Into<Vec<usize>>, frame_duration: f32) -> Self {
self.steps.push(SequenceStep::PlayFrames {
frames: frames.into(),
frame_duration,
});
self
}
#[must_use]
pub fn play_frame_range(self, first: usize, last: usize, frame_duration: f32) -> Self {
let frames: Vec<usize> = if first <= last {
(first..=last).collect()
} else {
Vec::new()
};
self.play_frames(frames, frame_duration)
}
#[must_use]
pub fn pause(mut self, duration: f32) -> Self {
self.steps.push(SequenceStep::Pause(duration));
self
}
#[must_use]
pub fn random_pause(mut self, min: f32, max: f32) -> Self {
self.steps.push(SequenceStep::RandomPause { min, max });
self
}
#[must_use]
pub fn action(mut self, id: u32) -> Self {
self.steps.push(SequenceStep::Action(id));
self
}
pub fn build(self) -> Result<AnimationSequence, SequenceError> {
if self.steps.is_empty() {
return Err(SequenceError::NoSteps);
}
for (step, item) in self.steps.iter().enumerate() {
match item {
SequenceStep::PlayFrames {
frames,
frame_duration,
} => {
if frames.is_empty() {
return Err(SequenceError::EmptyFrames { step });
}
if !frame_duration.is_finite() || *frame_duration < 0.0 {
return Err(SequenceError::BadDuration { step });
}
}
SequenceStep::Pause(duration) => {
if !duration.is_finite() || *duration < 0.0 {
return Err(SequenceError::BadDuration { step });
}
}
SequenceStep::RandomPause { min, max } => {
if !min.is_finite() || !max.is_finite() || *min < 0.0 || max < min {
return Err(SequenceError::BadRandomRange { step });
}
}
SequenceStep::Action(_) => {}
}
}
Ok(AnimationSequence {
cursors: vec![0; self.steps.len()],
held_frame: None,
steps: self.steps,
loop_mode: self.loop_mode,
step: 0,
timer: 0.0,
step_duration: None,
forward: true,
actions: std::collections::VecDeque::new(),
finished: false,
just_completed: false,
})
}
}
impl Default for SequenceBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::wasm_bindgen_test;
#[wasm_bindgen_test(unsupported = test)]
fn a_tick_over_zero_duration_steps_makes_one_full_pass() {
let mut sequence = AnimationSequence::builder()
.loop_mode(SequenceLoop::Repeat)
.action(1)
.action(2)
.build()
.unwrap();
let mut rng = rng();
for _ in 0..2 {
sequence.tick(0.5, &mut rng);
let mut fired = Vec::new();
while let Some(action) = sequence.take_action() {
fired.push(action);
}
assert_eq!(fired.len(), 2, "one action per step per tick: {fired:?}");
}
}
#[wasm_bindgen_test(unsupported = test)]
fn two_step_pingpong_repeat_restarts_one_step_in() {
let mut sequence = AnimationSequence::builder()
.loop_mode(SequenceLoop::PingPongRepeat)
.play_frames(vec![0, 1], 0.1)
.play_frames(vec![2, 3], 0.1)
.build()
.unwrap();
let mut rng = rng();
let mut seen = vec![sequence.current_frame()];
for _ in 0..9 {
sequence.tick(0.1, &mut rng);
seen.push(sequence.current_frame());
}
assert_eq!(seen, [0, 1, 2, 3, 1, 0, 2, 3, 1, 0].map(Some).to_vec());
}
#[wasm_bindgen_test(unsupported = test)]
fn a_remainder_carries_into_the_next_tick() {
let mut sequence = AnimationSequence::builder()
.loop_mode(SequenceLoop::Repeat)
.play_frames(vec![0, 1, 2, 3, 4], 0.1)
.build()
.unwrap();
let mut rng = rng();
sequence.tick(0.25, &mut rng);
assert_eq!(sequence.current_frame(), Some(2));
sequence.tick(0.05, &mut rng);
assert_eq!(
sequence.current_frame(),
Some(3),
"0.05 s remainder carried"
);
}
#[wasm_bindgen_test(unsupported = test)]
fn two_step_pingpong_enters_reversed_steps_at_their_last_frame() {
let mut sequence = AnimationSequence::builder()
.loop_mode(SequenceLoop::PingPongOnce)
.play_frames(vec![0, 1], 0.1)
.play_frames(vec![2, 3], 0.1)
.build()
.unwrap();
let mut rng = rng();
let mut seen = vec![sequence.current_frame()];
for _ in 0..6 {
sequence.tick(0.1, &mut rng);
seen.push(sequence.current_frame());
}
assert_eq!(
seen,
vec![
Some(0),
Some(1),
Some(2),
Some(3),
Some(1),
Some(0),
Some(0)
]
);
assert!(sequence.is_finished());
}
#[wasm_bindgen_test(unsupported = test)]
fn a_single_one_frame_step_pingpongs_without_panicking() {
let mut sequence = AnimationSequence::builder()
.loop_mode(SequenceLoop::PingPongOnce)
.play_frames(vec![7], 0.1)
.build()
.unwrap();
let mut rng = rng();
for _ in 0..4 {
sequence.tick(0.1, &mut rng);
assert!(matches!(sequence.current_frame(), Some(7) | None));
}
assert!(sequence.is_finished());
}
#[wasm_bindgen_test(unsupported = test)]
fn the_builder_accepts_zero_durations_and_equal_random_bounds() {
assert!(
AnimationSequence::builder()
.play_frames(vec![0], 0.0)
.random_pause(0.5, 0.5)
.build()
.is_ok()
);
assert_eq!(
AnimationSequence::builder()
.random_pause(0.6, 0.5)
.build()
.err(),
Some(SequenceError::BadRandomRange { step: 0 })
);
}
fn rng() -> Rng {
Rng::from_seed(1)
}
fn play(seq: &mut AnimationSequence, dt: f32, ticks: usize) -> Vec<Option<usize>> {
let mut rng = rng();
let mut seen = vec![seq.current_frame()];
for _ in 0..ticks {
if seq.tick(dt, &mut rng) {
break;
}
seen.push(seq.current_frame());
}
seen
}
fn single(frames: [usize; 3], loop_mode: SequenceLoop) -> AnimationSequence {
AnimationSequence::builder()
.loop_mode(loop_mode)
.play_frames(frames, 0.1)
.build()
.expect("valid sequence")
}
#[wasm_bindgen_test(unsupported = test)]
fn single_step_pingpong_once_plays_up_and_back() {
let mut seq = single([0, 1, 2], SequenceLoop::PingPongOnce);
assert_eq!(
play(&mut seq, 0.1, 10),
vec![Some(0), Some(1), Some(2), Some(1), Some(0)]
);
assert!(seq.is_finished() && seq.just_completed());
let mut rng = rng();
assert!(!seq.tick(0.1, &mut rng), "the completion edge fires once");
assert!(!seq.just_completed());
}
#[wasm_bindgen_test(unsupported = test)]
fn single_step_pingpong_repeat_bounces_forever() {
let mut seq = single([0, 1, 2], SequenceLoop::PingPongRepeat);
assert_eq!(
play(&mut seq, 0.1, 8),
vec![
Some(0),
Some(1),
Some(2),
Some(1),
Some(0),
Some(1),
Some(2),
Some(1),
Some(0),
]
);
assert!(!seq.is_finished());
}
#[wasm_bindgen_test(unsupported = test)]
fn multi_step_pingpong_reverses_step_order() {
let mut seq = AnimationSequence::builder()
.loop_mode(SequenceLoop::PingPongOnce)
.play_frames([0, 1], 0.1)
.play_frames([2, 3], 0.1)
.play_frames([4, 5], 0.1)
.build()
.expect("valid sequence");
assert_eq!(
play(&mut seq, 0.1, 20),
[0, 1, 2, 3, 4, 5, 3, 2, 1, 0].map(Some).to_vec()
);
assert!(seq.just_completed());
}
#[wasm_bindgen_test(unsupported = test)]
fn repeat_rewinds_every_step_on_wrap() {
let mut seq = AnimationSequence::builder()
.loop_mode(SequenceLoop::Repeat)
.play_frames([0, 1], 0.1)
.play_frames([7, 8], 0.1)
.build()
.expect("valid sequence");
assert_eq!(
play(&mut seq, 0.1, 8),
[0, 1, 7, 8, 0, 1, 7, 8, 0].map(Some).to_vec()
);
assert!(!seq.is_finished());
}
#[wasm_bindgen_test(unsupported = test)]
fn large_dt_steps_several_frames_and_carries_the_remainder() {
let mut seq = AnimationSequence::builder()
.loop_mode(SequenceLoop::Repeat)
.play_frames([0, 1, 2, 3], 0.1)
.build()
.expect("valid sequence");
let mut rng = rng();
assert!(!seq.tick(0.35, &mut rng));
assert_eq!(seq.current_frame(), Some(3));
assert!(!seq.tick(0.05, &mut rng));
assert_eq!(seq.current_frame(), Some(0));
}
#[wasm_bindgen_test(unsupported = test)]
fn leftover_time_carries_across_steps() {
let mut seq = AnimationSequence::builder()
.play_frames([0], 0.1)
.pause(0.1)
.play_frames([5, 6], 0.1)
.build()
.expect("valid sequence");
let mut rng = rng();
assert!(!seq.tick(0.25, &mut rng));
assert_eq!(seq.current_frame(), Some(5));
assert!(!seq.tick(0.05, &mut rng));
assert_eq!(seq.current_frame(), Some(6));
}
#[wasm_bindgen_test(unsupported = test)]
fn large_ticks_hold_the_last_frame_through_pauses() {
let frames = AnimationSequence::builder().play_frames([0, 1], 0.125);
for builder in [frames.clone().pause(1.0), frames.random_pause(1.0, 2.0)] {
for schedule in [&[0.25][..], &[0.125, 0.125][..]] {
let mut seq = builder.clone().build().unwrap();
let mut rng = rng();
for dt in schedule {
assert!(!seq.tick(*dt, &mut rng));
}
assert_eq!(seq.current_frame(), Some(1), "entered the pause");
assert!(!seq.tick(0.5, &mut rng));
assert_eq!(seq.current_frame(), Some(1), "still in the pause");
}
}
}
#[wasm_bindgen_test(unsupported = test)]
fn completing_tick_holds_the_last_traversed_frame() {
for (loop_mode, endpoint) in [(SequenceLoop::Once, 5), (SequenceLoop::PingPongOnce, 3)] {
let mut seq = AnimationSequence::builder()
.loop_mode(loop_mode)
.play_frames([3, 4, 5], 0.125)
.build()
.unwrap();
let mut rng = rng();
assert!(seq.tick(1.0, &mut rng));
assert_eq!(seq.current_frame(), Some(endpoint));
assert!(!seq.tick(1.0, &mut rng));
assert_eq!(seq.current_frame(), Some(endpoint));
}
}
#[wasm_bindgen_test(unsupported = test)]
fn reversed_pause_holds_the_frame_at_the_turnaround() {
let mut seq = AnimationSequence::builder()
.loop_mode(SequenceLoop::PingPongOnce)
.play_frames([0, 1], 0.125)
.pause(0.5)
.play_frames([2, 3], 0.125)
.build()
.unwrap();
let mut rng = rng();
assert!(!seq.tick(1.0, &mut rng));
assert_eq!(seq.current_frame(), Some(3), "reverse pause at 1.0 s");
assert!(seq.tick(0.75, &mut rng));
assert_eq!(seq.current_frame(), Some(0), "reverse run ends at 1.75 s");
}
#[wasm_bindgen_test(unsupported = test)]
fn reset_clears_the_held_frame_before_a_leading_pause() {
let mut seq = AnimationSequence::builder()
.pause(0.25)
.play_frames([4, 7], 0.125)
.build()
.unwrap();
let mut rng = rng();
assert!(seq.tick(0.5, &mut rng));
assert_eq!(seq.current_frame(), Some(7));
seq.reset();
assert!(!seq.tick(0.125, &mut rng));
assert_eq!(seq.current_frame(), None, "no frame reached after reset");
assert!(!seq.tick(0.125, &mut rng));
assert_eq!(seq.current_frame(), Some(4), "first frame reached again");
}
#[wasm_bindgen_test(unsupported = test)]
fn zero_duration_steps_cannot_spin_within_a_tick() {
let mut seq = AnimationSequence::builder()
.loop_mode(SequenceLoop::Repeat)
.action(3)
.pause(0.0)
.random_pause(0.0, 0.0)
.build()
.expect("valid sequence");
let mut rng = rng();
assert!(!seq.tick(60.0, &mut rng), "Repeat never completes");
assert_eq!(seq.take_action(), Some(3));
assert_eq!(seq.take_action(), None, "one pass fires one action");
assert!(!seq.tick(60.0, &mut rng));
}
#[wasm_bindgen_test(unsupported = test)]
fn actions_queue_in_order_and_drain_once() {
let mut seq = AnimationSequence::builder()
.play_frames([0], 0.1)
.action(7)
.action(9)
.play_frames([1], 0.1)
.build()
.expect("valid sequence");
let mut rng = rng();
assert_eq!(
seq.take_action(),
None,
"nothing fires before the first tick"
);
assert!(!seq.tick(0.1, &mut rng));
assert_eq!(seq.current_frame(), Some(1), "both actions were crossed");
assert_eq!(seq.take_action(), Some(7));
assert_eq!(seq.take_action(), Some(9));
assert_eq!(seq.take_action(), None);
assert!(seq.tick(0.1, &mut rng), "the last frame completes the run");
assert_eq!(seq.take_action(), None, "actions do not re-fire");
}
#[wasm_bindgen_test(unsupported = test)]
fn an_exact_random_pause_lasts_exactly_min() {
let mut seq = AnimationSequence::builder()
.play_frames([4], 0.125)
.random_pause(0.5, 0.5)
.build()
.expect("valid sequence");
let mut rng = rng();
assert!(!seq.tick(0.125, &mut rng), "off the frame, onto the pause");
assert_eq!(
seq.current_frame(),
Some(4),
"a pause holds its prior frame"
);
for tick in 1..4 {
assert!(
!seq.tick(0.125, &mut rng),
"still pausing after {tick} ticks"
);
}
assert!(seq.tick(0.125, &mut rng), "0.5 s of pause has elapsed");
}
#[wasm_bindgen_test(unsupported = test)]
fn random_pauses_are_bounded_and_seed_reproducible() {
let build = || {
AnimationSequence::builder()
.random_pause(0.4, 0.8)
.build()
.expect("valid sequence")
};
let ticks_to_finish = |seed: u64| {
let mut seq = build();
let mut rng = Rng::from_seed(seed);
(1..=100)
.find(|_| seq.tick(0.05, &mut rng))
.expect("the pause ends")
};
for seed in 0..32 {
let ticks = ticks_to_finish(seed);
assert!(
(8..=16).contains(&ticks),
"seed {seed} paused {ticks} ticks"
);
assert_eq!(ticks, ticks_to_finish(seed), "seed {seed} is reproducible");
}
}
#[wasm_bindgen_test(unsupported = test)]
fn steps_without_randomness_do_not_draw_from_the_rng() {
let mut seq = AnimationSequence::builder()
.loop_mode(SequenceLoop::Repeat)
.play_frames([0, 1], 0.1)
.pause(0.2)
.action(1)
.build()
.expect("valid sequence");
let mut used = Rng::from_seed(5);
for _ in 0..50 {
seq.tick(0.1, &mut used);
}
let mut untouched = Rng::from_seed(5);
assert_eq!(used.u32(0..u32::MAX), untouched.u32(0..u32::MAX));
}
#[wasm_bindgen_test(unsupported = test)]
fn reset_restarts_a_finished_sequence() {
let mut seq = single([3, 4, 5], SequenceLoop::Once);
let mut rng = rng();
while !seq.is_finished() {
seq.tick(0.1, &mut rng);
}
assert_eq!(seq.current_frame(), Some(5), "Once holds its last frame");
seq.reset();
assert!(!seq.is_finished() && !seq.just_completed());
assert_eq!(seq.current_frame(), Some(3));
assert!(!seq.tick(0.1, &mut rng));
assert_eq!(seq.current_frame(), Some(4));
}
#[wasm_bindgen_test(unsupported = test)]
fn the_builder_rejects_authoring_errors() {
let err = |builder: SequenceBuilder| builder.build().expect_err("authoring error");
assert_eq!(err(AnimationSequence::builder()), SequenceError::NoSteps);
assert_eq!(
err(AnimationSequence::builder().play_frames(Vec::new(), 0.1)),
SequenceError::EmptyFrames { step: 0 }
);
assert_eq!(
err(AnimationSequence::builder().play_frame_range(4, 2, 0.1)),
SequenceError::EmptyFrames { step: 0 },
"a backwards range plays nothing"
);
assert_eq!(
err(AnimationSequence::builder().play_frames([0], -0.1)),
SequenceError::BadDuration { step: 0 }
);
assert_eq!(
err(AnimationSequence::builder()
.play_frames([0], 0.1)
.pause(f32::NAN)),
SequenceError::BadDuration { step: 1 }
);
assert_eq!(
err(AnimationSequence::builder().pause(f32::INFINITY)),
SequenceError::BadDuration { step: 0 }
);
assert_eq!(
err(AnimationSequence::builder().random_pause(2.0, 1.0)),
SequenceError::BadRandomRange { step: 0 }
);
assert_eq!(
err(AnimationSequence::builder().random_pause(f32::NAN, 1.0)),
SequenceError::BadRandomRange { step: 0 }
);
assert!(
AnimationSequence::builder()
.random_pause(0.0, 0.0)
.build()
.is_ok(),
"a zero-length random pause is legitimate"
);
}
#[wasm_bindgen_test(unsupported = test)]
fn play_frame_range_is_inclusive() {
let mut seq = AnimationSequence::builder()
.loop_mode(SequenceLoop::Once)
.play_frame_range(8, 11, 0.1)
.build()
.expect("valid sequence");
assert_eq!(play(&mut seq, 0.1, 10), [8, 9, 10, 11].map(Some).to_vec());
}
}