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());
retroglyph_core::math::mul_add(self.to - self.from, t, self.from)
}
#[must_use]
pub fn is_finished(&self) -> bool {
self.elapsed >= self.duration
}
#[must_use]
pub const fn target(&self) -> f32 {
self.to
}
#[must_use]
pub const fn origin(&self) -> f32 {
self.from
}
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 target_and_origin_report_to_and_from() {
let tween = Tween::new(10.0, 20.0).duration(Duration::from_millis(100));
assert_eq!(tween.origin(), 10.0);
assert_eq!(tween.target(), 20.0);
}
#[test]
fn retarget_updates_target_and_origin_to_where_the_tween_actually_is() {
let mut tween = Tween::new(0.0, 10.0).duration(Duration::from_millis(100));
tween.update(Duration::from_millis(50));
tween.retarget(20.0);
assert_eq!(tween.origin(), 5.0);
assert_eq!(tween.target(), 20.0);
}
#[test]
#[allow(clippy::cast_precision_loss)] fn retarget_from_an_elastic_overshoot_still_finishes_at_the_new_target() {
const DURATION: Duration = Duration::from_millis(100);
let mut tween = Tween::new(0.0, 1.0)
.duration(DURATION)
.easing(Easing::EaseOutElastic);
let overshoot_millis = (0..100)
.find(|&i| !(0.0..=1.0).contains(&Easing::EaseOutElastic.apply(i as f32 / 100.0)))
.expect("EaseOutElastic should overshoot somewhere in its first 100 samples");
tween.update(Duration::from_millis(overshoot_millis));
let overshot = tween.value();
assert!(
!(0.0..=1.0).contains(&overshot),
"expected an overshot value, got {overshot}"
);
tween.retarget(5.0);
assert_eq!(tween.value(), overshot);
tween.update(DURATION);
assert_eq!(tween.value(), 5.0);
assert!(tween.is_finished());
}
#[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);
}
}