Files
k-os/crates/k-ui/src/motion.rs

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");
}
}