pub type Affine2D = [f32; 6];
pub const IDENTITY: Affine2D = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
pub fn affine_inverse(m: &Affine2D) -> Option<Affine2D> {
let [a, b, tx, c, d, ty] = *m;
let det = a * d - b * c;
if det.abs() < 1e-12 {
return None;
}
let inv_det = 1.0 / det;
Some([
d * inv_det,
-b * inv_det,
(b * ty - d * tx) * inv_det,
-c * inv_det,
a * inv_det,
(c * tx - a * ty) * inv_det,
])
}
pub fn affine_transform(m: &Affine2D, x: f32, y: f32) -> (f32, f32) {
let [a, b, tx, c, d, ty] = *m;
(a * x + b * y + tx, c * x + d * y + ty)
}
pub fn affine_multiply(a: &Affine2D, b: &Affine2D) -> Affine2D {
[
a[0] * b[0] + a[1] * b[3],
a[0] * b[1] + a[1] * b[4],
a[0] * b[2] + a[1] * b[5] + a[2],
a[3] * b[0] + a[4] * b[3],
a[3] * b[1] + a[4] * b[4],
a[3] * b[2] + a[4] * b[5] + a[5],
]
}
pub fn affine_translate(tx: f32, ty: f32) -> Affine2D {
[1.0, 0.0, tx, 0.0, 1.0, ty]
}
pub fn affine_scale(sx: f32, sy: f32) -> Affine2D {
[sx, 0.0, 0.0, 0.0, sy, 0.0]
}
pub fn affine_rotate(angle: f32) -> Affine2D {
let (s, c) = angle.sin_cos();
[c, -s, 0.0, s, c, 0.0]
}
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "mode", content = "data")]
pub enum Transform {
Basic(Affine2D),
}
impl Default for Transform {
fn default() -> Self {
Transform::Basic(IDENTITY)
}
}
impl Transform {
pub const fn identity() -> Self {
Transform::Basic(IDENTITY)
}
pub fn from_affine(m: Affine2D) -> Self {
Transform::Basic(m)
}
pub fn to_affine(&self) -> Affine2D {
match self {
Transform::Basic(m) => *m,
}
}
pub fn mode_tag(&self) -> u32 {
match self {
Transform::Basic(_) => 0,
}
}
pub fn decompose(&self) -> Decomposed {
let [a, b, tx, c, d, ty] = self.to_affine();
let sx = (a * a + c * c).sqrt();
let rotation = c.atan2(a);
let det = a * d - b * c;
let sy = if sx.abs() > 1e-12 { det / sx } else { 0.0 };
Decomposed {
offset: (tx, ty),
rotation,
scale: (sx, sy),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Decomposed {
pub offset: (f32, f32),
pub rotation: f32,
pub scale: (f32, f32),
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) {
assert!((a - b).abs() < 1e-4, "expected {b}, got {a}");
}
#[test]
fn affine_contract() {
let m: Affine2D = [2.0, 0.0, 10.0, 0.0, 3.0, 20.0];
let (x, y) = affine_transform(&m, 5.0, 7.0);
approx(x, 2.0 * 5.0 + 10.0); approx(y, 3.0 * 7.0 + 20.0); }
#[test]
fn inverse_round_trips() {
let m = affine_multiply(
&affine_translate(13.0, -4.0),
&affine_multiply(&affine_rotate(0.7), &affine_scale(2.0, 0.5)),
);
let inv = affine_inverse(&m).expect("invertible");
let (x, y) = affine_transform(&m, 9.0, -3.0);
let (rx, ry) = affine_transform(&inv, x, y);
approx(rx, 9.0);
approx(ry, -3.0);
}
#[test]
fn multiply_applies_b_first() {
let scale = affine_scale(2.0, 2.0);
let translate = affine_translate(1.0, 0.0);
let m = affine_multiply(&scale, &translate);
let (x, _) = affine_transform(&m, 3.0, 0.0);
approx(x, (3.0 + 1.0) * 2.0); }
#[test]
fn default_is_identity() {
assert_eq!(Transform::default(), Transform::Basic(IDENTITY));
assert_eq!(Transform::identity().to_affine(), IDENTITY);
}
#[test]
fn serde_round_trip_tagged() {
let t = Transform::from_affine([1.5, 0.0, 4.0, 0.0, 2.0, -3.0]);
let json = serde_json::to_string(&t).unwrap();
assert!(json.contains("Basic"), "tagged enum: {json}");
let back: Transform = serde_json::from_str(&json).unwrap();
assert_eq!(t, back);
}
#[test]
fn decompose_trs_no_shear() {
let angle = 0.5_f32;
let m = affine_multiply(&affine_rotate(angle), &affine_scale(3.0, 2.0));
let d = Transform::Basic([m[0], m[1], 12.0, m[3], m[4], -7.0]).decompose();
approx(d.offset.0, 12.0);
approx(d.offset.1, -7.0);
approx(d.rotation, angle);
approx(d.scale.0, 3.0);
approx(d.scale.1, 2.0);
}
}