#[derive(Copy, Clone, Debug, PartialEq)]
pub struct SpringClockConfig {
pub time_grid: f64,
pub step_ms: f64,
pub max_steps_per_frame: u32,
pub settle_check_steps: u32,
}
impl Default for SpringClockConfig {
fn default() -> SpringClockConfig {
SpringClockConfig { time_grid: 64.0, step_ms: 4.0, max_steps_per_frame: 300, settle_check_steps: 16 }
}
}
pub fn validate_spring_clock_config(config: &SpringClockConfig) {
let SpringClockConfig { time_grid, step_ms, max_steps_per_frame, settle_check_steps } = *config;
assert!(
time_grid >= 1.0 && time_grid.fract() == 0.0 && time_grid.log2().fract() == 0.0,
"the time grid must be a whole power of two, got {time_grid}"
);
assert!(
step_ms > 0.0 && (step_ms * time_grid).fract() == 0.0,
"a spring step must be a positive multiple of the time grid"
);
assert!(max_steps_per_frame >= 1, "max_steps_per_frame must be a positive integer");
assert!(settle_check_steps >= 1, "settle_check_steps must be a positive integer");
assert!(
max_steps_per_frame as f64 * step_ms * time_grid <= 16_777_216.0,
"max_steps_per_frame × step_ms must stay within 2^24 grid steps"
);
}
pub fn quantize_time_ms(time_stamp: f64, config: &SpringClockConfig) -> f64 {
assert!(time_stamp.is_finite(), "a timestamp must be finite, got {time_stamp}");
js_round(time_stamp * config.time_grid) / config.time_grid + 0.0
}
fn js_round(value: f64) -> f64 {
let rounded = value.round();
if rounded - value == -0.5 { rounded + 1.0 } else { rounded }
}
#[derive(Clone, Debug, PartialEq)]
pub struct FrameTiming {
pub frame_time_ms: f64,
pub duration_ms: f64,
pub event_times_ms: Vec<f64>,
}
pub fn quantize_frame_timing(
frame_time_stamp: f64,
previous_frame_time_ms: Option<f64>,
event_time_stamps: &[f64],
config: &SpringClockConfig,
) -> FrameTiming {
validate_spring_clock_config(config);
let cap_ms = config.max_steps_per_frame as f64 * config.step_ms;
let mut frame_time_ms = quantize_time_ms(frame_time_stamp, config);
if let Some(previous) = previous_frame_time_ms {
frame_time_ms = frame_time_ms.max(previous);
}
let duration_ms = previous_frame_time_ms.map_or(0.0, |previous| cap_ms.min(frame_time_ms - previous));
let earliest_event_ms = previous_frame_time_ms.map_or(-cap_ms, |previous| (-cap_ms).max(previous - frame_time_ms));
let event_times_ms = event_time_stamps
.iter()
.map(|&time_stamp| 0f64.min(earliest_event_ms.max(quantize_time_ms(time_stamp, config) - frame_time_ms)) + 0.0)
.collect();
FrameTiming { frame_time_ms, duration_ms, event_times_ms }
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct StepCount {
pub steps: u32,
pub leftover_ms: f64,
pub snap: bool,
}
pub fn count_spring_steps(pending_ms: f64, step_budget: u32, config: &SpringClockConfig) -> StepCount {
assert!(
(pending_ms * config.time_grid).fract() == 0.0 && pending_ms >= 0.0,
"pending time must lie on the time grid and be nonnegative, got {pending_ms}"
);
let steps = (pending_ms / config.step_ms).floor();
if steps > step_budget as f64 {
return StepCount { steps: 0, leftover_ms: 0.0, snap: true };
}
StepCount { steps: steps as u32, leftover_ms: pending_ms - steps * config.step_ms, snap: false }
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct SpringConstants {
pub stiffness: f64,
pub damping: f64,
}
pub type SpringMatrix = [f64; 4];
pub const IDENTITY_SPRING_MATRIX: SpringMatrix = [1.0, 0.0, 0.0, 1.0];
pub fn spring_step_matrix(constants: SpringConstants, step_ms: f64) -> SpringMatrix {
assert!(
constants.stiffness.is_finite()
&& constants.stiffness >= 0.0
&& constants.damping.is_finite()
&& constants.damping >= 0.0,
"stiffness and damping must be finite and nonnegative"
);
let dt = step_ms / 1000.0;
let velocity_keep = 1.0 - constants.damping * dt;
[1.0 - constants.stiffness * dt * dt, dt * velocity_keep, -constants.stiffness * dt, velocity_keep]
}
pub fn multiply_spring_matrices(left: SpringMatrix, right: SpringMatrix) -> SpringMatrix {
[
left[0] * right[0] + left[1] * right[2],
left[0] * right[1] + left[1] * right[3],
left[2] * right[0] + left[3] * right[2],
left[2] * right[1] + left[3] * right[3],
]
}
pub fn spring_matrix_power(matrix: SpringMatrix, count: u32) -> SpringMatrix {
let (mut result, mut square, mut remaining) = (IDENTITY_SPRING_MATRIX, matrix, count);
while remaining > 0 {
if remaining % 2 == 1 {
result = multiply_spring_matrices(result, square);
}
remaining /= 2;
if remaining > 0 {
square = multiply_spring_matrices(square, square);
}
}
result
}
pub fn spring_matrix_bound(constants: SpringConstants, config: &SpringClockConfig) -> f64 {
let step = spring_step_matrix(constants, config.step_ms);
let (mut power, mut bound) = (IDENTITY_SPRING_MATRIX, 1f64);
for _ in 1..=config.max_steps_per_frame {
power = multiply_spring_matrices(power, step);
for entry in power {
bound = bound.max(entry.abs());
}
}
let declared = (bound * (1.0 + 1e-6)) as f32 as f64;
assert!(declared.is_finite(), "the spring matrix overflows FP32 within max_steps_per_frame steps");
declared
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct SpringClock {
pub animated_until_ms: Option<f64>,
pub check_origin_ms: f64,
pub last_evaluation_ms: Option<f64>,
}
pub const STOPPED_SPRING_CLOCK: SpringClock =
SpringClock { animated_until_ms: None, check_origin_ms: 0.0, last_evaluation_ms: None };
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum EvaluationKind {
SettleCheck,
Events,
Frame,
Snap,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum WriteBack {
EveryEvaluation,
EventsAndSettleChecks,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PlannedEvaluation<Event> {
pub kind: EvaluationKind,
pub time_ms: f64,
pub simulated_time_ms: f64,
pub duration_ms: f64,
pub steps: u32,
pub settle_check: bool,
pub snap: bool,
pub events: Vec<Event>,
pub writes_back: bool,
}
pub fn advance_frame<Event: Clone>(
clock: SpringClock,
frame_time_stamp: f64,
events: &[Event],
time_stamp: impl Fn(&Event) -> f64,
mut evaluate: impl FnMut(&PlannedEvaluation<Event>) -> bool,
write_back: WriteBack,
config: &SpringClockConfig,
) -> SpringClock {
validate_spring_clock_config(config);
let check_interval_ms = config.step_ms * config.settle_check_steps as f64;
let earliest_ms = clock.last_evaluation_ms.unwrap_or(f64::NEG_INFINITY);
let frame_time_ms = quantize_time_ms(frame_time_stamp, config).max(earliest_ms);
struct State {
animated_until_ms: Option<f64>,
check_origin_ms: f64,
last_evaluation_ms: Option<f64>,
step_budget: u32,
}
let mut state = State {
animated_until_ms: clock.animated_until_ms,
check_origin_ms: clock.check_origin_ms,
last_evaluation_ms: clock.last_evaluation_ms,
step_budget: config.max_steps_per_frame,
};
let mut run =
|state: &mut State, kind: EvaluationKind, time_ms: f64, steps: u32, settle_check: bool, here: Vec<Event>| {
let writes_back = kind != EvaluationKind::Frame || write_back == WriteBack::EveryEvaluation;
let simulated_time_ms =
state.animated_until_ms.map_or(time_ms, |until| until + steps as f64 * config.step_ms);
let animating = evaluate(&PlannedEvaluation {
kind,
time_ms,
simulated_time_ms,
duration_ms: state.last_evaluation_ms.map_or(0.0, |last| time_ms - last),
steps,
settle_check,
snap: kind == EvaluationKind::Snap,
events: here,
writes_back,
});
if !writes_back {
return;
}
state.step_budget -= steps;
if state.animated_until_ms.is_none() && animating {
state.check_origin_ms = time_ms;
}
state.animated_until_ms = if animating { Some(simulated_time_ms) } else { None };
state.last_evaluation_ms = if animating { Some(time_ms) } else { None };
};
let mut step_to = |state: &mut State, target_ms: f64, kind: EvaluationKind, here: Vec<Event>| {
if let Some(until) = state.animated_until_ms
&& count_spring_steps(target_ms - until, state.step_budget, config).snap
{
state.animated_until_ms = None;
run(state, EvaluationKind::Snap, target_ms, 0, true, Vec::new());
}
while let Some(until) = state.animated_until_ms {
let reachable_ms =
until + count_spring_steps(target_ms - until, state.step_budget, config).steps as f64 * config.step_ms;
let next_check_ms = state.check_origin_ms
+ (((until - state.check_origin_ms) / check_interval_ms).floor() + 1.0) * check_interval_ms;
if next_check_ms > reachable_ms {
break;
}
let time_ms = next_check_ms.max(state.last_evaluation_ms.unwrap_or(next_check_ms));
let steps = ((next_check_ms - until) / config.step_ms) as u32;
run(state, EvaluationKind::SettleCheck, time_ms, steps, true, Vec::new());
}
match state.animated_until_ms {
None => run(state, kind, target_ms, 0, true, here),
Some(until) => {
let steps = count_spring_steps(target_ms - until, state.step_budget, config).steps;
run(state, kind, target_ms, steps, false, here);
}
}
};
let mut timed: Vec<(f64, usize, &Event)> = events
.iter()
.enumerate()
.map(|(index, event)| {
let time_ms = frame_time_ms.min(earliest_ms.max(quantize_time_ms(time_stamp(event), config)));
(time_ms, index, event)
})
.collect();
timed.sort_by(|left, right| left.0.total_cmp(&right.0).then(left.1.cmp(&right.1)));
let mut start = 0;
while start < timed.len() {
let time_ms = timed[start].0;
let mut end = start;
while end < timed.len() && timed[end].0 == time_ms {
end += 1;
}
let here: Vec<Event> = timed[start..end].iter().map(|&(_, _, event)| event.clone()).collect();
step_to(&mut state, time_ms, EvaluationKind::Events, here);
start = end;
}
step_to(&mut state, frame_time_ms, EvaluationKind::Frame, Vec::new());
SpringClock {
animated_until_ms: state.animated_until_ms,
check_origin_ms: state.check_origin_ms,
last_evaluation_ms: state.last_evaluation_ms,
}
}