145 lines
4.2 KiB
Rust
145 lines
4.2 KiB
Rust
//! Movement, in the theme's motion tokens: the focus ring springs to what
|
|
//! gains focus (`spring_stiffness`, `spring_damping`); a toggle's knob
|
|
//! slides (`duration_fast`) and a new screen comes in (`duration_normal`),
|
|
//! both easing out. A theme with zero durations has none of it. Pure:
|
|
//! time comes in as milliseconds.
|
|
|
|
use crate::layout::Rect;
|
|
use crate::theme::Theme;
|
|
|
|
/// Steps no longer than this, so a slow frame does not overshoot.
|
|
const MAX_STEP_S: f32 = 1.0 / 60.0;
|
|
/// Close enough to rest: pixels, and pixels a second.
|
|
const REST: f32 = 0.5;
|
|
|
|
/// 0 to 1, fast then slowing.
|
|
pub fn ease_out(t: f32) -> f32 {
|
|
let u = 1.0 - t.clamp(0.0, 1.0);
|
|
1.0 - u * u * u
|
|
}
|
|
|
|
/// A value pulled toward its target by a damped spring of unit mass.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct Spring {
|
|
pub value: f32,
|
|
velocity: f32,
|
|
pub target: f32,
|
|
}
|
|
|
|
impl Spring {
|
|
pub fn at(v: f32) -> Self {
|
|
Self { value: v, velocity: 0.0, target: v }
|
|
}
|
|
|
|
pub fn step(&mut self, mut dt: f32, stiffness: f32, damping: f32) {
|
|
while dt > 0.0 {
|
|
let h = dt.min(MAX_STEP_S);
|
|
let pull = stiffness * (self.target - self.value) - damping * self.velocity;
|
|
self.velocity += pull * h;
|
|
self.value += self.velocity * h;
|
|
dt -= h;
|
|
}
|
|
if self.settled() {
|
|
*self = Self::at(self.target);
|
|
}
|
|
}
|
|
|
|
pub fn settled(&self) -> bool {
|
|
libm::fabsf(self.target - self.value) < REST && libm::fabsf(self.velocity) < REST
|
|
}
|
|
}
|
|
|
|
/// The focus ring's rectangle, each side on a spring.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct Ring([Spring; 4]);
|
|
|
|
impl Ring {
|
|
pub fn at(r: Rect) -> Self {
|
|
Self([Spring::at(r.x), Spring::at(r.y), Spring::at(r.w), Spring::at(r.h)])
|
|
}
|
|
|
|
pub fn aim(&mut self, r: Rect) {
|
|
for (s, v) in self.0.iter_mut().zip([r.x, r.y, r.w, r.h]) {
|
|
s.target = v;
|
|
}
|
|
}
|
|
|
|
pub fn step(&mut self, dt: f32, t: &Theme) {
|
|
for s in &mut self.0 {
|
|
s.step(dt, t.spring_stiffness as f32, t.spring_damping as f32);
|
|
}
|
|
}
|
|
|
|
pub fn rect(&self) -> Rect {
|
|
let [x, y, w, h] = self.0.map(|s| s.value);
|
|
Rect { x, y, w, h }
|
|
}
|
|
|
|
pub fn settled(&self) -> bool {
|
|
self.0.iter().all(Spring::settled)
|
|
}
|
|
}
|
|
|
|
/// Something under way for `ms` milliseconds, from when it is first drawn.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct Tween {
|
|
start: Option<u64>,
|
|
ms: u16,
|
|
}
|
|
|
|
impl Tween {
|
|
pub fn new(ms: u16) -> Self {
|
|
Self { start: None, ms }
|
|
}
|
|
|
|
/// How far along it is at `now`, eased: 1 when done (or of no length).
|
|
pub fn progress(&mut self, now: u64) -> f32 {
|
|
let start = *self.start.get_or_insert(now);
|
|
match self.ms {
|
|
0 => 1.0,
|
|
ms => ease_out(now.saturating_sub(start) as f32 / ms as f32),
|
|
}
|
|
}
|
|
|
|
pub fn done(&self, now: u64) -> bool {
|
|
self.ms == 0 || self.start.is_some_and(|s| now.saturating_sub(s) >= self.ms as u64)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn a_spring_comes_to_rest_on_its_target() {
|
|
let t = Theme::WHIMSY_DARK;
|
|
let mut s = Spring::at(0.0);
|
|
s.target = 100.0;
|
|
let mut ms = 0;
|
|
while !s.settled() {
|
|
s.step(0.016, t.spring_stiffness as f32, t.spring_damping as f32);
|
|
ms += 16;
|
|
assert!(ms < 2000, "rests within two seconds");
|
|
assert!(s.value < 110.0, "overshoots a little at most");
|
|
}
|
|
assert_eq!(s.value, 100.0);
|
|
// A long frame is taken in small steps: no blowing up.
|
|
let mut s = Spring::at(0.0);
|
|
s.target = 100.0;
|
|
s.step(1.0, 2000.0, 1.0);
|
|
assert!(s.value.abs() < 1000.0);
|
|
}
|
|
|
|
#[test]
|
|
fn a_tween_eases_out_from_its_first_frame() {
|
|
let mut tw = Tween::new(200);
|
|
assert_eq!(tw.progress(5000), 0.0, "starts when first drawn");
|
|
let half = tw.progress(5100);
|
|
assert!(half > 0.5, "eases out: past halfway at half time, {half}");
|
|
assert!(!tw.done(5150));
|
|
assert_eq!(tw.progress(5200), 1.0);
|
|
assert!(tw.done(5200));
|
|
assert!(Tween::new(0).done(0), "motion off");
|
|
}
|
|
}
|