use std::time::Duration;
use crate::base::{Point, Rgba};
use super::Easing;
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 i32 {
fn lerp(self, to: Self, t: f32) -> Self {
(self as f32 + (to as f32 - self as f32) * t).round() as i32
}
}
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 Rgba {
fn lerp(self, to: Self, t: f32) -> Self {
Rgba::lerp(self, to, t)
}
}
#[derive(Copy, Clone, Debug)]
pub struct Tween<T: Lerp> {
pub from: T,
pub to: T,
pub duration: Duration,
pub easing: Easing,
}
impl<T: Lerp> Tween<T> {
pub fn new(from: T, to: T, duration: Duration) -> Tween<T> {
Tween {
from,
to,
duration,
easing: Easing::Linear,
}
}
pub fn with_easing(mut self, easing: Easing) -> Tween<T> {
self.easing = easing;
self
}
pub fn sample(&self, elapsed: Duration) -> T {
if self.duration.is_zero() || elapsed >= self.duration {
return self.to;
}
let t = elapsed.as_secs_f32() / self.duration.as_secs_f32();
self.from.lerp(self.to, self.easing.eval(t))
}
pub fn is_finished(&self, elapsed: Duration) -> bool {
elapsed >= self.duration
}
}
#[cfg(test)]
mod tests {
use super::*;
const MS: fn(u64) -> Duration = Duration::from_millis;
#[test]
fn f32_and_i32_lerp() {
let t = Tween::new(0.0f32, 10.0, MS(100));
assert_eq!(t.sample(MS(0)), 0.0);
assert_eq!(t.sample(MS(50)), 5.0);
assert_eq!(t.sample(MS(100)), 10.0);
assert_eq!(t.sample(MS(999)), 10.0, "rests at target after the end");
let t = Tween::new(0i32, 9, MS(90));
assert_eq!(t.sample(MS(30)), 3);
}
#[test]
fn point_and_color_lerp() {
let t = Tween::new(Point::new(0, 0), Point::new(10, -10), MS(100));
assert_eq!(t.sample(MS(50)), Point::new(5, -5));
let c = Tween::new(Rgba::BLACK, Rgba::WHITE, MS(100));
let mid = c.sample(MS(50));
assert!(mid.r > 100 && mid.r < 155);
}
#[test]
fn zero_duration_is_an_instant_jump() {
let t = Tween::new(0.0f32, 5.0, MS(0));
assert_eq!(t.sample(MS(0)), 5.0);
assert!(t.is_finished(MS(0)));
}
#[test]
fn easing_shapes_the_timeline() {
let t = Tween::new(0.0f32, 1.0, MS(100)).with_easing(Easing::EaseIn);
assert!(t.sample(MS(50)) < 0.5, "ease-in lags at the midpoint");
assert_eq!(t.sample(MS(100)), 1.0);
}
#[test]
fn overshooting_easing_extrapolates_scalars_and_saturates_color() {
let settle = Easing::bezier(0.34, 1.56, 0.64, 1.0);
let t = Tween::new(0.0f32, 100.0, MS(100)).with_easing(settle);
let peak = (1..100).map(|ms| t.sample(MS(ms))).fold(0.0f32, f32::max);
assert!(peak > 100.0, "f32 lerp must extrapolate past `to`: {peak}");
assert_eq!(t.sample(MS(100)), 100.0, "rests exactly at target");
let c =
Tween::new(Rgba::rgb(0, 0, 0), Rgba::rgb(250, 250, 250), MS(100)).with_easing(settle);
for ms in 1..100 {
let v = c.sample(MS(ms));
assert!(v.r >= v.g.min(v.b), "channels move together");
}
assert_eq!(c.sample(MS(100)), Rgba::rgb(250, 250, 250));
}
#[test]
fn finished_boundary() {
let t = Tween::new(0.0f32, 1.0, MS(100));
assert!(!t.is_finished(MS(99)));
assert!(t.is_finished(MS(100)));
}
}