1#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct Affine2D {
14 pub a: f32,
16 pub b: f32,
18 pub c: f32,
20 pub d: f32,
22 pub tx: f32,
24 pub ty: f32,
26}
27
28impl Affine2D {
29 pub const IDENTITY: Self = Self {
31 a: 1.0,
32 b: 0.0,
33 c: 0.0,
34 d: 1.0,
35 tx: 0.0,
36 ty: 0.0,
37 };
38
39 pub fn translate(tx: f32, ty: f32) -> Self {
41 Self {
42 a: 1.0,
43 b: 0.0,
44 c: 0.0,
45 d: 1.0,
46 tx,
47 ty,
48 }
49 }
50
51 pub fn scale(sx: f32, sy: f32) -> Self {
53 Self {
54 a: sx,
55 b: 0.0,
56 c: 0.0,
57 d: sy,
58 tx: 0.0,
59 ty: 0.0,
60 }
61 }
62
63 pub fn rotate(angle: f32) -> Self {
65 let (s, c) = angle.sin_cos();
66 Self {
67 a: c,
68 b: -s,
69 c: s,
70 d: c,
71 tx: 0.0,
72 ty: 0.0,
73 }
74 }
75
76 #[allow(clippy::suspicious_operation_groupings)]
78 pub fn then(self, other: Self) -> Self {
79 Self {
80 a: (other.a * self.a) + (other.b * self.c),
81 b: (other.a * self.b) + (other.b * self.d),
82 c: (other.c * self.a) + (other.d * self.c),
83 d: (other.c * self.b) + (other.d * self.d),
84 tx: other.a * self.tx + other.b * self.ty + other.tx,
85 ty: other.c * self.tx + other.d * self.ty + other.ty,
86 }
87 }
88
89 pub fn apply(self, p: [f32; 2]) -> [f32; 2] {
91 [
92 self.a * p[0] + self.b * p[1] + self.tx,
93 self.c * p[0] + self.d * p[1] + self.ty,
94 ]
95 }
96
97 pub fn to_mat3_cols(self) -> [f32; 12] {
99 [
101 self.a, self.c, 0.0, 0.0, self.b, self.d, 0.0, 0.0, self.tx, self.ty, 1.0, 0.0, ]
105 }
106}
107
108impl Default for Affine2D {
109 fn default() -> Self {
110 Self::IDENTITY
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn identity() {
120 let p = Affine2D::IDENTITY.apply([3.0, 4.0]);
121 assert!((p[0] - 3.0).abs() < 1e-6);
122 assert!((p[1] - 4.0).abs() < 1e-6);
123 }
124
125 #[test]
126 fn translate() {
127 let p = Affine2D::translate(10.0, 20.0).apply([1.0, 2.0]);
128 assert!((p[0] - 11.0).abs() < 1e-6);
129 assert!((p[1] - 22.0).abs() < 1e-6);
130 }
131
132 #[test]
133 fn scale() {
134 let p = Affine2D::scale(2.0, 3.0).apply([4.0, 5.0]);
135 assert!((p[0] - 8.0).abs() < 1e-6);
136 assert!((p[1] - 15.0).abs() < 1e-6);
137 }
138
139 #[test]
140 fn compose() {
141 let t = Affine2D::translate(10.0, 0.0);
142 let s = Affine2D::scale(2.0, 2.0);
143 let combined = s.then(t);
145 let p = combined.apply([5.0, 0.0]);
146 assert!((p[0] - 20.0).abs() < 1e-5);
147 }
148
149 #[test]
150 fn rotate_90() {
151 let r = Affine2D::rotate(std::f32::consts::FRAC_PI_2);
152 let p = r.apply([1.0, 0.0]);
153 assert!(p[0].abs() < 1e-5);
154 assert!((p[1] - 1.0).abs() < 1e-5);
155 }
156}