use kurbo::{Affine, Point, Vec2};
use crate::Color;
pub trait Lerp: Sized {
fn lerp(&self, to: &Self, t: f64) -> Self;
}
impl Lerp for f64 {
fn lerp(&self, to: &Self, t: f64) -> Self {
self + (to - self) * t
}
}
impl Lerp for f32 {
fn lerp(&self, to: &Self, t: f64) -> Self {
self + (to - self) * t as f32
}
}
impl Lerp for Vec2 {
fn lerp(&self, to: &Self, t: f64) -> Self {
Vec2::new(self.x.lerp(&to.x, t), self.y.lerp(&to.y, t))
}
}
impl Lerp for Point {
fn lerp(&self, to: &Self, t: f64) -> Self {
Point::new(self.x.lerp(&to.x, t), self.y.lerp(&to.y, t))
}
}
impl Lerp for Color {
fn lerp(&self, to: &Self, t: f64) -> Self {
if self == to {
return *self;
}
let a = self.to_oklab();
let b = to.to_oklab();
let t32 = t as f32;
let mix = |i: usize| a[i] + (b[i] - a[i]) * t32;
Color::from_oklab(
[mix(0), mix(1), mix(2)],
self.a.lerp(&to.a, t).clamp(0.0, 1.0),
)
}
}
impl<T: Lerp> Lerp for Option<T>
where
T: Clone,
{
fn lerp(&self, to: &Self, t: f64) -> Self {
match (self, to) {
(Some(a), Some(b)) => Some(a.lerp(b, t)),
_ if t < 0.5 => self.clone(),
_ => to.clone(),
}
}
}
impl<T: Lerp + Clone> Lerp for Vec<T> {
fn lerp(&self, to: &Self, t: f64) -> Self {
if self.len() == to.len() {
self.iter().zip(to).map(|(a, b)| a.lerp(b, t)).collect()
} else if t < 0.5 {
self.clone()
} else {
to.clone()
}
}
}
impl Lerp for String {
fn lerp(&self, to: &Self, t: f64) -> Self {
if t <= 0.0 {
return self.clone();
}
if t >= 1.0 {
return to.clone();
}
let a: Vec<char> = self.chars().collect();
let b: Vec<char> = to.chars().collect();
let common = a.iter().zip(&b).take_while(|(x, y)| x == y).count();
let (del, add) = (a.len() - common, b.len() - common);
let total = del + add;
if total == 0 {
return to.clone();
}
let steps = (t * total as f64).floor() as usize;
if steps < del {
a[..a.len() - steps].iter().collect()
} else {
b[..common + (steps - del)].iter().collect()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Transform {
pub translation: Vec2,
pub rotation: f64,
pub scale: Vec2,
}
impl Default for Transform {
fn default() -> Self {
Self::IDENTITY
}
}
impl Transform {
pub const IDENTITY: Transform = Transform {
translation: Vec2::ZERO,
rotation: 0.0,
scale: Vec2::new(1.0, 1.0),
};
pub fn to_affine(&self) -> Affine {
Affine::translate(self.translation)
* Affine::rotate(self.rotation)
* Affine::scale_non_uniform(self.scale.x, self.scale.y)
}
}
impl Lerp for Transform {
fn lerp(&self, to: &Self, t: f64) -> Self {
Transform {
translation: Lerp::lerp(&self.translation, &to.translation, t),
rotation: self.rotation.lerp(&to.rotation, t),
scale: Lerp::lerp(&self.scale, &to.scale, t),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scalars_and_vectors() {
assert_eq!(2.0f64.lerp(&4.0, 0.5), 3.0);
assert_eq!(
Lerp::lerp(&Vec2::new(0.0, 10.0), &Vec2::new(10.0, 0.0), 0.25),
Vec2::new(2.5, 7.5)
);
}
#[test]
fn color_endpoints_and_midpoint() {
let red = Color::from_hex("#ff0000").unwrap();
let blue = Color::from_hex("#0000ff").unwrap();
assert_eq!(red.lerp(&blue, 0.0).to_rgba8(), red.to_rgba8());
assert_eq!(red.lerp(&blue, 1.0).to_rgba8(), blue.to_rgba8());
let mid = red.lerp(&blue, 0.5).to_oklab();
let (a, b) = (red.to_oklab(), blue.to_oklab());
assert!((mid[0] - (a[0] + b[0]) / 2.0).abs() < 1e-3);
}
#[test]
fn string_typewriter() {
let a = String::from("Hello");
let b = String::from("Help me");
assert_eq!(a.lerp(&b, 0.0), "Hello");
assert_eq!(a.lerp(&b, 1.0 / 6.0 + 1e-9), "Hell");
assert_eq!(a.lerp(&b, 2.0 / 6.0 + 1e-9), "Hel");
assert_eq!(a.lerp(&b, 3.0 / 6.0 + 1e-9), "Help");
assert_eq!(a.lerp(&b, 1.0), "Help me");
assert_eq!(String::new().lerp(&"Привет".to_string(), 0.5), "При");
}
#[test]
fn transform_rotates_through_angle() {
let a = Transform::IDENTITY;
let b = Transform {
rotation: std::f64::consts::PI,
..a
};
let mid = a.lerp(&b, 0.5);
assert!((mid.rotation - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
assert_eq!(mid.scale, Vec2::new(1.0, 1.0));
}
}