mod sequence;
pub use sequence::{AnimationSequence, SequenceBuilder, SequenceError, SequenceLoop, SequenceStep};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopMode {
Once,
Repeat,
}
#[derive(Debug, Clone)]
pub struct FrameAnimation {
order: Vec<usize>,
frame_duration: f32,
loop_mode: LoopMode,
position: usize,
timer: f32,
finished: bool,
}
impl FrameAnimation {
pub fn new(order: impl Into<Vec<usize>>, fps: f32, loop_mode: LoopMode) -> Self {
let order = order.into();
assert!(!order.is_empty(), "FrameAnimation: empty frame order");
assert!(fps > 0.0, "FrameAnimation: fps must be positive, got {fps}");
Self {
order,
frame_duration: 1.0 / fps,
loop_mode,
position: 0,
timer: 0.0,
finished: false,
}
}
#[must_use]
pub fn start_at(mut self, position: usize) -> Self {
self.position = position.min(self.order.len() - 1);
self
}
pub fn tick(&mut self, dt: f32) -> bool {
if self.finished {
return false;
}
self.timer += dt;
while self.timer >= self.frame_duration {
self.timer -= self.frame_duration;
if self.position + 1 < self.order.len() {
self.position += 1;
} else {
match self.loop_mode {
LoopMode::Repeat => self.position = 0,
LoopMode::Once => {
self.finished = true;
self.timer = 0.0;
return true;
}
}
}
}
false
}
#[must_use]
pub fn current_frame(&self) -> usize {
self.order[self.position]
}
#[must_use]
pub fn position(&self) -> usize {
self.position
}
#[must_use]
pub fn is_finished(&self) -> bool {
self.finished
}
pub fn reset(&mut self) {
self.position = 0;
self.timer = 0.0;
self.finished = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::wasm_bindgen_test;
#[wasm_bindgen_test(unsupported = test)]
fn start_at_positions_the_cursor() {
let anim = FrameAnimation::new(vec![5, 6, 7, 8], 10.0, LoopMode::Repeat).start_at(2);
assert_eq!(anim.position(), 2);
assert_eq!(anim.current_frame(), 7);
let mut anim = anim;
anim.tick(0.1);
assert_eq!(anim.position(), 3);
let clamped = FrameAnimation::new(vec![5, 6], 10.0, LoopMode::Repeat).start_at(9);
assert_eq!(clamped.position(), 1);
}
#[wasm_bindgen_test(unsupported = test)]
fn repeat_order_cycles_at_exact_fps_boundaries() {
let mut a = FrameAnimation::new([0, 1, 2, 1], 10.0, LoopMode::Repeat);
let mut seen = vec![a.current_frame()];
for _ in 0..8 {
assert!(!a.tick(0.1), "Repeat never reports finished");
seen.push(a.current_frame());
}
assert_eq!(seen, vec![0, 1, 2, 1, 0, 1, 2, 1, 0]);
}
#[wasm_bindgen_test(unsupported = test)]
fn sub_boundary_tick_does_not_advance() {
let mut a = FrameAnimation::new([0, 1], 10.0, LoopMode::Repeat);
a.tick(0.099);
assert_eq!(a.current_frame(), 0, "0.099 s < 0.1 s: still frame 0");
a.tick(0.001);
assert_eq!(a.current_frame(), 1, "accumulated exactly 0.1 s: frame 1");
}
#[wasm_bindgen_test(unsupported = test)]
fn large_dt_steps_multiple_frames() {
let mut a = FrameAnimation::new([0, 1, 2, 1], 10.0, LoopMode::Repeat);
a.tick(0.25); assert_eq!(a.current_frame(), 2);
a.tick(0.05); assert_eq!(a.current_frame(), 1);
}
#[wasm_bindgen_test(unsupported = test)]
fn once_finishes_on_the_edge_and_holds() {
let mut a = FrameAnimation::new([0, 1, 2], 5.0, LoopMode::Once);
assert!(!a.tick(0.2));
assert_eq!(a.current_frame(), 1);
assert!(!a.tick(0.2));
assert_eq!(a.current_frame(), 2);
assert!(a.tick(0.2), "completion edge");
assert!(a.is_finished());
assert_eq!(a.current_frame(), 2, "holds the last frame");
assert!(!a.tick(1.0));
assert_eq!(a.current_frame(), 2);
}
#[wasm_bindgen_test(unsupported = test)]
fn chicken_starts_at_frame_two_and_follows_its_order() {
let mut a = FrameAnimation::new([0, 2, 1, 2], 20.0, LoopMode::Repeat).start_at(1);
let mut seen = vec![a.current_frame()];
for _ in 0..5 {
a.tick(0.05);
seen.push(a.current_frame());
}
assert_eq!(seen, vec![2, 1, 2, 0, 2, 1]);
}
#[wasm_bindgen_test(unsupported = test)]
fn reset_restarts_a_finished_animation() {
let mut a = FrameAnimation::new([3, 4], 10.0, LoopMode::Once);
a.tick(1.0);
assert!(a.is_finished());
a.reset();
assert!(!a.is_finished());
assert_eq!(a.current_frame(), 3);
assert!(!a.tick(0.1));
assert_eq!(a.current_frame(), 4);
}
}