const SPINNER_FRAMES: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
#[must_use]
pub fn spinner_frame(tick: u64) -> &'static str {
SPINNER_FRAMES[(tick as usize) % SPINNER_FRAMES.len()]
}
#[derive(Debug, Clone, Copy)]
pub struct AnimatedValue {
current: f64,
target: f64,
rate: f64,
}
impl AnimatedValue {
#[must_use]
pub fn new(rate: f64) -> Self {
Self {
current: 0.0,
target: 0.0,
rate: rate.max(0.1),
}
}
pub fn set_target(&mut self, target: f64) {
if target.is_finite() {
self.target = target;
}
}
pub fn snap(&mut self, value: f64) {
self.current = value;
self.target = value;
}
pub fn tick(&mut self, dt: f64) -> f64 {
let blend = 1.0 - (-self.rate * dt.max(0.0)).exp();
self.current += (self.target - self.current) * blend;
if (self.target - self.current).abs() < 1e-9 {
self.current = self.target;
}
self.current
}
#[must_use]
pub fn value(&self) -> f64 {
self.current
}
#[must_use]
pub fn is_animating(&self) -> bool {
(self.target - self.current).abs() > f64::EPSILON * self.target.abs().max(1.0)
}
}
const SPRING_MAX_STEP: f64 = 1.0 / 120.0;
#[derive(Debug, Clone, Copy)]
pub struct SpringValue {
current: f64,
velocity: f64,
target: f64,
stiffness: f64,
damping: f64,
}
impl SpringValue {
#[must_use]
pub fn new(stiffness: f64, damping: f64) -> Self {
Self {
current: 0.0,
velocity: 0.0,
target: 0.0,
stiffness: stiffness.max(0.1),
damping: damping.max(0.0),
}
}
pub fn set_target(&mut self, target: f64) {
if target.is_finite() {
self.target = target;
}
}
pub fn snap(&mut self, value: f64) {
self.current = value;
self.velocity = 0.0;
self.target = value;
}
pub fn tick(&mut self, dt: f64) -> f64 {
let mut remaining = dt.clamp(0.0, 0.5);
while remaining > 0.0 {
let step = remaining.min(SPRING_MAX_STEP);
let acceleration =
self.stiffness * (self.target - self.current) - self.damping * self.velocity;
self.velocity += acceleration * step;
self.current += self.velocity * step;
remaining -= step;
}
self.current
}
#[must_use]
pub fn value(&self) -> f64 {
self.current
}
}