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