#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TimelineConfig {
pub start_frame: u32,
pub end_frame: u32,
pub fps: f32,
pub loop_playback: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlaybackState {
Stopped,
Playing,
Paused,
Scrubbing,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TimelineScrubber {
pub config: TimelineConfig,
pub current_frame: u32,
pub time_accumulator: f32,
pub state: PlaybackState,
}
#[allow(dead_code)]
pub fn default_timeline_config() -> TimelineConfig {
TimelineConfig {
start_frame: 0,
end_frame: 239,
fps: 24.0,
loop_playback: true,
}
}
#[allow(dead_code)]
pub fn new_timeline_scrubber(cfg: &TimelineConfig) -> TimelineScrubber {
TimelineScrubber {
current_frame: cfg.start_frame,
config: cfg.clone(),
time_accumulator: 0.0,
state: PlaybackState::Stopped,
}
}
#[allow(dead_code)]
pub fn timeline_set_frame(scrubber: &mut TimelineScrubber, frame: u32) {
scrubber.current_frame = frame.clamp(
scrubber.config.start_frame,
scrubber.config.end_frame,
);
scrubber.time_accumulator = 0.0;
}
#[allow(dead_code)]
pub fn timeline_advance(scrubber: &mut TimelineScrubber, dt: f32) {
if scrubber.state != PlaybackState::Playing {
return;
}
if scrubber.config.fps <= 0.0 {
return;
}
scrubber.time_accumulator += dt;
let frame_duration = 1.0 / scrubber.config.fps;
while scrubber.time_accumulator >= frame_duration {
scrubber.time_accumulator -= frame_duration;
if scrubber.current_frame < scrubber.config.end_frame {
scrubber.current_frame += 1;
} else if scrubber.config.loop_playback {
scrubber.current_frame = scrubber.config.start_frame;
} else {
scrubber.state = PlaybackState::Stopped;
break;
}
}
}
#[allow(dead_code)]
pub fn timeline_play(scrubber: &mut TimelineScrubber) {
scrubber.state = PlaybackState::Playing;
}
#[allow(dead_code)]
pub fn timeline_pause(scrubber: &mut TimelineScrubber) {
scrubber.state = PlaybackState::Paused;
}
#[allow(dead_code)]
pub fn timeline_stop(scrubber: &mut TimelineScrubber) {
scrubber.state = PlaybackState::Stopped;
scrubber.current_frame = scrubber.config.start_frame;
scrubber.time_accumulator = 0.0;
}
#[allow(dead_code)]
pub fn timeline_current_frame(scrubber: &TimelineScrubber) -> u32 {
scrubber.current_frame
}
#[allow(dead_code)]
pub fn timeline_playback_state(scrubber: &TimelineScrubber) -> PlaybackState {
scrubber.state
}
#[allow(dead_code)]
pub fn timeline_normalized_position(scrubber: &TimelineScrubber) -> f32 {
let start = scrubber.config.start_frame as f32;
let end = scrubber.config.end_frame as f32;
let range = end - start;
if range <= 0.0 {
return 0.0;
}
((scrubber.current_frame as f32) - start) / range
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let cfg = default_timeline_config();
assert_eq!(cfg.start_frame, 0);
assert_eq!(cfg.end_frame, 239);
assert!((cfg.fps - 24.0).abs() < 1e-5);
assert!(cfg.loop_playback);
}
#[test]
fn test_new_scrubber_initial_state() {
let cfg = default_timeline_config();
let scrubber = new_timeline_scrubber(&cfg);
assert_eq!(timeline_current_frame(&scrubber), 0);
assert_eq!(timeline_playback_state(&scrubber), PlaybackState::Stopped);
}
#[test]
fn test_set_frame_clamped() {
let cfg = default_timeline_config();
let mut scrubber = new_timeline_scrubber(&cfg);
timeline_set_frame(&mut scrubber, 9999);
assert_eq!(timeline_current_frame(&scrubber), 239);
timeline_set_frame(&mut scrubber, 0);
assert_eq!(timeline_current_frame(&scrubber), 0);
}
#[test]
fn test_play_pause_stop() {
let cfg = default_timeline_config();
let mut scrubber = new_timeline_scrubber(&cfg);
timeline_play(&mut scrubber);
assert_eq!(timeline_playback_state(&scrubber), PlaybackState::Playing);
timeline_pause(&mut scrubber);
assert_eq!(timeline_playback_state(&scrubber), PlaybackState::Paused);
timeline_stop(&mut scrubber);
assert_eq!(timeline_playback_state(&scrubber), PlaybackState::Stopped);
assert_eq!(timeline_current_frame(&scrubber), 0);
}
#[test]
fn test_advance_one_frame() {
let cfg = default_timeline_config(); let mut scrubber = new_timeline_scrubber(&cfg);
timeline_play(&mut scrubber);
timeline_advance(&mut scrubber, 1.0 / 24.0);
assert_eq!(timeline_current_frame(&scrubber), 1);
}
#[test]
fn test_advance_no_change_when_stopped() {
let cfg = default_timeline_config();
let mut scrubber = new_timeline_scrubber(&cfg);
timeline_advance(&mut scrubber, 10.0);
assert_eq!(timeline_current_frame(&scrubber), 0);
}
#[test]
fn test_normalized_position_at_start() {
let cfg = default_timeline_config();
let scrubber = new_timeline_scrubber(&cfg);
assert!((timeline_normalized_position(&scrubber) - 0.0).abs() < 1e-5);
}
#[test]
fn test_normalized_position_at_end() {
let cfg = default_timeline_config();
let mut scrubber = new_timeline_scrubber(&cfg);
timeline_set_frame(&mut scrubber, 239);
let pos = timeline_normalized_position(&scrubber);
assert!((pos - 1.0).abs() < 1e-5);
}
#[test]
fn test_loop_playback() {
let mut cfg = default_timeline_config();
cfg.start_frame = 0;
cfg.end_frame = 2;
cfg.fps = 1.0;
cfg.loop_playback = true;
let mut scrubber = new_timeline_scrubber(&cfg);
timeline_play(&mut scrubber);
timeline_advance(&mut scrubber, 4.0);
assert_eq!(timeline_current_frame(&scrubber), 1);
}
#[test]
fn test_stop_no_loop() {
let mut cfg = default_timeline_config();
cfg.start_frame = 0;
cfg.end_frame = 1;
cfg.fps = 1.0;
cfg.loop_playback = false;
let mut scrubber = new_timeline_scrubber(&cfg);
timeline_play(&mut scrubber);
timeline_advance(&mut scrubber, 5.0);
assert_eq!(timeline_playback_state(&scrubber), PlaybackState::Stopped);
}
}