use std::time::Duration;
pub const IDLE_POLL: Duration = Duration::from_millis(100);
pub const FRAME: Duration = Duration::from_millis(80);
pub fn frame(elapsed: Duration, n: usize) -> usize {
if n < 2 {
return 0;
}
(elapsed.as_millis() / FRAME.as_millis()) as usize % n
}
pub fn until_next_frame(elapsed: Duration) -> Duration {
let into_window = (elapsed.as_millis() % FRAME.as_millis()) as u64;
FRAME - Duration::from_millis(into_window)
}
pub fn poll_timeout(animating: bool, elapsed: Duration) -> Duration {
if animating {
IDLE_POLL.min(until_next_frame(elapsed))
} else {
IDLE_POLL
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ms(n: u64) -> Duration {
Duration::from_millis(n)
}
#[test]
fn a_frame_is_stable_within_its_window() {
for e in [0, 1, 40, 79] {
assert_eq!(frame(ms(e), 10), 0, "{e}ms left the first frame");
}
for e in [80, 100, 159] {
assert_eq!(frame(ms(e), 10), 1, "{e}ms left the second frame");
}
}
#[test]
fn the_frame_advances_across_a_window_boundary() {
assert_ne!(frame(ms(79), 10), frame(ms(80), 10));
assert_ne!(frame(ms(159), 10), frame(ms(160), 10));
}
#[test]
fn the_cycle_returns_to_its_first_frame() {
for n in [4usize, 6, 10] {
let full = 80 * n as u64;
assert_eq!(frame(ms(full), n), 0, "{n} frames did not wrap to 0");
assert_eq!(frame(ms(full + 80), n), 1, "{n} frames drifted after wrap");
let seen: Vec<usize> = (0..n).map(|i| frame(ms(i as u64 * 80), n)).collect();
assert_eq!(seen, (0..n).collect::<Vec<_>>(), "{n} frames out of order");
}
}
#[test]
fn a_tier_with_nothing_to_animate_stays_on_its_only_frame() {
for e in [0u64, 80, 1000, 99_999] {
assert_eq!(frame(ms(e), 1), 0, "{e}ms moved a one-frame tier");
assert_eq!(frame(ms(e), 0), 0, "{e}ms divided by zero");
}
}
#[test]
fn the_poll_timeout_is_untouched_when_nothing_is_in_progress() {
for e in [0, 1, 350, 699, 700, 1399, 1400, 9999] {
assert_eq!(
poll_timeout(false, ms(e)),
IDLE_POLL,
"{e}ms changed the idle timeout"
);
}
}
#[test]
fn the_poll_timeout_is_clamped_to_the_next_frame_while_something_is_in_progress() {
assert_eq!(poll_timeout(true, ms(0)), ms(80), "did not wake for the frame");
assert_eq!(poll_timeout(true, ms(60)), ms(20), "woke late for the frame");
assert_eq!(poll_timeout(true, ms(1340)), ms(20), "clamp stopped working");
for e in [0, 100, 350, 600, 700, 1234] {
assert!(
poll_timeout(true, ms(e)) <= IDLE_POLL,
"{e}ms lengthened the timeout"
);
}
}
#[test]
fn animation_never_makes_the_loop_spin() {
for e in 0..1400u64 {
assert!(poll_timeout(true, ms(e)) > Duration::ZERO, "{e}ms");
let next = until_next_frame(ms(e));
assert!(next > Duration::ZERO && next <= FRAME, "{e}ms -> {next:?}");
}
}
}