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
51pub struct Transform2<WFrom = Generic, WTo = WFrom> {
52    pub m: [f64; 6],
53    _panthom_from: PhantomData<WFrom>,
54    _panthom_to: PhantomData<WTo>,
55}
56
57// Implement Clone, Copy, PartialEq, etc manually, as
58// to not require the Space to implement these.
59
60impl<F, T> Clone for Transform2<F, T> {
61    fn clone(&self) -> Self {
62        Self {
63            m: self.m,
64            _panthom_from: PhantomData,
65            _panthom_to: PhantomData,
66        }
67    }
68}
69
70impl<F, T> std::fmt::Debug for Transform2<F, T> {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        writeln!(f, "{} {} {}", self.m[0], self.m[2], self.m[4])?;
73        write!(f, "{} {} {}", self.m[1], self.m[3], self.m[5])
74    }
75}
76
77impl<F, T> PartialEq for Transform2<F, T> {
78    fn eq(&self, other: &Self) -> bool {
79        self.m == other.m
80    }
81}
82
83impl<F, T> Copy for Transform2<F, T> {}
84
85impl<F: Space, T: Space> Default for Transform2<F, T> {
86    fn default() -> Self {
87        Self::identity()
88    }
89}
90
91impl<WFrom: Space, WTo: Space> Transform2<WFrom, WTo> {
92    pub fn new(m: [f64; 6]) -> Self {
93        Self {
94            m,
95            _panthom_from: PhantomData,
96            _panthom_to: PhantomData,
97        }
98    }
99
100    pub fn identity() -> Self {
101        Self::new([1.0, 0.0, 0.0, 1.0, 0.0, 0.0])
102    }
103
104    pub fn scale(s: f64) -> Self {
105        Self::scale_sep(Vector2::new(s, s))
106    }
107
108    pub fn scale_sep(s: Vector2<WTo>) -> Self {
109        Self::new([s.x, 0.0, 0.0, s.y, 0.0, 0.0])
110    }
111
112    pub fn skew(k: Vector2<WTo>) -> Self {
113        Self::new([1.0, k.y, k.x, 1.0, 0.0, 0.0])
114    }
115
116    pub fn rotate(th: f64) -> Self {
117        let (s, c) = th.sin_cos();
118        Self::new([c, s, -s, c, 0.0, 0.0])
119    }
120
121    pub fn translate(p: Vector2<WTo>) -> Self {
122        Self::new([1.0, 0.0, 0.0, 1.0, p.x, p.y])
123    }
124
125    pub fn determinant(self) -> f64 {
126        self.m[0] * self.m[3] - self.m[1] * self.m[2]
127    }
128
129    pub fn coeffs(&self) -> [f64; 6] {
130        self.m
131    }
132
133    pub fn get_translation(self) -> Vector2<WFrom> {
134        (self * Point2::<WFrom>::default()).cast_space().to_vector()
135    }
136
137    pub fn get_scale(self) -> Vector2<WTo> {
138        self * Vector2::<WFrom>::new(1.0, 1.0)
139    }
140
141    pub fn cast_spaces<W: Space, T: Space>(self) -> Transform2<W, T> {
142        Transform2::new(self.m)
143    }
144
145    /// Produces NaN values when the determinant is zero.
146    pub fn inverse(self) -> Transform2<WTo, WFrom> {
147        let inv_det = self.determinant().recip();
148        Transform2::<WTo, WFrom>::new([
149            inv_det * self.m[3],
150            -inv_det * self.m[1],
151            -inv_det * self.m[2],
152            inv_det * self.m[0],
153            inv_det * (self.m[2] * self.m[5] - self.m[3] * self.m[4]),
154            inv_det * (self.m[1] * self.m[4] - self.m[0] * self.m[5]),
155        ])
156    }
157
158    pub fn compose(p: Point2<WTo>, vx: Vector2<WTo>, vy: Vector2<WTo>) -> Self {
159        Self::new([vx.x, vx.y, vy.x, vy.y, p.x, p.y])
160    }
161
162    // Decomposes the transform into translation point + unit vector transforms
163    // (ie. where (0, 1) and (1, 0) end up)
164    pub fn decompose(&self) -> (Point2<WTo>, Vector2<WTo>, Vector2<WTo>) {
165        let [v1x, v1y, v2x, v2y, px, py] = self.m;
166        (
167            Point2::new(px, py),
168            Vector2::new(v1x, v1y),
169            Vector2::new(v2x, v2y),
170        )
171    }
172
173    pub fn contains_point(&self, point: Point2<WTo>) -> bool {
174        let unit = self.inverse() * point;
175        unit.x > 0.0 && unit.y > 0.0 && unit.x < 1.0 && unit.y < 1.0
176    }
177}
178
179#[derive(PartialEq, Clone)]
180pub struct TransformParts {
181    pub origin: Vector2,
182    pub scale: Vector2,
183    pub skew: Vector2,
184    pub rotation: f64,
185}
186
187impl<F: Space, W: Space> Into<Transform2<F, W>> for TransformParts {
188    fn into(self) -> Transform2<F, W> {
189        (Transform2::<Generic>::translate(self.origin)
190            * Transform2::rotate(self.rotation)
191            * Transform2::<Generic>::scale_sep(self.scale)
192            * Transform2::<Generic>::skew(self.skew))
193        .cast_spaces()
194    }
195}
196
197/// NOTE: the returned parts.skew.y will always be equal to 0,
198impl<F: Space, T: Space> Into<TransformParts> for Transform2<F, T> {
199    fn into(self) -> TransformParts {
200        let [a, b, c, d, e, f] = self.m;
201        let angle = f64::atan2(b, a);
202        let denom = a.powi(2) + b.powi(2);
203        let scale_x = f64::sqrt(denom);
204        let scale_y = (a * d - c * b) / scale_x;
205        let skew_x = (a * c + b * d) / denom;
206
207        TransformParts {
208            origin: Vector2::new(e, f),
209            scale: Vector2::new(scale_x, scale_y),
210            skew: Vector2::new(skew_x, 0.0),
211            rotation: angle,
212        }
213    }
214}
215
216impl std::fmt::Debug for TransformParts {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        f.debug_struct("Parts")
219            .field("origin", &self.origin)
220            .field("scale", &self.scale)
221            .field("skew", &self.skew)
222            .field("rotation", &self.rotation)
223            .finish()
224    }
225}
226
227impl<W1: Space, W2: Space, W3: Space> Mul<Transform2<W1, W2>> for Transform2<W2, W3> {
228    type Output = Transform2<W1, W3>;
229
230    fn mul(self, rhs: Transform2<W1, W2>) -> Self::Output {
231        Self::Output::new([
232            self.m[0] * rhs.m[0] + self.m[2] * rhs.m[1],
233            self.m[1] * rhs.m[0] + self.m[3] * rhs.m[1],
234            self.m[0] * rhs.m[2] + self.m[2] * rhs.m[3],
235            self.m[1] * rhs.m[2] + self.m[3] * rhs.m[3],
236            self.m[0] * rhs.m[4] + self.m[2] * rhs.m[5] + self.m[4],
237            self.m[1] * rhs.m[4] + self.m[3] * rhs.m[5] + self.m[5],
238        ])
239    }
240}
241
242impl<F: Space, T: Space> Mul<Point2<F>> for Transform2<F, T> {
243    type Output = Point2<T>;
244
245    fn mul(self, other: Point2<F>) -> Self::Output {
246        Self::Output::new(
247            self.m[0] * other.x + self.m[2] * other.y + self.m[4],
248            self.m[1] * other.x + self.m[3] * other.y + self.m[5],
249        )
250    }
251}
252
253impl<F: Space, T: Space> Mul<Vector2<F>> for Transform2<F, T> {
254    type Output = Vector2<T>;
255
256    fn mul(self, other: Vector2<F>) -> Self::Output {
257        Self::Output::new(
258            self.m[0] * other.x + self.m[2] * other.y,
259            self.m[1] * other.x + self.m[3] * other.y,
260        )
261    }
262}
263
264impl<T: Space, F: Space> From<Transform2<T, F>> for kurbo::Affine {
265    fn from(value: Transform2<T, F>) -> Self {
266        Self::new(value.m)
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use std::f64::consts::PI;
273
274    use crate::math::{Generic, Vector2};
275
276    use super::{Transform2, TransformParts};
277
278    #[test]
279    fn from_to_parts() {
280        // any values
281        for origin in [
282            Vector2::new(0.0, 0.0),
283            Vector2::new(11.2, 10.5),
284            Vector2::new(-9.2, 0.98),
285            Vector2::new(14.2, -3.2),
286            Vector2::new(-5.0, -5.1),
287        ] {
288            // always > 0
289            for scale in [
290                Vector2::new(1.0, 1.0),
291                Vector2::new(1.2, 1.5),
292                Vector2::new(0.2, 0.4395),
293            ] {
294                // any values
295                for rotation in [0.0, 1.0, 1.2940, -0.495, 5.0, -40.1] {
296                    // skew x any value, skew_y = 0.0
297                    for skew in [
298                        Vector2::new(0.0, 0.0),
299                        Vector2::new(1.0, 0.0),
300                        Vector2::new(0.13904, 0.0),
301                        Vector2::new(-0.55, 0.0),
302                        Vector2::new(100.0, 0.0),
303                    ] {
304                        let parts = TransformParts {
305                            origin,
306                            scale,
307                            rotation,
308                            skew,
309                        };
310                        let transform: Transform2 = parts.clone().into();
311                        let new_parts: TransformParts = transform.into();
312                        assert!((parts.skew - new_parts.skew).length() < 1e-3);
313                        assert!((parts.origin - new_parts.origin).length() < 1e-3);
314                        assert!((parts.scale - new_parts.scale).length() < 1e-3);
315                        // rotation should be similar up to multiples of 2 pi
316                        assert!(
317                            ((parts.rotation - new_parts.rotation).rem_euclid(2.0 * PI)).abs()
318                                < 1e-3
319                        );
320                    }
321                }
322            }
323        }
324    }
325
326    #[test]
327    fn test_scale_and_resize() {
328        let transform = Transform2::<Generic>::new([1.0, 0.4, 0.8, 0.2, 0.1, 0.5]);
329        let mut parts: TransformParts = transform.into();
330        parts.scale.x = 2.6 * parts.scale.x;
331        parts.scale.y = 1.8 * parts.scale.y;
332        let res_transform: Transform2<Generic> = parts.into();
333        let res2_transform = transform * Transform2::<Generic>::scale_sep(Vector2::new(2.6, 1.8));
334        assert!(
335            !(res_transform
336                .coeffs()
337                .into_iter()
338                .zip(res2_transform.coeffs())
339                .map(|(a, b)| (a - b).abs())
340                .sum::<f64>()
341                < 1e-3)
342        );
343    }
344}