use core::time::Duration;
#[must_use]
pub fn oscillate(elapsed: Duration, period: Duration) -> f32 {
oscillate_with_phase(elapsed, period, 0.0)
}
#[must_use]
pub fn oscillate_with_phase(elapsed: Duration, period: Duration, phase: f32) -> f32 {
if period.is_zero() {
return 0.5;
}
let cycles = elapsed.as_secs_f32() / period.as_secs_f32() + phase; let radians = cycles * 2.0 * core::f32::consts::PI;
retroglyph_core::math::mul_add(0.5, retroglyph_core::math::sin(radians), 0.5)
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn oscillate_at_zero_elapsed_is_the_midpoint() {
assert_eq!(oscillate(Duration::ZERO, Duration::from_secs(1)), 0.5);
}
#[test]
fn oscillate_completes_a_full_cycle_after_one_period() {
let period = Duration::from_secs(4);
let start = oscillate(Duration::ZERO, period);
let one_cycle_later = oscillate(period, period);
assert!((start - one_cycle_later).abs() < 1e-5);
}
#[test]
fn oscillate_stays_within_0_and_1() {
let period = Duration::from_secs(1);
for ms in 0..2000u64 {
let v = oscillate(Duration::from_millis(ms), period);
assert!((0.0..=1.0).contains(&v), "{v} out of range at {ms}ms");
}
}
#[test]
fn zero_period_is_a_defined_constant_not_a_panic() {
assert_eq!(oscillate(Duration::from_secs(1), Duration::ZERO), 0.5);
}
#[test]
fn zero_phase_matches_plain_oscillate() {
let period = Duration::from_secs(1);
for ms in 0..2000u64 {
let elapsed = Duration::from_millis(ms);
assert_eq!(
oscillate_with_phase(elapsed, period, 0.0),
oscillate(elapsed, period)
);
}
}
#[test]
fn phase_offset_shifts_the_sampled_point_in_the_cycle() {
let period = Duration::from_secs(2);
let a = oscillate_with_phase(Duration::ZERO, period, 0.25);
let b = oscillate_with_phase(Duration::from_millis(500), period, 0.0);
assert!((a - b).abs() < 1e-6);
}
#[test]
fn phase_offset_by_a_whole_number_of_cycles_is_no_offset() {
let period = Duration::from_secs(1);
let elapsed = Duration::from_millis(137);
let a = oscillate_with_phase(elapsed, period, 0.0);
let b = oscillate_with_phase(elapsed, period, 3.0);
assert!((a - b).abs() < 1e-5);
}
#[test]
fn negative_phase_is_defined() {
let period = Duration::from_secs(2);
let a = oscillate_with_phase(Duration::from_millis(500), period, -0.25);
let b = oscillate_with_phase(Duration::ZERO, period, 0.0);
assert!((a - b).abs() < 1e-6);
}
#[test]
fn phase_offset_stays_within_0_and_1() {
let period = Duration::from_secs(1);
for ms in 0..2000u64 {
let v = oscillate_with_phase(Duration::from_millis(ms), period, 0.37);
assert!((0.0..=1.0).contains(&v), "{v} out of range at {ms}ms");
}
}
#[test]
fn zero_period_with_phase_is_a_defined_constant_not_a_panic() {
assert_eq!(
oscillate_with_phase(Duration::from_secs(1), Duration::ZERO, 0.25),
0.5
);
}
}