1use super::{Point, Vector2};
4
5#[derive(Clone, Copy, Debug, PartialEq)]
12pub struct Transform {
13 coefficients: [f64; 6],
14}
15
16impl Default for Transform {
17 fn default() -> Self {
18 Self::IDENTITY
19 }
20}
21
22impl Transform {
23 pub const IDENTITY: Self = Self::from_coefficients([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
25
26 pub const fn from_coefficients(coefficients: [f64; 6]) -> Self {
28 Self { coefficients }
29 }
30
31 pub const fn coefficients(self) -> [f64; 6] {
33 self.coefficients
34 }
35
36 pub const fn translate(horizontal: f64, vertical: f64) -> Self {
38 Self::from_coefficients([1.0, 0.0, 0.0, 1.0, horizontal, vertical])
39 }
40
41 pub fn rotate(radians: f64) -> Self {
43 let direction = Vector2::from_angle(radians);
44 Self::from_coefficients([
45 direction.x,
46 direction.y,
47 -direction.y,
48 direction.x,
49 0.0,
50 0.0,
51 ])
52 }
53
54 pub const fn scale(factor: f64) -> Self {
56 Self::scale_non_uniform(factor, factor)
57 }
58
59 pub const fn scale_non_uniform(horizontal: f64, vertical: f64) -> Self {
61 Self::from_coefficients([horizontal, 0.0, 0.0, vertical, 0.0, 0.0])
62 }
63
64 pub fn skew(horizontal: f64, vertical: f64) -> Self {
67 Self::from_coefficients([
68 1.0,
69 libm::tan(vertical),
70 libm::tan(horizontal),
71 1.0,
72 0.0,
73 0.0,
74 ])
75 }
76
77 pub fn then(self, next: Self) -> Self {
85 let first = self.coefficients;
86 let second = next.coefficients;
87 let translation = next.apply(Point::new(first[4], first[5]));
88 Self::from_coefficients([
89 second[0] * first[0] + second[2] * first[1],
90 second[1] * first[0] + second[3] * first[1],
91 second[0] * first[2] + second[2] * first[3],
92 second[1] * first[2] + second[3] * first[3],
93 translation.x,
94 translation.y,
95 ])
96 }
97
98 pub fn apply(self, point: Point) -> Point {
100 let matrix = self.coefficients;
101 Point::new(
102 matrix[0] * point.x + matrix[2] * point.y + matrix[4],
103 matrix[1] * point.x + matrix[3] * point.y + matrix[5],
104 )
105 }
106
107 pub fn apply_vector(self, vector: Vector2) -> Vector2 {
109 let matrix = self.coefficients;
110 Vector2::new(
111 matrix[0] * vector.x + matrix[2] * vector.y,
112 matrix[1] * vector.x + matrix[3] * vector.y,
113 )
114 }
115
116 pub fn is_finite(self) -> bool {
118 self.coefficients.iter().all(|value| value.is_finite())
119 }
120
121 pub fn inverse(self) -> Option<Self> {
126 if !self.is_finite() {
127 return None;
128 }
129 let matrix = self.coefficients;
130 let horizontal_scale = matrix[0].abs().max(matrix[2].abs());
131 let vertical_scale = matrix[1].abs().max(matrix[3].abs());
132 if horizontal_scale == 0.0 || vertical_scale == 0.0 {
133 return None;
134 }
135 let normalized = [
136 matrix[0] / horizontal_scale,
137 matrix[1] / vertical_scale,
138 matrix[2] / horizontal_scale,
139 matrix[3] / vertical_scale,
140 ];
141 let determinant = normalized[0] * normalized[3] - normalized[1] * normalized[2];
142 if determinant == 0.0 {
143 return None;
144 }
145 let divide = |coefficient: f64, scale: f64| {
146 let quotient = coefficient / determinant;
147 if quotient.is_finite() {
148 quotient / scale
149 } else {
150 (coefficient / scale) / determinant
151 }
152 };
153 let linear = [
154 divide(normalized[3], horizontal_scale),
155 divide(-normalized[1], horizontal_scale),
156 divide(-normalized[2], vertical_scale),
157 divide(normalized[0], vertical_scale),
158 ];
159 let inverse = Self::from_coefficients([
160 linear[0],
161 linear[1],
162 linear[2],
163 linear[3],
164 -linear[0] * matrix[4] - linear[2] * matrix[5],
165 -linear[1] * matrix[4] - linear[3] * matrix[5],
166 ]);
167 inverse.is_finite().then_some(inverse)
168 }
169
170 pub fn decompose(self) -> Option<TransformComponents> {
177 if !self.is_finite() {
178 return None;
179 }
180 let matrix = self.coefficients;
181 let horizontal_scale = libm::hypot(matrix[0], matrix[1]);
182 if horizontal_scale == 0.0 || !horizontal_scale.is_finite() {
183 return None;
184 }
185 let cosine = matrix[0] / horizontal_scale;
186 let sine = matrix[1] / horizontal_scale;
187 let projection = cosine * matrix[2] + sine * matrix[3];
188 let vertical_scale = cosine * matrix[3] - sine * matrix[2];
189 if vertical_scale == 0.0 || !vertical_scale.is_finite() {
190 return None;
191 }
192 let shear = projection / vertical_scale;
193 if !shear.is_finite() {
194 return None;
195 }
196 Some(TransformComponents {
197 translation: Vector2::new(matrix[4], matrix[5]),
198 rotation: libm::atan2(matrix[1], matrix[0]),
199 scale: Vector2::new(horizontal_scale, vertical_scale),
200 shear,
201 })
202 }
203
204 pub fn interpolate(&self, target: &Self, progress: f64) -> Self {
210 if progress == 0.0 {
211 return *self;
212 }
213 if progress == 1.0 {
214 return *target;
215 }
216 let (Some(first), Some(second)) = (self.decompose(), target.decompose()) else {
217 return Self::from_coefficients(std::array::from_fn(|index| {
218 self.coefficients[index] * (1.0 - progress) + target.coefficients[index] * progress
219 }));
220 };
221 let mut rotation = (second.rotation - first.rotation).rem_euclid(std::f64::consts::TAU);
222 if rotation > std::f64::consts::PI {
223 rotation -= std::f64::consts::TAU;
224 }
225 TransformComponents {
226 translation: first.translation.interpolate(&second.translation, progress),
227 rotation: first.rotation + rotation * progress,
228 scale: first.scale.interpolate(&second.scale, progress),
229 shear: first.shear * (1.0 - progress) + second.shear * progress,
230 }
231 .compose()
232 }
233}
234
235#[derive(Clone, Copy, Debug, PartialEq)]
237pub struct TransformComponents {
238 pub translation: Vector2,
240 pub rotation: f64,
242 pub scale: Vector2,
244 pub shear: f64,
247}
248
249impl TransformComponents {
250 pub fn compose(self) -> Transform {
252 let direction = Vector2::from_angle(self.rotation);
253 Transform::from_coefficients([
254 direction.x * self.scale.x,
255 direction.y * self.scale.x,
256 (direction.x * self.shear - direction.y) * self.scale.y,
257 (direction.y * self.shear + direction.x) * self.scale.y,
258 self.translation.x,
259 self.translation.y,
260 ])
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267 use crate::AngleLiteral;
268
269 fn close(actual: f64, expected: f64) {
270 let tolerance = 1e-12 * actual.abs().max(expected.abs()).max(1.0);
271 assert!(
272 (actual - expected).abs() <= tolerance,
273 "{actual} != {expected}"
274 );
275 }
276
277 fn point_close(actual: Point, expected: Point) {
278 close(actual.x, expected.x);
279 close(actual.y, expected.y);
280 }
281
282 #[test]
283 fn reading_order_matches_sequential_application_and_preserves_vector_semantics() {
284 let first = Transform::translate(10.0, -4.0);
285 let second = Transform::rotate(90.degrees());
286 let point = Point::new(2.0, 3.0);
287 point_close(
288 first.then(second).apply(point),
289 second.apply(first.apply(point)),
290 );
291 point_close(first.then(second).apply(point), Point::new(1.0, 12.0));
292 point_close(second.then(first).apply(point), Point::new(7.0, -2.0));
293 assert_eq!(
294 first.apply_vector(Vector2::new(2.0, 3.0)),
295 Vector2::new(2.0, 3.0)
296 );
297 assert_eq!(Transform::default(), Transform::IDENTITY);
298 let offset = Transform::translate(16_777_217.125, 0.0);
299 assert_eq!(offset.apply(Point::ORIGIN).x, 16_777_217.125);
300 }
301
302 #[test]
303 fn inverse_handles_reflection_shear_and_extreme_axis_scales() {
304 let transforms = [
305 Transform::IDENTITY,
306 Transform::scale_non_uniform(-2.0, 3.0)
307 .then(Transform::skew(15.degrees(), -10.degrees()))
308 .then(Transform::rotate(37.degrees()))
309 .then(Transform::translate(31.0, -17.0)),
310 Transform::scale_non_uniform(1e-200, 1e200),
311 Transform::scale_non_uniform(1e200, 1e200),
312 Transform::scale_non_uniform(1e-200, 1e-200),
313 ];
314 for transform in transforms {
315 let inverse = transform.inverse().unwrap();
316 for point in [
317 Point::ORIGIN,
318 Point::new(-2.5, 3.75),
319 Point::new(17.0, -9.0),
320 ] {
321 point_close(inverse.apply(transform.apply(point)), point);
322 point_close(transform.apply(inverse.apply(point)), point);
323 }
324 }
325 assert!(Transform::scale_non_uniform(0.0, 1.0).inverse().is_none());
326 assert!(Transform::scale(f64::from_bits(1)).inverse().is_none());
327 assert!(Transform::translate(f64::NAN, 1.0).inverse().is_none());
328 assert!(Transform::scale(f64::INFINITY).inverse().is_none());
329 }
330
331 #[test]
332 fn decomposition_recomposes_and_carries_reflection_in_vertical_scale() {
333 for scale in [
334 Vector2::new(2.0, 3.0),
335 Vector2::new(-2.0, 3.0),
336 Vector2::new(-2.0, -3.0),
337 ] {
338 for shear in [-2.5, 0.0, 1.25, 1e20] {
339 let transform = TransformComponents {
340 translation: Vector2::new(21.5, -12.25),
341 rotation: 0.0,
342 scale,
343 shear,
344 }
345 .compose();
346 let parts = transform.decompose().unwrap();
347 assert!(parts.scale.x > 0.0);
348 assert_eq!(
349 parts.scale.y.is_sign_negative(),
350 (scale.x * scale.y).is_sign_negative()
351 );
352 for (actual, expected) in parts
353 .compose()
354 .coefficients()
355 .into_iter()
356 .zip(transform.coefficients())
357 {
358 close(actual, expected);
359 }
360 }
361 }
362 let rotated = Transform::scale_non_uniform(-2.0, 3.0)
363 .then(Transform::skew(0.2, -0.15))
364 .then(Transform::rotate(-127.degrees()));
365 let recomposed = rotated.decompose().unwrap().compose();
366 point_close(
367 recomposed.apply(Point::new(13.0, -8.0)),
368 rotated.apply(Point::new(13.0, -8.0)),
369 );
370 assert!(Transform::scale(0.0).decompose().is_none());
371 assert!(Transform::rotate(f64::NAN).decompose().is_none());
372 }
373
374 #[test]
375 fn transform_interpolation_uses_short_rotation_arcs_and_preserves_endpoints() {
376 let first = Transform::rotate(170.degrees());
377 let second = Transform::rotate(-170.degrees());
378 let midpoint = first.interpolate(&second, 0.5);
379 point_close(midpoint.apply(Point::new(1.0, 0.0)), Point::new(-1.0, 0.0));
380 assert_eq!(first.interpolate(&second, 0.0), first);
381 assert_eq!(first.interpolate(&second, 1.0), second);
382 let half_turn = Transform::IDENTITY.interpolate(&Transform::rotate(180.degrees()), 0.5);
383 point_close(half_turn.apply(Point::new(1.0, 0.0)), Point::new(0.0, 1.0));
384 let singular = Transform::scale(0.0).interpolate(&Transform::scale(2.0), 0.5);
385 point_close(singular.apply(Point::new(2.0, 3.0)), Point::new(2.0, 3.0));
386 let extended = Transform::IDENTITY.interpolate(&Transform::translate(10.0, -20.0), 1.5);
387 assert_eq!(extended.apply(Point::ORIGIN), Point::new(15.0, -30.0));
388 }
389}