use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Easing {
Linear,
EaseIn,
#[default]
EaseOut,
EaseInOut,
}
impl Easing {
pub const ALL: [Self; 4] = [Self::Linear, Self::EaseIn, Self::EaseOut, Self::EaseInOut];
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Linear => "linear",
Self::EaseIn => "ease-in",
Self::EaseOut => "ease-out",
Self::EaseInOut => "ease-in-out",
}
}
#[must_use]
pub fn apply(self, t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
match self {
Self::Linear => t,
Self::EaseIn => t * t * t,
Self::EaseOut => 1.0 - (1.0 - t).powi(3),
Self::EaseInOut => {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Tween {
from: f32,
to: f32,
start: Duration,
duration: Duration,
easing: Easing,
}
impl Tween {
#[must_use]
pub fn settled(value: f32) -> Self {
Self { from: value, to: value, start: Duration::ZERO, duration: Duration::ZERO, easing: Easing::Linear }
}
#[must_use]
pub fn value(&self, now: Duration) -> f32 {
if self.duration.is_zero() || now >= self.end() {
return self.to;
}
let elapsed = now.saturating_sub(self.start).as_secs_f32() / self.duration.as_secs_f32();
self.from + (self.to - self.from) * self.easing.apply(elapsed)
}
#[must_use]
pub fn target(&self) -> f32 {
self.to
}
#[must_use]
pub fn is_running(&self, now: Duration) -> bool {
now < self.end() && self.from != self.to
}
fn end(&self) -> Duration {
self.start.saturating_add(self.duration)
}
pub fn retarget(&mut self, to: f32, now: Duration, duration: Duration, easing: Easing) {
let current = self.value(now);
*self = Self { from: current, to, start: now, duration, easing };
}
}
#[must_use]
pub fn steps(progress: f32, count: u16) -> u16 {
let position = (progress.clamp(0.0, 1.0) * f32::from(count)).round();
position as u16
}
#[derive(Debug, Default)]
pub(crate) struct Tweens {
values: Vec<(&'static str, Tween)>,
}
impl Tweens {
pub(crate) fn drive(
&mut self,
name: &'static str,
target: f32,
now: Duration,
duration: Duration,
easing: Easing,
) -> Tween {
match self.values.iter_mut().find(|(n, _)| *n == name) {
Some((_, tween)) => {
if (tween.target() - target).abs() > f32::EPSILON {
tween.retarget(target, now, duration, easing);
}
*tween
}
None => {
let tween = Tween::settled(target);
self.values.push((name, tween));
tween
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn easings_start_at_zero_and_end_at_one() {
for easing in [Easing::Linear, Easing::EaseIn, Easing::EaseOut, Easing::EaseInOut] {
assert!(easing.apply(0.0).abs() < 1e-6);
assert!((easing.apply(1.0) - 1.0).abs() < 1e-6);
}
assert!(Easing::EaseOut.apply(0.5) > 0.5);
assert!(Easing::EaseIn.apply(0.5) < 0.5);
}
#[test]
fn tween_moves_and_retargets_from_current_value() {
let mut tween = Tween::settled(0.0);
tween.retarget(10.0, Duration::ZERO, Duration::from_millis(100), Easing::Linear);
assert!((tween.value(Duration::from_millis(50)) - 5.0).abs() < 1e-4);
assert!(tween.is_running(Duration::from_millis(50)));
tween.retarget(0.0, Duration::from_millis(50), Duration::from_millis(100), Easing::Linear);
assert!((tween.value(Duration::from_millis(50)) - 5.0).abs() < 1e-4);
assert_eq!(tween.value(Duration::from_millis(200)), 0.0);
assert!(!tween.is_running(Duration::from_millis(200)));
}
#[test]
fn endless_tween_does_not_overflow_the_clock() {
let mut tween = Tween::settled(0.0);
tween.retarget(1.0, Duration::from_secs(5), Duration::MAX, Easing::Linear);
assert!(tween.is_running(Duration::from_secs(6)));
assert!(tween.value(Duration::from_secs(6)) < 1e-6);
}
#[test]
fn steps_round_to_cells() {
assert_eq!(steps(0.0, 3), 0);
assert_eq!(steps(0.49, 3), 1);
assert_eq!(steps(1.0, 3), 3);
assert_eq!(steps(7.0, 3), 3);
}
#[test]
fn first_sight_does_not_animate() {
let mut tweens = Tweens::default();
let first = tweens.drive("x", 1.0, Duration::ZERO, Duration::from_millis(100), Easing::Linear);
assert!(!first.is_running(Duration::ZERO));
let moving = tweens.drive("x", 0.0, Duration::from_millis(10), Duration::from_millis(100), Easing::Linear);
assert!(moving.is_running(Duration::from_millis(20)));
}
}