use crate::geometry::{Affine, Point, Rect, Vec2};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Transform {
pub translate: Vec2,
pub scale: Vec2,
pub rotate: f64,
pub origin: Point,
}
impl Default for Transform {
fn default() -> Self {
Self::IDENTITY
}
}
impl Transform {
pub const IDENTITY: Self = Self {
translate: Vec2::new(0.0, 0.0),
scale: Vec2::new(1.0, 1.0),
rotate: 0.0,
origin: Point::new(0.5, 0.5),
};
pub fn translate(mut self, dx: f64, dy: f64) -> Self {
self.translate = Vec2::new(self.translate.dx + dx, self.translate.dy + dy);
self
}
pub fn scale(self, s: f64) -> Self {
self.scale_xy(s, s)
}
pub fn scale_xy(mut self, sx: f64, sy: f64) -> Self {
self.scale = Vec2::new(self.scale.dx * sx, self.scale.dy * sy);
self
}
pub fn rotate(mut self, radians: f64) -> Self {
self.rotate += radians;
self
}
pub fn rotate_degrees(self, degrees: f64) -> Self {
self.rotate(degrees.to_radians())
}
pub fn origin(mut self, x: f64, y: f64) -> Self {
self.origin = Point::new(x, y);
self
}
pub fn is_identity(&self) -> bool {
self.translate == Vec2::new(0.0, 0.0)
&& self.scale == Vec2::new(1.0, 1.0)
&& self.rotate == 0.0
}
pub fn lerp(a: Transform, b: Transform, t: f64) -> Transform {
let mix = |x: f64, y: f64| x + (y - x) * t;
Transform {
translate: Vec2::new(
mix(a.translate.dx, b.translate.dx),
mix(a.translate.dy, b.translate.dy),
),
scale: Vec2::new(mix(a.scale.dx, b.scale.dx), mix(a.scale.dy, b.scale.dy)),
rotate: mix(a.rotate, b.rotate),
origin: Point::new(mix(a.origin.x, b.origin.x), mix(a.origin.y, b.origin.y)),
}
}
pub fn origin_in(&self, bounds: Rect) -> Point {
Point::new(
bounds.min_x() + self.origin.x * bounds.width(),
bounds.min_y() + self.origin.y * bounds.height(),
)
}
pub fn to_affine(&self, bounds: Rect) -> Affine {
let o = self.origin_in(bounds);
Affine::translate(Vec2::new(-o.x, -o.y))
.then(Affine::scale(self.scale.dx, self.scale.dy))
.then(Affine::rotate(self.rotate))
.then(Affine::translate(Vec2::new(
o.x + self.translate.dx,
o.y + self.translate.dy,
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: Point, b: Point) -> bool {
(a.x - b.x).abs() < 1e-9 && (a.y - b.y).abs() < 1e-9
}
#[test]
fn identity_maps_points_to_themselves() {
let a = Transform::IDENTITY.to_affine(Rect::from_xywh(10.0, 20.0, 30.0, 40.0));
assert_eq!(a, Affine::IDENTITY);
assert!(Transform::default().is_identity());
}
#[test]
fn scale_is_about_the_origin() {
let b = Rect::from_xywh(0.0, 0.0, 100.0, 50.0);
let a = Transform::IDENTITY.scale(2.0).to_affine(b);
assert!(close(
a.apply(Point::new(50.0, 25.0)),
Point::new(50.0, 25.0)
));
assert!(close(
a.apply(Point::new(0.0, 0.0)),
Point::new(-50.0, -25.0)
));
let a = Transform::IDENTITY.scale(2.0).origin(0.0, 0.0).to_affine(b);
assert!(close(a.apply(Point::new(0.0, 0.0)), Point::ORIGIN));
assert!(close(
a.apply(Point::new(10.0, 5.0)),
Point::new(20.0, 10.0)
));
}
#[test]
fn rotation_is_clockwise_on_screen() {
let b = Rect::from_xywh(0.0, 0.0, 20.0, 20.0);
let a = Transform::IDENTITY.rotate_degrees(90.0).to_affine(b);
assert!(close(
a.apply(Point::new(10.0, 0.0)),
Point::new(20.0, 10.0)
));
}
#[test]
fn parts_apply_in_a_fixed_order() {
let b = Rect::from_xywh(0.0, 0.0, 20.0, 20.0);
let x = Transform::IDENTITY
.translate(5.0, 0.0)
.rotate_degrees(90.0)
.scale(2.0);
let y = Transform::IDENTITY
.scale(2.0)
.rotate_degrees(90.0)
.translate(5.0, 0.0);
assert_eq!(x, y);
let p = x.to_affine(b).apply(Point::new(20.0, 10.0));
assert!(close(p, Point::new(15.0, 30.0)), "{p:?}");
}
#[test]
fn lerp_interpolates_each_part() {
let a = Transform::IDENTITY;
let b = Transform::IDENTITY
.translate(10.0, 0.0)
.scale(3.0)
.rotate(2.0);
let m = Transform::lerp(a, b, 0.5);
assert_eq!(m.translate, Vec2::new(5.0, 0.0));
assert_eq!(m.scale, Vec2::new(2.0, 2.0));
assert_eq!(m.rotate, 1.0);
assert_eq!(Transform::lerp(a, b, 1.0), b);
}
}