use super::easing::Easing;
use core::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Tween {
from: f32,
to: f32,
elapsed: Duration,
duration: Duration,
easing: Easing,
}
impl Tween {
pub const DEFAULT_DURATION: Duration = Duration::from_millis(200);
#[must_use]
pub const fn new(from: f32, to: f32) -> Self {
Self {
from,
to,
elapsed: Duration::ZERO,
duration: Self::DEFAULT_DURATION,
easing: Easing::Linear,
}
}
#[must_use]
pub const fn duration(mut self, duration: Duration) -> Self {
self.duration = duration;
self
}
#[must_use]
pub const fn easing(mut self, easing: Easing) -> Self {
self.easing = easing;
self
}
pub fn update(&mut self, dt: Duration) {
self.elapsed = (self.elapsed + dt).min(self.duration);
}
#[must_use]
pub fn progress(&self) -> f32 {
if self.duration.is_zero() {
return 1.0;
}
self.elapsed.as_secs_f32() / self.duration.as_secs_f32()
}
#[must_use]
pub fn value(&self) -> f32 {
let t = self.easing.apply(self.progress());
libm::fmaf(self.to - self.from, t, self.from)
}
#[must_use]
pub fn is_finished(&self) -> bool {
self.elapsed >= self.duration
}
pub fn retarget(&mut self, target: f32) {
self.from = self.value();
self.to = target;
self.elapsed = Duration::ZERO;
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)] mod tests {
use super::*;
#[test]
fn starts_at_from_and_ends_at_to() {
let mut tween = Tween::new(10.0, 20.0).duration(Duration::from_millis(100));
assert_eq!(tween.value(), 10.0);
assert!(!tween.is_finished());
tween.update(Duration::from_millis(100));
assert_eq!(tween.value(), 20.0);
assert!(tween.is_finished());
}
#[test]
fn update_past_duration_clamps_instead_of_overshooting() {
let mut tween = Tween::new(0.0, 1.0).duration(Duration::from_millis(100));
tween.update(Duration::from_millis(500)); assert_eq!(tween.value(), 1.0);
assert!(tween.is_finished());
tween.update(Duration::from_millis(500)); assert!(tween.is_finished());
assert_eq!(tween.value(), 1.0);
}
#[test]
fn easing_reshapes_the_midpoint() {
let mut linear = Tween::new(0.0, 1.0).duration(Duration::from_millis(100));
let mut eased = Tween::new(0.0, 1.0)
.duration(Duration::from_millis(100))
.easing(Easing::EaseInQuad);
linear.update(Duration::from_millis(50));
eased.update(Duration::from_millis(50));
assert_eq!(linear.value(), 0.5);
assert!(eased.value() < linear.value()); }
#[test]
fn zero_duration_finishes_immediately() {
let tween = Tween::new(0.0, 5.0).duration(Duration::ZERO);
assert!(tween.is_finished());
assert_eq!(tween.value(), 5.0);
}
#[test]
fn retarget_starts_from_the_current_value_not_the_original_from() {
let mut tween = Tween::new(0.0, 10.0).duration(Duration::from_millis(100));
tween.update(Duration::from_millis(50)); assert_eq!(tween.value(), 5.0);
tween.retarget(20.0);
assert_eq!(tween.value(), 5.0);
assert!(!tween.is_finished());
tween.update(Duration::from_millis(100));
assert_eq!(tween.value(), 20.0);
}
#[test]
fn repeated_retargets_never_snap() {
let mut tween = Tween::new(0.0, 1.0).duration(Duration::from_millis(100));
tween.update(Duration::from_millis(30));
let before = tween.value();
tween.retarget(0.0);
assert_eq!(tween.value(), before);
tween.update(Duration::from_millis(10));
let before = tween.value();
tween.retarget(1.0);
assert_eq!(tween.value(), before);
}
}