Skip to main content

pax_runtime_api/math/
transform.rs

1use crate::Interpolatable;
2
3use super::{Generic, Point2, Space, Vector2};
4use std::{f64::consts::PI, marker::PhantomData, ops::Mul};
5
6//-----------------------------------------------------------
7// Pax matrix/transform class heavily borrows from kurbos
8// transform impl (copy/pasted initially with some modifications)
9// curbo crate: https://www.michaelfbryan.com/arcs/kurbo/index.html
10// original source code: https://www.michaelfbryan.com/arcs/src/kurbo/affine.rs.html#10
11// Kurbo is distributed under an MIT license.
12//-----------------------------------------------------------
13
14impl<W: Space, T: Space> Interpolatable for Transform2<W, T> {
15    fn interpolate(&self, other: &Self, t: f64) -> Self {
16        let parts1: TransformParts = (*self).into();
17        let parts2: TransformParts = (*other).into();
18
19        // Interpolate origin
20        let origin = parts1.origin + (parts2.origin - parts1.origin) * t;
21
22        // Interpolate scale
23        let scale = parts1.scale + (parts2.scale - parts1.scale) * t;
24
25        // Interpolate skew
26        let skew = parts1.skew + (parts2.skew - parts1.skew) * t;
27
28        // Interpolate rotation
29        let mut rotation = parts1.rotation + (parts2.rotation - parts1.rotation) * t;
30
31        // Ensure rotation takes the shortest path
32        if (parts2.rotation - parts1.rotation).abs() > PI {
33            if parts2.rotation > parts1.rotation {
34                rotation += 2.0 * PI * (1.0 - t);
35            } else {
36                rotation -= 2.0 * PI * (1.0 - t);
37            }
38        }
39
40        // Construct and return the interpolated transform
41        TransformParts {
42            origin,
43            scale,
44            skew,
45            rotation,
46        }
47        .into()
48    }
49}
50
51/// A 2D affine transform between two phantom coordinate spaces.
52pub struct Transform2<WFrom = Generic, WTo = WFrom> {
53    /// Affine matrix coefficients in kurbo-compatible `[a, b, c, d, e, f]` order.
54    pub m: [f64; 6],
55    _phantom_from: PhantomData<WFrom>,
56    _phantom_to: PhantomData<WTo>,
57}
58
59// Implement Clone, Copy, PartialEq, etc manually, as
60// to not require the Space to implement these.
61
62impl<F, T> Clone for Transform2<F, T> {
63    fn clone(&self) -> Self {
64        Self {
65            m: self.m,
66            _phantom_from: PhantomData,
67            _phantom_to: PhantomData,
68        }
69    }
70}
71
72impl<F, T> std::fmt::Debug for Transform2<F, T> {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        writeln!(f, "{} {} {}", self.m[0], self.m[2], self.m[4])?;
75        write!(f, "{} {} {}", self.m[1], self.m[3], self.m[5])
76    }
77}
78
79impl<F, T> PartialEq for Transform2<F, T> {
80    fn eq(&self, other: &Self) -> bool {
81        self.m == other.m
82    }
83}
84
85impl<F, T> Copy for Transform2<F, T> {}
86
87impl<F: Space, T: Space> Default for Transform2<F, T> {
88    fn default() -> Self {
89        Self::identity()
90    }
91}
92
93impl<WFrom: Space, WTo: Space> Transform2<WFrom, WTo> {
94    /// Constructs a transform from raw affine coefficients.
95    pub fn new(m: [f64; 6]) -> Self {
96        Self {
97            m,
98            _phantom_from: PhantomData,
99            _phantom_to: PhantomData,
100        }
101    }
102
103    /// Returns the identity transform.
104    pub fn identity() -> Self {
105        Self::new([1.0, 0.0, 0.0, 1.0, 0.0, 0.0])
106    }
107
108    /// Constructs a uniform scale transform.
109    pub fn scale(s: f64) -> Self {
110        Self::scale_sep(Vector2::new(s, s))
111    }
112
113    /// Constructs a non-uniform scale transform.
114    pub fn scale_sep(s: Vector2<WTo>) -> Self {
115        Self::new([s.x, 0.0, 0.0, s.y, 0.0, 0.0])
116    }
117
118    /// Constructs a skew transform.
119    pub fn skew(k: Vector2<WTo>) -> Self {
120        Self::new([1.0, k.y, k.x, 1.0, 0.0, 0.0])
121    }
122
123    /// Constructs a rotation transform, in radians.
124    pub fn rotate(th: f64) -> Self {
125        let (s, c) = th.sin_cos();
126        Self::new([c, s, -s, c, 0.0, 0.0])
127    }
128
129    /// Constructs a translation transform.
130    pub fn translate(p: Vector2<WTo>) -> Self {
131        Self::new([1.0, 0.0, 0.0, 1.0, p.x, p.y])
132    }
133
134    /// Returns the affine determinant.
135    pub fn determinant(self) -> f64 {
136        self.m[0] * self.m[3] - self.m[1] * self.m[2]
137    }
138
139    /// Returns raw affine coefficients in kurbo-compatible `[a, b, c, d, e, f]` order.
140    pub fn coeffs(&self) -> [f64; 6] {
141        self.m
142    }
143
144    /// Extracts the translation component as a vector.
145    pub fn get_translation(self) -> Vector2<WFrom> {
146        (self * Point2::<WFrom>::default()).cast_space().to_vector()
147    }
148
149    /// Returns the transformed unit scale vector.
150    pub fn get_scale(self) -> Vector2<WTo> {
151        self * Vector2::<WFrom>::new(1.0, 1.0)
152    }
153
154    /// Casts the phantom coordinate spaces without changing coefficients.
155    pub fn cast_spaces<W: Space, T: Space>(self) -> Transform2<W, T> {
156        Transform2::new(self.m)
157    }
158
159    /// Produces NaN values when the determinant is zero.
160    pub fn inverse(self) -> Transform2<WTo, WFrom> {
161        let inv_det = self.determinant().recip();
162        Transform2::<WTo, WFrom>::new([
163            inv_det * self.m[3],
164            -inv_det * self.m[1],
165            -inv_det * self.m[2],
166            inv_det * self.m[0],
167            inv_det * (self.m[2] * self.m[5] - self.m[3] * self.m[4]),
168            inv_det * (self.m[1] * self.m[4] - self.m[0] * self.m[5]),
169        ])
170    }
171
172    /// Composes a transform from origin and basis vectors.
173    pub fn compose(p: Point2<WTo>, vx: Vector2<WTo>, vy: Vector2<WTo>) -> Self {
174        Self::new([vx.x, vx.y, vy.x, vy.y, p.x, p.y])
175    }
176
177    /// Decomposes the transform into origin and transformed basis vectors.
178    pub fn decompose(&self) -> (Point2<WTo>, Vector2<WTo>, Vector2<WTo>) {
179        let [v1x, v1y, v2x, v2y, px, py] = self.m;
180        (
181            Point2::new(px, py),
182            Vector2::new(v1x, v1y),
183            Vector2::new(v2x, v2y),
184        )
185    }
186
187    /// Returns true when `point` lands inside the transformed unit square.
188    pub fn contains_point(&self, point: Point2<WTo>) -> bool {
189        let unit = self.inverse() * point;
190        unit.x > 0.0 && unit.y > 0.0 && unit.x < 1.0 && unit.y < 1.0
191    }
192}
193
194/// Decomposed transform components used for interpolation.
195#[derive(PartialEq, Clone)]
196pub struct TransformParts {
197    /// Translation component.
198    pub origin: Vector2,
199    /// Scale component.
200    pub scale: Vector2,
201    /// Skew component.
202    pub skew: Vector2,
203    /// Rotation component, in radians.
204    pub rotation: f64,
205}
206
207impl<F: Space, W: Space> Into<Transform2<F, W>> for TransformParts {
208    fn into(self) -> Transform2<F, W> {
209        (Transform2::<Generic>::translate(self.origin)
210            * Transform2::rotate(self.rotation)
211            * Transform2::<Generic>::scale_sep(self.scale)
212            * Transform2::<Generic>::skew(self.skew))
213        .cast_spaces()
214    }
215}
216
217/// NOTE: the returned parts.skew.y will always be equal to 0,
218impl<F: Space, T: Space> Into<TransformParts> for Transform2<F, T> {
219    fn into(self) -> TransformParts {
220        let [a, b, c, d, e, f] = self.m;
221        let angle = f64::atan2(b, a);
222        let denom = a.powi(2) + b.powi(2);
223        let scale_x = f64::sqrt(denom);
224        let scale_y = (a * d - c * b) / scale_x;
225        let skew_x = (a * c + b * d) / denom;
226
227        TransformParts {
228            origin: Vector2::new(e, f),
229            scale: Vector2::new(scale_x, scale_y),
230            skew: Vector2::new(skew_x, 0.0),
231            rotation: angle,
232        }
233    }
234}
235
236impl std::fmt::Debug for TransformParts {
237    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        f.debug_struct("Parts")
239            .field("origin", &self.origin)
240            .field("scale", &self.scale)
241            .field("skew", &self.skew)
242            .field("rotation", &self.rotation)
243            .finish()
244    }
245}
246
247impl<W1: Space, W2: Space, W3: Space> Mul<Transform2<W1, W2>> for Transform2<W2, W3> {
248    type Output = Transform2<W1, W3>;
249
250    fn mul(self, rhs: Transform2<W1, W2>) -> Self::Output {
251        Self::Output::new([
252            self.m[0] * rhs.m[0] + self.m[2] * rhs.m[1],
253            self.m[1] * rhs.m[0] + self.m[3] * rhs.m[1],
254            self.m[0] * rhs.m[2] + self.m[2] * rhs.m[3],
255            self.m[1] * rhs.m[2] + self.m[3] * rhs.m[3],
256            self.m[0] * rhs.m[4] + self.m[2] * rhs.m[5] + self.m[4],
257            self.m[1] * rhs.m[4] + self.m[3] * rhs.m[5] + self.m[5],
258        ])
259    }
260}
261
262impl<F: Space, T: Space> Mul<Point2<F>> for Transform2<F, T> {
263    type Output = Point2<T>;
264
265    fn mul(self, other: Point2<F>) -> Self::Output {
266        Self::Output::new(
267            self.m[0] * other.x + self.m[2] * other.y + self.m[4],
268            self.m[1] * other.x + self.m[3] * other.y + self.m[5],
269        )
270    }
271}
272
273impl<F: Space, T: Space> Mul<Vector2<F>> for Transform2<F, T> {
274    type Output = Vector2<T>;
275
276    fn mul(self, other: Vector2<F>) -> Self::Output {
277        Self::Output::new(
278            self.m[0] * other.x + self.m[2] * other.y,
279            self.m[1] * other.x + self.m[3] * other.y,
280        )
281    }
282}
283
284impl<T: Space, F: Space> From<Transform2<T, F>> for kurbo::Affine {
285    fn from(value: Transform2<T, F>) -> Self {
286        Self::new(value.m)
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use std::f64::consts::PI;
293
294    use crate::math::{Generic, Vector2};
295
296    use super::{Transform2, TransformParts};
297
298    #[test]
299    fn from_to_parts() {
300        // any values
301        for origin in [
302            Vector2::new(0.0, 0.0),
303            Vector2::new(11.2, 10.5),
304            Vector2::new(-9.2, 0.98),
305            Vector2::new(14.2, -3.2),
306            Vector2::new(-5.0, -5.1),
307        ] {
308            // always > 0
309            for scale in [
310                Vector2::new(1.0, 1.0),
311                Vector2::new(1.2, 1.5),
312                Vector2::new(0.2, 0.4395),
313            ] {
314                // any values
315                for rotation in [0.0, 1.0, 1.2940, -0.495, 5.0, -40.1] {
316                    // skew x any value, skew_y = 0.0
317                    for skew in [
318                        Vector2::new(0.0, 0.0),
319                        Vector2::new(1.0, 0.0),
320                        Vector2::new(0.13904, 0.0),
321                        Vector2::new(-0.55, 0.0),
322                        Vector2::new(100.0, 0.0),
323                    ] {
324                        let parts = TransformParts {
325                            origin,
326                            scale,
327                            rotation,
328                            skew,
329                        };
330                        let transform: Transform2 = parts.clone().into();
331                        let new_parts: TransformParts = transform.into();
332                        assert!((parts.skew - new_parts.skew).length() < 1e-3);
333                        assert!((parts.origin - new_parts.origin).length() < 1e-3);
334                        assert!((parts.scale - new_parts.scale).length() < 1e-3);
335                        // rotation should be similar up to multiples of 2 pi
336                        assert!(
337                            ((parts.rotation - new_parts.rotation).rem_euclid(2.0 * PI)).abs()
338                                < 1e-3
339                        );
340                    }
341                }
342            }
343        }
344    }
345
346    #[test]
347    fn test_scale_and_resize() {
348        let transform = Transform2::<Generic>::new([1.0, 0.4, 0.8, 0.2, 0.1, 0.5]);
349        let mut parts: TransformParts = transform.into();
350        parts.scale.x = 2.6 * parts.scale.x;
351        parts.scale.y = 1.8 * parts.scale.y;
352        let res_transform: Transform2<Generic> = parts.into();
353        let res2_transform = transform * Transform2::<Generic>::scale_sep(Vector2::new(2.6, 1.8));
354        assert!(
355            !(res_transform
356                .coeffs()
357                .into_iter()
358                .zip(res2_transform.coeffs())
359                .map(|(a, b)| (a - b).abs())
360                .sum::<f64>()
361                < 1e-3)
362        );
363    }
364}