#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Vec2 {
pub x: f32,
pub y: f32,
}
impl Vec2 {
pub const ZERO: Vec2 = Vec2 { x: 0.0, y: 0.0 };
}
impl std::ops::Add for Vec2 {
type Output = Vec2;
fn add(self, other: Vec2) -> Vec2 {
Vec2 {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
impl std::ops::Sub for Vec2 {
type Output = Vec2;
fn sub(self, other: Vec2) -> Vec2 {
Vec2 {
x: self.x - other.x,
y: self.y - other.y,
}
}
}
impl std::ops::Neg for Vec2 {
type Output = Vec2;
fn neg(self) -> Vec2 {
Vec2 {
x: -self.x,
y: -self.y,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Size {
pub width: f32,
pub height: f32,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Rect {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
impl Rect {
pub fn contains(&self, p: Vec2) -> bool {
p.x >= self.x && p.x <= self.x + self.w && p.y >= self.y && p.y <= self.y + self.h
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Transform {
pub translate_x: f32,
pub translate_y: f32,
pub scale_x: f32,
pub scale_y: f32,
pub rotate: f32, pub shear_x: f32,
pub shear_y: f32,
pub origin_x: f32,
pub origin_y: f32,
}
impl Transform {
pub fn identity() -> Self {
Self {
translate_x: 0.0,
translate_y: 0.0,
scale_x: 1.0,
scale_y: 1.0,
rotate: 0.0,
shear_x: 0.0,
shear_y: 0.0,
origin_x: 0.5,
origin_y: 0.5,
}
}
pub fn translate(x: f32, y: f32) -> Self {
Self {
translate_x: x,
translate_y: y,
scale_x: 1.0,
scale_y: 1.0,
rotate: 0.0,
shear_x: 0.0,
shear_y: 0.0,
origin_x: 0.5,
origin_y: 0.5,
}
}
pub fn linear(&self) -> [f32; 4] {
let c = self.rotate.cos();
let s = self.rotate.sin();
let hsx = self.scale_x;
let hsy = self.scale_y;
let k00 = hsx;
let k01 = self.shear_x * hsy;
let k10 = self.shear_y * hsx;
let k11 = hsy;
[
c * k00 - s * k10,
c * k01 - s * k11,
s * k00 + c * k10,
s * k01 + c * k11,
]
}
pub fn inverse_linear(&self) -> Option<[f32; 4]> {
let m = self.linear();
let det = m[0] * m[3] - m[1] * m[2];
if det.abs() < 1e-12 {
return None;
}
Some([m[3] / det, -m[1] / det, -m[2] / det, m[0] / det])
}
pub fn linear_is_identity(&self) -> bool {
self.scale_x == 1.0
&& self.scale_y == 1.0
&& self.rotate == 0.0
&& self.shear_x == 0.0
&& self.shear_y == 0.0
}
pub fn apply_to_point(&self, p: Vec2) -> Vec2 {
let ox = self.origin_x;
let oy = self.origin_y;
let m = self.linear();
let x = p.x - ox;
let y = p.y - oy;
Vec2 {
x: m[0] * x + m[1] * y + ox + self.translate_x,
y: m[2] * x + m[3] * y + oy + self.translate_y,
}
}
pub fn apply_to_rect(&self, r: Rect) -> Rect {
let ox = r.x + r.w * self.origin_x;
let oy = r.y + r.h * self.origin_y;
let m = self.linear();
let corners = [
Vec2 { x: r.x, y: r.y },
Vec2 {
x: r.x + r.w,
y: r.y,
},
Vec2 {
x: r.x,
y: r.y + r.h,
},
Vec2 {
x: r.x + r.w,
y: r.y + r.h,
},
];
let mut min_x = f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = f32::MIN;
let mut max_y = f32::MIN;
for c in corners {
let x = c.x - ox;
let y = c.y - oy;
let tx = m[0] * x + m[1] * y + ox + self.translate_x;
let ty = m[2] * x + m[3] * y + oy + self.translate_y;
min_x = min_x.min(tx);
min_y = min_y.min(ty);
max_x = max_x.max(tx);
max_y = max_y.max(ty);
}
Rect {
x: min_x,
y: min_y,
w: max_x - min_x,
h: max_y - min_y,
}
}
pub fn combine(&self, other: &Transform) -> Transform {
let a = self.linear();
let b = other.linear();
let m = [
a[0] * b[0] + a[1] * b[2],
a[0] * b[1] + a[1] * b[3],
a[2] * b[0] + a[3] * b[2],
a[2] * b[1] + a[3] * b[3],
];
let translate_x = a[0] * other.translate_x + a[1] * other.translate_y + self.translate_x;
let translate_y = a[2] * other.translate_x + a[3] * other.translate_y + self.translate_y;
let (scale_x, scale_y, rotate, shear_x, shear_y) =
decompose_linear(m).unwrap_or((1.0, 1.0, 0.0, 0.0, 0.0));
Transform {
translate_x,
translate_y,
scale_x,
scale_y,
rotate,
shear_x,
shear_y,
origin_x: self.origin_x,
origin_y: self.origin_y,
}
}
}
fn decompose_linear(m: [f32; 4]) -> Option<(f32, f32, f32, f32, f32)> {
let (mut a, mut b, c, d) = (m[0], m[1], m[2], m[3]);
let mut angle_sign = 1.0;
if a * d - b * c < 0.0 {
a = -a;
b = -b;
angle_sign = -1.0;
}
let e = a * a + c * c;
let f = a * b + c * d;
let g = b * b + d * d;
let det_p = (e * g - f * f).max(0.0);
let s = (e + g + 2.0 * det_p.sqrt()).sqrt();
if !(s > 1e-12) {
return None;
}
let root_det = det_p.sqrt();
let k00 = (e + root_det) / s;
let k01 = f / s;
let k10 = f / s;
let k11 = (g + root_det) / s;
let det_k = (k00 * k11 - k01 * k10).max(1e-24);
let r00 = (a * k11 - b * k10) / det_k;
let r10 = (c * k11 - d * k10) / det_k;
let rotate = r10.atan2(r00);
let (sx, sy) = (k00, k11);
if sx.abs() < 1e-12 || sy.abs() < 1e-12 {
return None;
}
if angle_sign < 0.0 {
return Some((-sx, sy, -rotate, -(k01 / sy), -(k10 / sx)));
}
Some((sx, sy, rotate, k01 / sy, k10 / sx))
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-3
}
fn transform(tx: f32, ty: f32, sx: f32, sy: f32, rot: f32, hx: f32, hy: f32) -> Transform {
Transform {
translate_x: tx,
translate_y: ty,
scale_x: sx,
scale_y: sy,
rotate: rot,
shear_x: hx,
shear_y: hy,
origin_x: 0.0,
origin_y: 0.0,
}
}
#[test]
fn shear_free_matches_legacy_formula() {
let t = transform(5.0, -3.0, 2.0, 0.5, 0.7, 0.0, 0.0);
let p = Vec2 { x: 11.0, y: 7.0 };
let got = t.apply_to_point(p);
let (c, s) = (0.7f32.cos(), 0.7f32.sin());
let x = (p.x) * 2.0;
let y = (p.y) * 0.5;
assert!(approx(got.x, x * c - y * s + 5.0));
assert!(approx(got.y, x * s + y * c - 3.0));
}
#[test]
fn shear_order_is_scale_then_shear_then_rotate() {
let t = transform(0.0, 0.0, 2.0, 3.0, 0.0, 1.0, 0.0);
let got = t.apply_to_point(Vec2 { x: 1.0, y: 1.0 });
assert!(approx(got.x, 2.0 + 3.0));
assert!(approx(got.y, 3.0));
}
#[test]
fn combine_round_trips_through_apply() {
let cases = [
(
transform(3.0, 4.0, 1.0, 1.0, 0.0, 0.0, 0.0),
transform(0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 0.0),
),
(
transform(1.0, 2.0, 1.5, 0.5, 0.6, 0.0, 0.0),
transform(-2.0, 1.0, 0.7, 1.3, -0.4, 0.0, 0.0),
),
(
transform(0.0, 0.0, 2.0, 0.5, 0.9, 0.0, 0.0),
transform(4.0, -1.0, 1.0, 1.0, 0.3, 0.0, 0.0),
),
(
transform(2.0, 0.0, 1.0, 1.0, 0.2, 0.8, -0.3),
transform(0.0, 5.0, 1.2, 0.9, -0.5, 0.4, 0.1),
),
(
transform(0.0, 0.0, -1.0, 1.0, 0.0, 0.0, 0.0),
transform(7.0, 7.0, 1.0, 1.0, 1.1, 0.2, 0.0),
),
];
let points = [
Vec2 { x: 0.0, y: 0.0 },
Vec2 { x: 10.0, y: -4.0 },
Vec2 { x: -3.5, y: 8.25 },
Vec2 { x: 100.0, y: 200.0 },
];
for (a, b) in cases {
let combined = a.combine(&b);
for p in points {
let expect = a.apply_to_point(b.apply_to_point(p));
let got = combined.apply_to_point(p);
assert!(
approx(got.x, expect.x) && approx(got.y, expect.y),
"combine mismatch: {a:?} then {b:?} at {p:?}: got {got:?}, want {expect:?}"
);
}
}
}
#[test]
fn inverse_linear_round_trips() {
let t = transform(3.0, -2.0, 1.5, 0.75, 0.8, 0.6, -0.2);
let m = t.linear();
let n = t.inverse_linear().expect("invertible");
let id = [
n[0] * m[0] + n[1] * m[2],
n[0] * m[1] + n[1] * m[3],
n[2] * m[0] + n[3] * m[2],
n[2] * m[1] + n[3] * m[3],
];
assert!(approx(id[0], 1.0) && approx(id[3], 1.0));
assert!(approx(id[1], 0.0) && approx(id[2], 0.0));
}
}