use crate::core::color::Rgba;
use crate::core::geometry::{Bounds, Pixels, Point, Size};
use rustc_hash::FxHashMap;
use std::f32::consts::{PI, TAU};
use std::time::{Duration, Instant};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Easing {
Linear,
EaseIn,
EaseOut,
EaseInOut,
CubicBezier(f32, f32, f32, f32),
Spring {
bounce: f32,
},
}
impl Easing {
pub const STANDARD: Easing = Easing::CubicBezier(0.2, 0.0, 0.0, 1.0);
pub const SPRING: Easing = Easing::Spring { bounce: 0.15 };
pub fn apply(self, linear: f32) -> f32 {
let t = linear.clamp(0.0, 1.0);
match self {
Easing::Linear => t,
Easing::EaseIn => t.powi(3),
Easing::EaseOut => 1.0 - (1.0 - t).powi(3),
Easing::EaseInOut if t < 0.5 => 4.0 * t.powi(3),
Easing::EaseInOut => 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0,
Easing::CubicBezier(x1, y1, x2, y2) => cubic_bezier(x1, y1, x2, y2, t),
Easing::Spring { bounce } => spring(bounce, t),
}
}
}
fn bezier(a: f32, b: f32, t: f32) -> f32 {
3.0 * a * (1.0 - t).powi(2) * t + 3.0 * b * (1.0 - t) * t * t + t.powi(3)
}
fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32, x: f32) -> f32 {
let mut t = x;
for _ in 0..8 {
let error = bezier(x1, x2, t) - x;
if error.abs() < 1.0e-5 {
return bezier(y1, y2, t);
}
let slope = 3.0 * x1 * (1.0 - t).powi(2) + 6.0 * (x2 - x1) * (1.0 - t) * t + 3.0 * (1.0 - x2) * t * t;
if slope.abs() < 1.0e-6 {
break;
}
t = (t - error / slope).clamp(0.0, 1.0);
}
let (mut low, mut high) = (0.0, 1.0);
for _ in 0..20 {
t = (low + high) / 2.0;
if bezier(x1, x2, t) < x { low = t } else { high = t }
}
bezier(y1, y2, t)
}
fn spring(bounce: f32, t: f32) -> f32 {
if t >= 1.0 {
return 1.0;
}
let damping = (1.0 - bounce).clamp(0.05, 1.0);
let omega = 3.0 * PI;
if damping >= 1.0 {
return 1.0 - (1.0 + omega * t) * (-omega * t).exp();
}
let damped = omega * (1.0 - damping * damping).sqrt();
let envelope = (-damping * omega * t).exp();
1.0 - envelope * ((damped * t).cos() + damping * omega / damped * (damped * t).sin())
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Repeat {
Once,
Times(u32),
Forever,
}
pub const fn ms(millis: u64) -> Duration {
Duration::from_millis(millis)
}
#[derive(Clone, Copy, Debug)]
pub struct Animation {
pub duration: Duration,
pub easing: Easing,
pub delay: Duration,
pub repeat: Repeat,
pub alternate: bool,
}
impl Animation {
pub fn new(duration: Duration) -> Self {
Self { duration, easing: Easing::EaseOut, delay: Duration::ZERO, repeat: Repeat::Once, alternate: false }
}
pub fn easing(mut self, easing: Easing) -> Self {
self.easing = easing;
self
}
pub fn delay(mut self, delay: Duration) -> Self {
self.delay = delay;
self
}
pub fn repeat(mut self, repeat: Repeat) -> Self {
self.repeat = repeat;
self
}
pub fn ping_pong(mut self) -> Self {
self.repeat = Repeat::Forever;
self.alternate = true;
self
}
pub fn sample(&self, elapsed: Duration) -> (f32, bool) {
let Some(active) = elapsed.checked_sub(self.delay) else { return (self.easing.apply(0.0), true) };
let total = self.duration.as_secs_f32().max(f32::EPSILON);
let cycles = active.as_secs_f32() / total;
let limit = match self.repeat {
Repeat::Once => 1.0,
Repeat::Times(times) => times.max(1) as f32,
Repeat::Forever => f32::INFINITY,
};
let running = cycles < limit;
let cycles = cycles.min(limit);
let cycle = cycles.floor();
let mut linear = if running { cycles - cycle } else { 1.0 };
let backwards = self.alternate && if running { cycle % 2.0 == 1.0 } else { (limit - 1.0) % 2.0 == 1.0 };
if backwards {
linear = 1.0 - linear;
}
(self.easing.apply(linear), running)
}
}
pub fn wave(seconds: f32, period: f32, offset: f32) -> f32 {
0.5 + 0.5 * ((seconds / period + offset) * TAU).sin()
}
pub trait Lerp: Copy {
fn lerp(self, to: Self, t: f32) -> Self;
}
impl Lerp for f32 {
fn lerp(self, to: Self, t: f32) -> Self {
self + (to - self) * t
}
}
impl Lerp for Pixels {
fn lerp(self, to: Self, t: f32) -> Self {
Pixels(self.0.lerp(to.0, t))
}
}
impl Lerp for Point {
fn lerp(self, to: Self, t: f32) -> Self {
Point::new(self.x.lerp(to.x, t), self.y.lerp(to.y, t))
}
}
impl Lerp for Size {
fn lerp(self, to: Self, t: f32) -> Self {
Size::new(self.width.lerp(to.width, t), self.height.lerp(to.height, t))
}
}
impl Lerp for Bounds {
fn lerp(self, to: Self, t: f32) -> Self {
Bounds::new(self.origin.lerp(to.origin, t), self.size.lerp(to.size, t))
}
}
impl Lerp for Rgba {
fn lerp(self, to: Self, t: f32) -> Self {
Rgba { r: self.r.lerp(to.r, t), g: self.g.lerp(to.g, t), b: self.b.lerp(to.b, t), a: self.a.lerp(to.a, t) }
}
}
#[derive(Clone, Copy)]
struct Transition {
from: f32,
to: f32,
started: Instant,
}
#[derive(Default)]
pub struct AnimationClock {
started: FxHashMap<String, Instant>,
transitions: FxHashMap<String, Transition>,
}
impl AnimationClock {
pub fn progress(&mut self, id: &str, animation: Animation, now: Instant) -> (f32, bool) {
let started = *self.started.entry(id.to_owned()).or_insert(now);
animation.sample(now.saturating_duration_since(started))
}
pub fn restart(&mut self, id: &str) {
self.started.remove(id);
}
pub fn transition(&mut self, id: &str, target: f32, animation: Animation, now: Instant) -> (f32, bool) {
let state =
self.transitions.entry(id.to_owned()).or_insert(Transition { from: target, to: target, started: now });
let (eased, running) = animation.sample(now.saturating_duration_since(state.started));
let current = state.from.lerp(state.to, eased);
if state.to != target {
*state = Transition { from: current, to: target, started: now };
return (current, true);
}
(current, running && state.from != state.to)
}
}
#[cfg(test)]
#[path = "animation_tests.rs"]
mod tests;