use glam::{Mat3, Vec2};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Transform2D {
matrix: Mat3,
}
impl Default for Transform2D {
fn default() -> Self {
Self::IDENTITY
}
}
impl Transform2D {
pub const IDENTITY: Self = Self {
matrix: Mat3::IDENTITY,
};
pub fn from_mat3(matrix: Mat3) -> Self {
Self { matrix }
}
pub fn translate(offset: Vec2) -> Self {
Self {
matrix: Mat3::from_translation(offset),
}
}
pub fn rotate(angle: f32) -> Self {
Self {
matrix: Mat3::from_angle(angle),
}
}
pub fn scale(factor: f32) -> Self {
Self {
matrix: Mat3::from_scale(Vec2::splat(factor)),
}
}
pub fn scale_xy(scale: Vec2) -> Self {
Self {
matrix: Mat3::from_scale(scale),
}
}
pub fn skew(skew_x: f32, skew_y: f32) -> Self {
Self {
matrix: Mat3::from_cols(
glam::Vec3::new(1.0, skew_y.tan(), 0.0),
glam::Vec3::new(skew_x.tan(), 1.0, 0.0),
glam::Vec3::new(0.0, 0.0, 1.0),
),
}
}
pub fn then(&self, other: &Transform2D) -> Self {
Self {
matrix: other.matrix * self.matrix,
}
}
pub fn then_translate(&self, offset: Vec2) -> Self {
self.then(&Transform2D::translate(offset))
}
pub fn then_rotate(&self, angle: f32) -> Self {
self.then(&Transform2D::rotate(angle))
}
pub fn then_scale(&self, factor: f32) -> Self {
self.then(&Transform2D::scale(factor))
}
pub fn then_scale_xy(&self, scale: Vec2) -> Self {
self.then(&Transform2D::scale_xy(scale))
}
pub fn transform_point(&self, point: Vec2) -> Vec2 {
self.matrix.transform_point2(point)
}
pub fn transform_vector(&self, vector: Vec2) -> Vec2 {
self.matrix.transform_vector2(vector)
}
pub fn inverse(&self) -> Option<Self> {
let det = self.matrix.determinant();
if det.abs() < f32::EPSILON {
None
} else {
Some(Self {
matrix: self.matrix.inverse(),
})
}
}
pub fn as_mat3(&self) -> &Mat3 {
&self.matrix
}
pub fn translation(&self) -> Vec2 {
Vec2::new(self.matrix.z_axis.x, self.matrix.z_axis.y)
}
pub fn scale_factor(&self) -> Vec2 {
Vec2::new(
Vec2::new(self.matrix.x_axis.x, self.matrix.x_axis.y).length(),
Vec2::new(self.matrix.y_axis.x, self.matrix.y_axis.y).length(),
)
}
pub fn rotation(&self) -> f32 {
self.matrix.x_axis.y.atan2(self.matrix.x_axis.x)
}
}
impl std::ops::Mul<Transform2D> for Transform2D {
type Output = Transform2D;
fn mul(self, rhs: Transform2D) -> Transform2D {
self.then(&rhs)
}
}
impl std::ops::Mul<Vec2> for Transform2D {
type Output = Vec2;
fn mul(self, rhs: Vec2) -> Vec2 {
self.transform_point(rhs)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f32::consts::PI;
#[test]
fn test_identity() {
let t = Transform2D::IDENTITY;
let point = Vec2::new(10.0, 20.0);
assert_eq!(t.transform_point(point), point);
}
#[test]
fn test_translate() {
let t = Transform2D::translate(Vec2::new(5.0, 10.0));
let point = Vec2::new(10.0, 20.0);
assert_eq!(t.transform_point(point), Vec2::new(15.0, 30.0));
}
#[test]
fn test_scale() {
let t = Transform2D::scale(2.0);
let point = Vec2::new(10.0, 20.0);
assert_eq!(t.transform_point(point), Vec2::new(20.0, 40.0));
}
#[test]
fn test_rotate_90() {
let t = Transform2D::rotate(PI / 2.0);
let point = Vec2::new(1.0, 0.0);
let result = t.transform_point(point);
assert!((result.x - 0.0).abs() < 0.001);
assert!((result.y - 1.0).abs() < 0.001);
}
#[test]
fn test_chain_transforms() {
let t = Transform2D::translate(Vec2::new(10.0, 0.0)).then_scale(2.0);
let point = Vec2::new(5.0, 5.0);
let result = t.transform_point(point);
assert_eq!(result, Vec2::new(30.0, 10.0));
}
#[test]
fn test_inverse() {
let t = Transform2D::translate(Vec2::new(10.0, 20.0)).then_scale(2.0);
let inv = t.inverse().unwrap();
let point = Vec2::new(5.0, 5.0);
let transformed = t.transform_point(point);
let restored = inv.transform_point(transformed);
assert!((restored - point).length() < 0.001);
}
}