use std::f64::consts::TAU;
use std::time::Duration;
use frust::{AnimationController, Curve, FrameTime};
use crate::tokens::MaterialMotion;
const CYCLE_MS: u64 = 1750;
pub(crate) const LINEAR_CYCLE: Duration = Duration::from_millis(CYCLE_MS);
pub(crate) const GLOBAL_ROTATION: Duration = Duration::from_millis(6000);
pub(crate) const ADDITIONAL_ROTATION: Duration = Duration::from_millis(4500);
pub(crate) const SWEEP: Duration = Duration::from_millis(1300);
pub(crate) const WAVE_PHASE: Duration = MaterialMotion::EXTRA_LONG_2;
pub(crate) const LINE_EASING: Curve = Curve::Cubic(0.3, 0.0, 0.8, 0.15);
pub(crate) const MIN_SWEEP: f64 = 0.10;
pub(crate) const MAX_SWEEP: f64 = 0.87;
pub(crate) const GLOBAL_ROT_DEG: f64 = 1080.0;
pub(crate) const ADDITIONAL_ROT_DEG: f64 = 360.0;
const FIRST_HEAD: (f64, f64) = (0.0, 1000.0);
const FIRST_TAIL: (f64, f64) = (250.0, 1000.0);
const SECOND_HEAD: (f64, f64) = (650.0, 850.0);
const SECOND_TAIL: (f64, f64) = (900.0, 850.0);
pub type AmplitudeCurve = std::rc::Rc<dyn Fn(f64) -> f64>;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Segments {
pub(crate) first_head: f64,
pub(crate) first_tail: f64,
pub(crate) second_head: f64,
pub(crate) second_tail: f64,
}
pub(crate) fn evaluate_segment(t: f64, delay_ms: f64, duration_ms: f64) -> f64 {
let total = CYCLE_MS as f64;
let start = delay_ms / total;
let end = (delay_ms + duration_ms) / total;
LINE_EASING.interval(start, end).transform(t)
}
pub(crate) fn segments(t: f64) -> Segments {
Segments {
first_head: evaluate_segment(t, FIRST_HEAD.0, FIRST_HEAD.1),
first_tail: evaluate_segment(t, FIRST_TAIL.0, FIRST_TAIL.1),
second_head: evaluate_segment(t, SECOND_HEAD.0, SECOND_HEAD.1),
second_tail: evaluate_segment(t, SECOND_TAIL.0, SECOND_TAIL.1),
}
}
#[derive(Debug)]
pub(crate) struct LinearClock {
timer: AnimationController,
}
impl LinearClock {
pub(crate) fn new() -> Self {
let mut timer = AnimationController::new(LINEAR_CYCLE).with_curve(Curve::Linear);
timer.repeat();
Self { timer }
}
pub(crate) fn step(&mut self, now: FrameTime) {
self.timer.advance(now);
}
pub(crate) fn cycle(&self) -> f64 {
self.timer.value_clamped()
}
pub(crate) fn segments(&self) -> Segments {
segments(self.cycle())
}
}
#[derive(Debug)]
pub(crate) struct CircularClock {
global: AnimationController,
additional: AnimationController,
sweep: AnimationController,
expanding: bool,
}
impl CircularClock {
pub(crate) fn new() -> Self {
let mut global = AnimationController::new(GLOBAL_ROTATION).with_curve(Curve::Linear);
global.repeat();
let mut additional =
AnimationController::new(ADDITIONAL_ROTATION).with_curve(Curve::Linear);
additional.repeat();
let mut sweep = AnimationController::new(SWEEP).with_curve(Curve::Linear);
sweep.forward();
Self {
global,
additional,
sweep,
expanding: true,
}
}
pub(crate) fn step(&mut self, now: FrameTime) {
self.global.advance(now);
self.additional.advance(now);
if !self.sweep.advance(now) {
self.expanding = !self.expanding;
if self.expanding {
self.sweep.forward();
} else {
self.sweep.reverse();
}
self.sweep.advance(now);
}
}
pub(crate) fn rotation(&self) -> f64 {
let total_deg = self.global.value_clamped() * GLOBAL_ROT_DEG
+ self.additional.value_clamped() * ADDITIONAL_ROT_DEG;
total_deg.to_radians()
}
pub(crate) fn sweep(&self) -> f64 {
let fraction = MIN_SWEEP + (MAX_SWEEP - MIN_SWEEP) * self.sweep.value_clamped();
fraction.clamp(MIN_SWEEP, MAX_SWEEP) * TAU
}
}
#[derive(Debug)]
pub(crate) struct WaveClock {
timer: AnimationController,
cycles: u64,
phase: f64,
}
impl WaveClock {
pub(crate) fn new() -> Self {
let mut timer = AnimationController::new(WAVE_PHASE).with_curve(Curve::Linear);
timer.repeat();
Self {
timer,
cycles: 0,
phase: 0.0,
}
}
pub(crate) fn step(&mut self, now: FrameTime) {
self.timer.advance(now);
let phase = self.timer.value_clamped();
if phase < self.phase {
self.cycles = self.cycles.saturating_add(1);
}
self.phase = phase;
}
pub(crate) fn seconds(&self) -> f64 {
(self.cycles as f64 + self.phase) * WAVE_PHASE.as_secs_f64()
}
pub(crate) fn radians(&self, wavelength: f64, wave_speed: f64) -> f64 {
wave_phase(self.seconds(), wavelength, wave_speed)
}
}
pub(crate) fn wave_phase(seconds: f64, wavelength: f64, wave_speed: f64) -> f64 {
if wavelength <= 0.0 {
return 0.0;
}
seconds * wave_speed / wavelength * TAU
}
pub(crate) fn amplitude_ramp(progress: f64) -> f64 {
if progress <= 0.1 || progress >= 0.95 {
0.0
} else {
1.0
}
}
pub(crate) fn amplitude_factor(
progress: Option<f64>,
fixed: Option<f64>,
for_progress: Option<&dyn Fn(f64) -> f64>,
) -> f64 {
let raw = match progress {
None => fixed.unwrap_or(1.0),
Some(p) => match (for_progress, fixed) {
(Some(curve), _) => curve(p),
(None, Some(a)) => a,
(None, None) => amplitude_ramp(p),
},
};
if raw.is_nan() {
0.0
} else {
raw.clamp(0.0, 1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn secs(t: f64) -> FrameTime {
FrameTime::from_nanos((t * 1e9) as u64)
}
#[test]
fn a_segment_is_zero_before_its_window_and_one_after_it() {
let before = 649.0 / 1750.0;
let after = 1501.0 / 1750.0;
assert_eq!(segments(before).second_head, 0.0);
assert_eq!(segments(after).second_head, 1.0);
assert!(segments(1749.0 / 1750.0).second_tail < 1.0);
assert_eq!(segments(1.0).second_tail, 1.0);
}
#[test]
fn every_segment_starts_at_zero_and_ends_at_one() {
let start = segments(0.0);
assert_eq!(
(
start.first_head,
start.first_tail,
start.second_head,
start.second_tail
),
(0.0, 0.0, 0.0, 0.0)
);
let end = segments(1.0);
assert_eq!(
(
end.first_head,
end.first_tail,
end.second_head,
end.second_tail
),
(1.0, 1.0, 1.0, 1.0)
);
}
#[test]
fn a_head_never_trails_its_own_tail() {
for i in 0..=175 {
let t = i as f64 / 175.0;
let s = segments(t);
assert!(
s.first_head >= s.first_tail,
"first line inverted at t = {t}"
);
assert!(
s.second_head >= s.second_tail,
"second line inverted at t = {t}"
);
}
}
#[test]
fn the_first_head_matches_the_line_easing_at_its_window_midpoint() {
let t = 500.0 / 1750.0;
let head = segments(t).first_head;
assert!(
(head - 0.1539).abs() < 1e-3,
"first_head at the window midpoint was {head}"
);
}
#[test]
fn the_two_lines_are_offset_by_their_delay_table() {
let s = segments(650.0 / 1750.0);
assert_eq!(s.second_head, 0.0);
assert_eq!(s.second_tail, 0.0);
assert!(s.first_head > 0.0);
assert!(s.first_tail > 0.0);
}
#[test]
fn the_linear_clock_runs_one_cycle_per_1750ms() {
let mut clock = LinearClock::new();
clock.step(secs(0.0));
clock.step(secs(0.875));
assert!((clock.cycle() - 0.5).abs() < 1e-9);
clock.step(secs(1.75));
assert!(clock.cycle() < 1e-9, "the cycle wraps at its period");
}
#[test]
fn the_circular_clock_pins_rotation_and_sweep_at_a_fixed_timestamp() {
let mut clock = CircularClock::new();
clock.step(secs(0.0));
clock.step(secs(0.65));
assert!((clock.rotation() - 169.0_f64.to_radians()).abs() < 1e-9);
assert!((clock.sweep() - 0.485 * TAU).abs() < 1e-9);
}
#[test]
fn the_circular_sweep_ping_pongs_between_its_bounds() {
let mut clock = CircularClock::new();
clock.step(secs(0.0));
clock.step(secs(1.3));
assert!((clock.sweep() - MAX_SWEEP * TAU).abs() < 1e-9);
assert!(!clock.expanding);
clock.step(secs(1.95));
assert!(
(clock.sweep() - 0.485 * TAU).abs() < 1e-9,
"half-way back down the contracting leg"
);
clock.step(secs(2.6));
assert!((clock.sweep() - MIN_SWEEP * TAU).abs() < 1e-9);
assert!(clock.expanding, "and flipped back to expanding");
}
#[test]
fn the_wave_clock_accumulates_seconds_across_period_wraps() {
let mut clock = WaveClock::new();
clock.step(secs(0.0));
clock.step(secs(0.4));
assert!((clock.seconds() - 0.4).abs() < 1e-9);
for (t, expected) in [(1.0, 1.0), (1.4, 1.4), (1.8, 1.8), (2.4, 2.4)] {
clock.step(secs(t));
assert!(
(clock.seconds() - expected).abs() < 1e-9,
"elapsed must stay monotone at t = {t}, read {}",
clock.seconds()
);
}
}
#[test]
fn a_frame_gap_longer_than_a_period_costs_the_wave_clock_its_whole_cycles() {
let mut clock = WaveClock::new();
clock.step(secs(0.0));
clock.step(secs(2.0));
assert!(
(clock.seconds() - 0.4).abs() < 1e-9,
"read {}",
clock.seconds()
);
}
#[test]
fn the_wave_phase_advances_one_turn_per_wavelength_of_travel() {
assert!((wave_phase(1.0, 20.0, 20.0) - TAU).abs() < 1e-9);
assert!((wave_phase(0.5, 20.0, 20.0) - TAU / 2.0).abs() < 1e-9);
assert!((wave_phase(1.0, 20.0, 40.0) - 2.0 * TAU).abs() < 1e-9);
assert_eq!(wave_phase(1.0, 0.0, 20.0), 0.0);
}
#[test]
fn the_amplitude_ramp_flattens_the_wave_near_both_ends() {
assert_eq!(amplitude_ramp(0.0), 0.0);
assert_eq!(amplitude_ramp(0.1), 0.0);
assert_eq!(amplitude_ramp(0.11), 1.0);
assert_eq!(amplitude_ramp(0.94), 1.0);
assert_eq!(amplitude_ramp(0.95), 0.0);
assert_eq!(amplitude_ramp(1.0), 0.0);
}
#[test]
fn amplitude_precedence_is_curve_then_constant_then_ramp() {
let curve = |_: f64| 0.25;
assert_eq!(
amplitude_factor(Some(0.5), Some(0.75), Some(&curve)),
0.25,
"an app-supplied curve outranks a constant"
);
assert_eq!(amplitude_factor(Some(0.5), Some(0.75), None), 0.75);
assert_eq!(amplitude_factor(Some(0.5), None, None), 1.0);
assert_eq!(amplitude_factor(Some(0.99), None, None), 0.0);
}
#[test]
fn an_indeterminate_amplitude_is_the_constant_or_one() {
let curve = |_: f64| 0.25;
assert_eq!(
amplitude_factor(None, None, Some(&curve)),
1.0,
"indeterminate consults neither the curve nor the ramp"
);
assert_eq!(amplitude_factor(None, Some(0.4), Some(&curve)), 0.4);
}
#[test]
fn an_out_of_range_amplitude_is_clamped_not_propagated() {
assert_eq!(amplitude_factor(Some(0.5), Some(9.0), None), 1.0);
assert_eq!(amplitude_factor(Some(0.5), Some(-9.0), None), 0.0);
assert_eq!(amplitude_factor(None, Some(f64::NAN), None), 0.0);
}
}