use std::ops::Mul;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Affine2 {
pub a: f32,
pub b: f32,
pub c: f32,
pub d: f32,
pub tx: f32,
pub ty: f32,
}
impl Affine2 {
pub const IDENTITY: Affine2 = Affine2 {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
tx: 0.0,
ty: 0.0,
};
pub const fn translate(x: f32, y: f32) -> Self {
Self {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
tx: x,
ty: y,
}
}
pub const fn scale(s: f32) -> Self {
Self::scale_xy(s, s)
}
pub const fn scale_xy(sx: f32, sy: f32) -> Self {
Self {
a: sx,
b: 0.0,
c: 0.0,
d: sy,
tx: 0.0,
ty: 0.0,
}
}
pub fn rotate(radians: f32) -> Self {
let (s, c) = radians.sin_cos();
Self {
a: c,
b: s,
c: -s,
d: c,
tx: 0.0,
ty: 0.0,
}
}
pub fn transform_point(self, x: f32, y: f32) -> (f32, f32) {
(
self.a * x + self.c * y + self.tx,
self.b * x + self.d * y + self.ty,
)
}
pub fn is_identity(self) -> bool {
self == Self::IDENTITY
}
}
impl Default for Affine2 {
fn default() -> Self {
Self::IDENTITY
}
}
impl Mul for Affine2 {
type Output = Affine2;
fn mul(self, rhs: Affine2) -> Affine2 {
Affine2 {
a: self.a * rhs.a + self.c * rhs.b,
b: self.b * rhs.a + self.d * rhs.b,
c: self.a * rhs.c + self.c * rhs.d,
d: self.b * rhs.c + self.d * rhs.d,
tx: self.a * rhs.tx + self.c * rhs.ty + self.tx,
ty: self.b * rhs.tx + self.d * rhs.ty + self.ty,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-5
}
#[test]
fn identity_leaves_points_unchanged() {
let m = Affine2::IDENTITY;
assert_eq!(m.transform_point(3.0, 4.0), (3.0, 4.0));
}
#[test]
fn translate_offsets_points() {
let m = Affine2::translate(2.0, 3.0);
assert_eq!(m.transform_point(0.0, 0.0), (2.0, 3.0));
assert_eq!(m.transform_point(1.0, 1.0), (3.0, 4.0));
}
#[test]
fn scale_uniform_and_xy() {
assert_eq!(Affine2::scale(2.0).transform_point(3.0, 4.0), (6.0, 8.0));
assert_eq!(
Affine2::scale_xy(-1.0, 1.0).transform_point(5.0, 5.0),
(-5.0, 5.0)
);
}
#[test]
fn rotate_quarter_turn() {
let m = Affine2::rotate(std::f32::consts::FRAC_PI_2);
let (x, y) = m.transform_point(1.0, 0.0);
assert!(approx_eq(x, 0.0) && approx_eq(y, 1.0));
}
#[test]
fn composition_applies_right_factor_first() {
let m = Affine2::translate(5.0, 0.0) * Affine2::rotate(std::f32::consts::FRAC_PI_2);
let (x, y) = m.transform_point(1.0, 0.0);
assert!(approx_eq(x, 5.0) && approx_eq(y, 1.0));
}
#[test]
fn composition_with_identity_is_idempotent() {
let m = Affine2::rotate(0.4) * Affine2::scale(1.5);
assert_eq!(m * Affine2::IDENTITY, m);
assert_eq!(Affine2::IDENTITY * m, m);
}
#[test]
fn is_identity_only_for_identity() {
assert!(Affine2::IDENTITY.is_identity());
assert!(!Affine2::translate(0.0, 1.0).is_identity());
assert!(!Affine2::scale(1.000_001).is_identity());
}
}