Skip to main content

optic_render/util/transform/
trans2d.rs

1use cgmath::*;
2
3/// A 2D transform with position, rotation, scale, layer, and aspect ratio.
4///
5/// The position is expressed in normalized coordinates (0–1 across the screen).
6/// The aspect ratio corrects the horizontal component so that a square sprite
7/// appears square regardless of the viewport dimensions.
8///
9/// Call [`calc_matrix`](Transform2D::calc_matrix) after mutating to recompute
10/// the 4×4 matrix used for rendering.
11///
12/// # Coordinate system
13///
14/// - **Position** — normalized space: `(0, 0)` is bottom-left, `(1, 1)` is
15///   top-right.
16/// - **Rotation** — degrees around the Z axis (counter-clockwise).
17/// - **Layer** — draw order: higher values are rendered on top.
18///
19/// # Operations
20///
21/// | Category | Methods |
22/// |---|---|
23/// | **Position** — getter | [`pos`](Transform2D::pos) |
24/// | **Position** — absolute setter | [`set_pos_all`](Transform2D::set_pos_all), [`set_pos_x`](Transform2D::set_pos_x), [`set_pos_y`](Transform2D::set_pos_y) |
25/// | **Position** — relative move | [`move_all`](Transform2D::move_all), [`move_x`](Transform2D::move_x), [`move_y`](Transform2D::move_y) |
26/// | **Rotation** — getter/setter | [`rot`](Transform2D::rot), [`set_rot`](Transform2D::set_rot), [`rotate`](Transform2D::rotate) |
27/// | **Scale** — getter | [`scale`](Transform2D::scale) |
28/// | **Scale** — absolute setter | [`set_scale_all`](Transform2D::set_scale_all), [`set_scale_same`](Transform2D::set_scale_same), [`set_scale_x`](Transform2D::set_scale_x), [`set_scale_y`](Transform2D::set_scale_y) |
29/// | **Scale** — relative add | [`scale_all`](Transform2D::scale_all), [`scale_same`](Transform2D::scale_same), [`scale_x`](Transform2D::scale_x), [`scale_y`](Transform2D::scale_y) |
30/// | **Layer** | [`layer`](Transform2D::layer), [`set_layer`](Transform2D::set_layer) |
31/// | **Aspect** | [`aspect`](Transform2D::aspect), [`set_aspect`](Transform2D::set_aspect) |
32/// | **Matrix** | [`matrix`](Transform2D::matrix), [`calc_matrix`](Transform2D::calc_matrix) |
33#[derive(Clone, Debug)]
34pub struct Transform2D {
35    matrix: Matrix4<f32>,
36    pos: Vector2<f32>,
37    rot: f32,
38    layer: u8,
39    aspect: f32,
40    scale: Vector2<f32>,
41}
42
43impl Default for Transform2D {
44    fn default() -> Self {
45        Self {
46            matrix: Matrix4::identity(),
47            pos: Vector2::new(0.0, 0.0),
48            rot: 0.0,
49            layer: 0,
50            aspect: 1.0,
51            scale: Vector2::new(1.0, 1.0),
52        }
53    }
54}
55
56impl Transform2D {
57    fn calc_pos_matrix(&self) -> Matrix4<f32> {
58        let p = self.pos * 2.0;
59        let v = vec3((p.x - 1.0) * self.aspect, p.y - 1.0, 0.0);
60        Matrix4::from_translation(v)
61    }
62
63    fn calc_rot_matrix(&self) -> Matrix4<f32> {
64        Matrix4::from_angle_z(Rad::from(Deg(self.rot)))
65    }
66
67    fn calc_scale_matrix(&self) -> Matrix4<f32> {
68        Matrix4::from_nonuniform_scale(self.scale.x, self.scale.y, 1.0)
69    }
70
71    /// Recomputes the transformation matrix from the current pos/rot/scale.
72    pub fn calc_matrix(&mut self) {
73        self.matrix = self.calc_pos_matrix() * self.calc_rot_matrix() * self.calc_scale_matrix();
74    }
75
76    /// Returns the aspect ratio (width / height).
77    pub fn aspect(&self) -> f32 { self.aspect }
78    /// Sets the aspect ratio.
79    pub fn set_aspect(&mut self, aspect: f32) { self.aspect = aspect; }
80    /// Returns the position in normalized coordinates.
81    pub fn pos(&self) -> Vector2<f32> { self.pos }
82    /// Returns the rotation in degrees.
83    pub fn rot(&self) -> f32 { self.rot }
84    /// Returns the layer (draw order).
85    pub fn layer(&self) -> u8 { self.layer }
86    /// Returns the scale factor.
87    pub fn scale(&self) -> Vector2<f32> { self.scale }
88    /// Returns the cached 4×4 transformation matrix.
89    pub fn matrix(&self) -> Matrix4<f32> { self.matrix }
90
91    /// Translates by `(x, y)` in normalized coordinates.
92    pub fn move_all(&mut self, x: f32, y: f32) { self.pos += vec2(x, y); }
93    /// Translates along the X axis.
94    pub fn move_x(&mut self, x: f32) { self.pos.x += x; }
95    /// Translates along the Y axis.
96    pub fn move_y(&mut self, y: f32) { self.pos.y += y; }
97    /// Sets the position to `(x, y)`.
98    pub fn set_pos_all(&mut self, x: f32, y: f32) { self.pos = vec2(x, y); }
99    /// Sets the X coordinate.
100    pub fn set_pos_x(&mut self, x: f32) { self.pos.x = x; }
101    /// Sets the Y coordinate.
102    pub fn set_pos_y(&mut self, y: f32) { self.pos.y = y; }
103
104    /// Adds `rot` degrees to the current rotation.
105    pub fn rotate(&mut self, rot: f32) { self.rot += rot; }
106    /// Sets the rotation to `rot` degrees.
107    pub fn set_rot(&mut self, rot: f32) { self.rot = rot; }
108    /// Sets the layer.
109    pub fn set_layer(&mut self, layer: u8) { self.layer = layer; }
110
111    /// Adds `(x, y)` to the current scale.
112    pub fn scale_all(&mut self, x: f32, y: f32) { self.scale += vec2(x, y); }
113    /// Adds `xy` to both scale components.
114    pub fn scale_same(&mut self, xy: f32) { self.scale_all(xy, xy); }
115    /// Adds `x` to the scale X component.
116    pub fn scale_x(&mut self, x: f32) { self.scale.x += x; }
117    /// Adds `y` to the scale Y component.
118    pub fn scale_y(&mut self, y: f32) { self.scale.y += y; }
119    /// Sets the scale to `(x, y)`.
120    pub fn set_scale_all(&mut self, x: f32, y: f32) { self.scale = vec2(x, y); }
121    /// Sets both scale components to `xy`.
122    pub fn set_scale_same(&mut self, xy: f32) { self.set_scale_all(xy, xy); }
123    /// Sets the scale X component.
124    pub fn set_scale_x(&mut self, x: f32) { self.scale.x = x; }
125    /// Sets the scale Y component.
126    pub fn set_scale_y(&mut self, y: f32) { self.scale.y = y; }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    fn approx_eq(a: f32, b: f32) -> bool {
134        (a - b).abs() < 1e-5
135    }
136
137    #[test]
138    fn transform2d_default() {
139        let t = Transform2D::default();
140        assert_eq!(t.pos(), vec2(0.0, 0.0));
141        assert_eq!(t.rot(), 0.0);
142        assert_eq!(t.scale(), vec2(1.0, 1.0));
143    }
144
145    #[test]
146    fn transform2d_set_pos() {
147        let mut t = Transform2D::default();
148        t.set_pos_all(100.0, 200.0);
149        assert_eq!(t.pos(), vec2(100.0, 200.0));
150        t.set_pos_x(50.0);
151        t.set_pos_y(150.0);
152        assert_eq!(t.pos(), vec2(50.0, 150.0));
153    }
154
155    #[test]
156    fn transform2d_move() {
157        let mut t = Transform2D::default();
158        t.move_all(10.0, 20.0);
159        assert_eq!(t.pos(), vec2(10.0, 20.0));
160        t.move_x(5.0);
161        t.move_y(3.0);
162        assert_eq!(t.pos(), vec2(15.0, 23.0));
163    }
164
165    #[test]
166    fn transform2d_rotate() {
167        let mut t = Transform2D::default();
168        t.rotate(90.0);
169        assert!(approx_eq(t.rot(), 90.0));
170        t.set_rot(45.0);
171        assert!(approx_eq(t.rot(), 45.0));
172    }
173
174    #[test]
175    fn transform2d_scale() {
176        let mut t = Transform2D::default();
177        t.set_scale_all(2.0, 3.0);
178        assert_eq!(t.scale(), vec2(2.0, 3.0));
179        t.set_scale_x(5.0);
180        t.set_scale_y(6.0);
181        assert_eq!(t.scale(), vec2(5.0, 6.0));
182    }
183
184    #[test]
185    fn transform2d_scale_operations() {
186        let mut t = Transform2D::default();
187        t.scale_all(1.0, 2.0);
188        assert_eq!(t.scale(), vec2(2.0, 3.0));
189        t.scale_same(3.0);
190        assert_eq!(t.scale(), vec2(5.0, 6.0));
191        t.scale_x(1.0);
192        t.scale_y(1.0);
193        assert_eq!(t.scale(), vec2(6.0, 7.0));
194    }
195
196    #[test]
197    fn transform2d_layer() {
198        let mut t = Transform2D::default();
199        assert_eq!(t.layer(), 0);
200        t.set_layer(5);
201        assert_eq!(t.layer(), 5);
202    }
203
204    #[test]
205    fn transform2d_matrix() {
206        let mut t = Transform2D::default();
207        t.calc_matrix();
208        let m = t.matrix();
209        assert!(approx_eq(m[0][0], 1.0));
210        assert!(approx_eq(m[1][1], 1.0));
211        assert!(approx_eq(m[2][2], 1.0));
212    }
213
214    #[test]
215    fn transform2d_matrix_translation() {
216        let mut t = Transform2D::default();
217        t.set_pos_all(0.5, 0.0);
218        t.calc_matrix();
219        let m = t.matrix();
220        assert!(approx_eq(m[3][0], (0.5 * 2.0 - 1.0) * 1.0));
221        assert!(approx_eq(m[3][1], 0.0 * 2.0 - 1.0));
222    }
223
224    #[test]
225    fn transform2d_aspect() {
226        let mut t = Transform2D::default();
227        assert!(approx_eq(t.aspect(), 1.0));
228        t.set_aspect(1.5);
229        assert!(approx_eq(t.aspect(), 1.5));
230    }
231}