use parking_lot::RwLock;
use std::sync::OnceLock;
use web_time::{Duration, Instant};
pub(crate) fn now() -> Instant {
let lock = CLOCK.get_or_init(|| RwLock::new(Box::new(SystemClock) as Box<dyn Clock>));
lock.read().now()
}
#[derive(Clone, Copy, Debug)]
pub enum Easing {
Linear,
EaseIn,
EaseOut,
EaseInOut,
SpringCrit {
omega: f32,
},
SpringGentle,
SpringBouncy,
FastOutSlowIn,
}
impl Easing {
pub fn interpolate(&self, t: f32) -> f32 {
match self {
Easing::Linear => t,
Easing::EaseIn => t * t,
Easing::EaseOut => t * (2.0 - t),
Easing::EaseInOut => {
if t < 0.5 {
2.0 * t * t
} else {
-1.0 + (4.0 - 2.0 * t) * t
}
}
Easing::SpringCrit { omega } => {
let w = (*omega).max(0.0);
let tt = t.max(0.0);
1.0 - (1.0 + w * tt) * (-(w * tt)).exp()
}
Easing::SpringGentle => spring_underdamped_normalized(t, 0.5, 8.0),
Easing::SpringBouncy => spring_underdamped_normalized(t, 0.2, 12.0),
Easing::FastOutSlowIn => eval_cubic_bezier(0.4, 0.0, 0.2, 1.0, t),
}
}
}
fn eval_cubic_bezier(p1x: f32, p1y: f32, p2x: f32, p2y: f32, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
if t <= 0.0 {
return 0.0;
}
if t >= 1.0 {
return 1.0;
}
let mut u = t;
for _ in 0..6 {
let omu = 1.0 - u;
let x = 3.0 * omu * omu * u * p1x + 3.0 * omu * u * u * p2x + u * u * u;
let dx = 3.0 * omu * omu * p1x + 6.0 * omu * u * (p2x - p1x) + 3.0 * u * u * (1.0 - p2x);
if dx.abs() < 1e-10 {
break;
}
u -= (x - t) / dx;
u = u.clamp(0.0, 1.0);
}
let omu = 1.0 - u;
3.0 * omu * omu * u * p1y + 3.0 * omu * u * u * p2y + u * u * u
}
fn spring_underdamped_normalized(t: f32, zeta: f32, omega: f32) -> f32 {
let tt = t.max(0.0);
let z = zeta.clamp(0.0, 0.999);
let w = omega.max(0.0);
let wd = w * (1.0 - z * z).sqrt();
let exp_term = (-z * w * tt).exp();
let cos_term = (wd * tt).cos();
let sin_term = (wd * tt).sin();
let c = z / (1.0 - z * z).sqrt();
let y = 1.0 - exp_term * (cos_term + c * sin_term);
y.clamp(0.0, 1.0)
}
#[derive(Clone, Copy, Debug)]
pub struct AnimationSpec {
pub duration: Duration,
pub easing: Easing,
pub delay: Duration,
}
impl Default for AnimationSpec {
fn default() -> Self {
Self {
duration: Duration::from_millis(300),
easing: Easing::EaseInOut,
delay: Duration::ZERO,
}
}
}
impl AnimationSpec {
pub fn tween(duration: Duration, easing: Easing) -> Self {
Self {
duration,
easing,
delay: Duration::ZERO,
}
}
pub fn spring_crit(omega: f32, duration: Duration) -> Self {
Self {
duration,
easing: Easing::SpringCrit { omega },
delay: Duration::ZERO,
}
}
pub fn spring_gentle() -> Self {
Self {
duration: Duration::from_millis(450),
easing: Easing::SpringGentle,
delay: Duration::ZERO,
}
}
pub fn spring_bouncy() -> Self {
Self {
duration: Duration::from_millis(700),
easing: Easing::SpringBouncy,
delay: Duration::ZERO,
}
}
pub fn fast() -> Self {
Self {
duration: Duration::from_millis(150),
easing: Easing::EaseOut,
delay: Duration::ZERO,
}
}
pub fn slow() -> Self {
Self {
duration: Duration::from_millis(600),
easing: Easing::EaseInOut,
delay: Duration::ZERO,
}
}
pub fn m3_elevation_in() -> Self {
Self {
duration: Duration::from_millis(120),
easing: Easing::FastOutSlowIn,
delay: Duration::ZERO,
}
}
pub fn m3_elevation_out() -> Self {
Self {
duration: Duration::from_millis(150),
easing: Easing::EaseOut,
delay: Duration::ZERO,
}
}
}
pub trait Interpolate {
fn interpolate(&self, other: &Self, t: f32) -> Self;
}
impl Interpolate for f32 {
fn interpolate(&self, other: &Self, t: f32) -> Self {
self + (other - self) * t
}
}
impl Interpolate for crate::Color {
fn interpolate(&self, other: &Self, t: f32) -> Self {
let lerp = |a: u8, b: u8| (a as f32 + (b as f32 - a as f32) * t).round().clamp(0.0, 255.0) as u8;
crate::Color(lerp(self.0, other.0), lerp(self.1, other.1), lerp(self.2, other.2), lerp(self.3, other.3))
}
}
pub trait Clock: Send + Sync + 'static {
fn now(&self) -> Instant;
}
pub struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> Instant {
Instant::now()
}
}
static CLOCK: OnceLock<RwLock<Box<dyn Clock>>> = OnceLock::new();
pub fn set_clock(clock: Box<dyn Clock>) {
let lock = CLOCK.get_or_init(|| RwLock::new(Box::new(SystemClock) as Box<dyn Clock>));
*lock.write() = clock;
}
pub fn ensure_system_clock() {
let _ = CLOCK.get_or_init(|| RwLock::new(Box::new(SystemClock) as Box<dyn Clock>));
}
#[derive(Clone)]
pub struct TestClock {
pub t: Instant,
}
impl Clock for TestClock {
fn now(&self) -> Instant {
self.t
}
}
pub struct AnimatedValue<T: Interpolate + Clone> {
current: T,
target: T,
start: T,
spec: AnimationSpec,
start_time: Option<Instant>,
}
impl<T: Interpolate + Clone> AnimatedValue<T> {
pub fn new(initial: T, spec: AnimationSpec) -> Self {
Self {
current: initial.clone(),
target: initial.clone(),
start: initial,
spec,
start_time: None,
}
}
pub fn set_spec(&mut self, spec: AnimationSpec) {
self.spec = spec;
}
pub fn set_target(&mut self, target: T) {
if self.start_time.is_some() {
self.update();
self.start = self.current.clone();
} else {
self.start = self.current.clone();
}
self.target = target;
self.start_time = Some(now());
}
pub fn update(&mut self) -> bool {
if let Some(start) = self.start_time {
let elapsed = now().saturating_duration_since(start);
if elapsed < self.spec.delay {
return true; }
let animation_time = elapsed - self.spec.delay;
if animation_time >= self.spec.duration {
self.current = self.target.clone();
self.start_time = None;
return false;
}
let t =
(animation_time.as_secs_f32() / self.spec.duration.as_secs_f32()).clamp(0.0, 1.0);
let eased_t = self.spec.easing.interpolate(t);
let eased_t = eased_t.clamp(0.0, 1.0);
self.current = self.start.interpolate(&self.target, eased_t);
true
} else {
false
}
}
pub fn get(&self) -> &T {
&self.current
}
pub fn is_animating(&self) -> bool {
self.start_time.is_some()
}
}