use crate::geometry::{Insets, Point, Rect, Size, Transform, Vec2};
use crate::render::Color;
pub trait Vector: Copy + PartialEq + core::fmt::Debug + Send + Sync + 'static {
const ZERO: Self;
fn plus(self, other: Self) -> Self;
fn minus(self, other: Self) -> Self;
fn times(self, k: f64) -> Self;
fn norm_squared(self) -> f64;
fn norm(self) -> f64 {
self.norm_squared().sqrt()
}
fn lerp(self, other: Self, t: f64) -> Self {
self.plus(other.minus(self).times(t))
}
}
impl Vector for f64 {
const ZERO: Self = 0.0;
#[inline]
fn plus(self, other: Self) -> Self {
self + other
}
#[inline]
fn minus(self, other: Self) -> Self {
self - other
}
#[inline]
fn times(self, k: f64) -> Self {
self * k
}
#[inline]
fn norm_squared(self) -> f64 {
self * self
}
}
impl<const N: usize> Vector for [f64; N] {
const ZERO: Self = [0.0; N];
#[inline]
fn plus(self, other: Self) -> Self {
core::array::from_fn(|i| self[i] + other[i])
}
#[inline]
fn minus(self, other: Self) -> Self {
core::array::from_fn(|i| self[i] - other[i])
}
#[inline]
fn times(self, k: f64) -> Self {
self.map(|c| c * k)
}
#[inline]
fn norm_squared(self) -> f64 {
self.iter().map(|c| c * c).sum()
}
}
impl<A: Vector, B: Vector> Vector for (A, B) {
const ZERO: Self = (A::ZERO, B::ZERO);
fn plus(self, o: Self) -> Self {
(self.0.plus(o.0), self.1.plus(o.1))
}
fn minus(self, o: Self) -> Self {
(self.0.minus(o.0), self.1.minus(o.1))
}
fn times(self, k: f64) -> Self {
(self.0.times(k), self.1.times(k))
}
fn norm_squared(self) -> f64 {
self.0.norm_squared() + self.1.norm_squared()
}
}
impl<A: Vector, B: Vector, C: Vector> Vector for (A, B, C) {
const ZERO: Self = (A::ZERO, B::ZERO, C::ZERO);
fn plus(self, o: Self) -> Self {
(self.0.plus(o.0), self.1.plus(o.1), self.2.plus(o.2))
}
fn minus(self, o: Self) -> Self {
(self.0.minus(o.0), self.1.minus(o.1), self.2.minus(o.2))
}
fn times(self, k: f64) -> Self {
(self.0.times(k), self.1.times(k), self.2.times(k))
}
fn norm_squared(self) -> f64 {
self.0.norm_squared() + self.1.norm_squared() + self.2.norm_squared()
}
}
pub trait Animatable: Copy + PartialEq + Send + Sync + 'static {
type Vector: Vector;
fn to_vector(self) -> Self::Vector;
fn from_vector(v: Self::Vector) -> Self;
fn interpolate(self, to: Self, t: f64) -> Self {
Self::from_vector(self.to_vector().lerp(to.to_vector(), t))
}
}
impl Animatable for f64 {
type Vector = f64;
#[inline]
fn to_vector(self) -> f64 {
self
}
#[inline]
fn from_vector(v: f64) -> Self {
v
}
}
impl<const N: usize> Animatable for [f64; N] {
type Vector = [f64; N];
#[inline]
fn to_vector(self) -> Self {
self
}
#[inline]
fn from_vector(v: Self) -> Self {
v
}
}
impl Animatable for Point {
type Vector = [f64; 2];
fn to_vector(self) -> [f64; 2] {
[self.x, self.y]
}
fn from_vector([x, y]: [f64; 2]) -> Self {
Point::new(x, y)
}
}
impl Animatable for Vec2 {
type Vector = [f64; 2];
fn to_vector(self) -> [f64; 2] {
[self.dx, self.dy]
}
fn from_vector([x, y]: [f64; 2]) -> Self {
Vec2::new(x, y)
}
}
impl Animatable for Transform {
type Vector = [f64; 7];
fn to_vector(self) -> [f64; 7] {
[
self.translate.dx,
self.translate.dy,
self.scale.dx,
self.scale.dy,
self.rotate,
self.origin.x,
self.origin.y,
]
}
fn from_vector([tx, ty, sx, sy, r, ox, oy]: [f64; 7]) -> Self {
Transform {
translate: Vec2::new(tx, ty),
scale: Vec2::new(sx, sy),
rotate: r,
origin: Point::new(ox, oy),
}
}
}
impl Animatable for Size {
type Vector = [f64; 2];
fn to_vector(self) -> [f64; 2] {
[self.width, self.height]
}
fn from_vector([w, h]: [f64; 2]) -> Self {
Size::new(w.max(0.0), h.max(0.0))
}
}
impl Animatable for Rect {
type Vector = [f64; 4];
fn to_vector(self) -> [f64; 4] {
[
self.origin.x,
self.origin.y,
self.size.width,
self.size.height,
]
}
fn from_vector([x, y, w, h]: [f64; 4]) -> Self {
Rect::from_xywh(x, y, w.max(0.0), h.max(0.0))
}
}
impl Animatable for Insets {
type Vector = [f64; 4];
fn to_vector(self) -> [f64; 4] {
[self.top, self.right, self.bottom, self.left]
}
fn from_vector([top, right, bottom, left]: [f64; 4]) -> Self {
Insets {
top,
right,
bottom,
left,
}
}
}
impl Animatable for Color {
type Vector = [f64; 4];
fn to_vector(self) -> [f64; 4] {
let a = self.a as f64 / 255.0;
let c = |v: u8| v as f64 / 255.0 * a;
[c(self.r), c(self.g), c(self.b), a]
}
fn from_vector([r, g, b, a]: [f64; 4]) -> Self {
let a = a.clamp(0.0, 1.0);
let alpha = (a * 255.0).round() as u8;
if alpha == 0 {
return Color::TRANSPARENT;
}
let c = |v: f64| ((v / a).clamp(0.0, 1.0) * 255.0).round() as u8;
Color::rgba(c(r), c(g), c(b), alpha)
}
}
impl<A: Animatable, B: Animatable> Animatable for (A, B) {
type Vector = (A::Vector, B::Vector);
fn to_vector(self) -> Self::Vector {
(self.0.to_vector(), self.1.to_vector())
}
fn from_vector(v: Self::Vector) -> Self {
(A::from_vector(v.0), B::from_vector(v.1))
}
}
impl<A: Animatable, B: Animatable, C: Animatable> Animatable for (A, B, C) {
type Vector = (A::Vector, B::Vector, C::Vector);
fn to_vector(self) -> Self::Vector {
(self.0.to_vector(), self.1.to_vector(), self.2.to_vector())
}
fn from_vector(v: Self::Vector) -> Self {
(
A::from_vector(v.0),
B::from_vector(v.1),
C::from_vector(v.2),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn colors_round_trip_and_blend_premultiplied() {
for c in [
Color::rgb(12, 200, 99),
Color::rgba(255, 0, 0, 128),
Color::rgba(1, 2, 3, 1),
Color::WHITE,
] {
assert_eq!(Color::from_vector(c.to_vector()), c);
}
let (a, b) = (Color::rgb(0, 100, 200), Color::rgb(255, 50, 0));
for t in [0.0, 0.25, 0.5, 0.8, 1.0] {
assert_eq!(a.interpolate(b, t), a.mix(b, t), "t={t}");
}
let mid = Color::rgb(255, 0, 0).interpolate(Color::TRANSPARENT, 0.5);
assert_eq!((mid.r, mid.g, mid.b), (255, 0, 0));
assert!((mid.a as i32 - 128).abs() <= 1);
let over = Color::BLACK.interpolate(Color::WHITE, 1.5);
assert_eq!(over, Color::WHITE);
}
#[test]
fn geometry_interpolates_componentwise() {
let r = Rect::from_xywh(0.0, 0.0, 10.0, 20.0)
.interpolate(Rect::from_xywh(10.0, 20.0, 30.0, 40.0), 0.5);
assert_eq!(r, Rect::from_xywh(5.0, 10.0, 20.0, 30.0));
let s = Size::new(10.0, 10.0).interpolate(Size::ZERO, 2.0);
assert_eq!(s, Size::ZERO, "sizes clamp at zero");
let p = (Point::new(0.0, 0.0), 1.0).interpolate((Point::new(2.0, 4.0), 3.0), 0.5);
assert_eq!(p, (Point::new(1.0, 2.0), 2.0));
let i = Insets::uniform(4.0).interpolate(Insets::uniform(8.0), 0.25);
assert_eq!(i, Insets::uniform(5.0));
}
}
#[cfg(test)]
mod transform_tests {
use super::*;
#[test]
fn transforms_animate_part_by_part_like_lerp() {
let a = Transform::IDENTITY;
let b = Transform::IDENTITY
.scale(2.0)
.rotate(1.0)
.translate(10.0, -4.0);
for t in [0.0, 0.25, 0.5, 1.0] {
assert_eq!(a.interpolate(b, t), Transform::lerp(a, b, t));
}
}
}