Skip to main content

cranpose_render_common/
layer_transform.rs

1use cranpose_ui_graphics::{GraphicsLayer, Point, Rect};
2
3use crate::graph::{quad_bounds, ProjectiveTransform};
4
5pub(crate) fn layer_scale_x(layer: &GraphicsLayer) -> f32 {
6    layer.scale * layer.scale_x
7}
8
9pub(crate) fn layer_scale_y(layer: &GraphicsLayer) -> f32 {
10    layer.scale * layer.scale_y
11}
12
13pub fn layer_uniform_scale(layer: &GraphicsLayer) -> f32 {
14    layer_scale_x(layer).min(layer_scale_y(layer))
15}
16
17/// True when the layer's affine part moves nothing: unit scale and zero
18/// translation. A plain draw canvas — the layer under every game scene —
19/// looks like this, and the per-primitive transform helpers below run once
20/// per recorded shape per frame, so the scan matters at scene scale.
21pub(crate) fn layer_affine_is_identity(layer: &GraphicsLayer) -> bool {
22    layer.scale == 1.0
23        && layer.scale_x == 1.0
24        && layer.scale_y == 1.0
25        && layer.translation_x == 0.0
26        && layer.translation_y == 0.0
27}
28
29pub fn apply_layer_affine_to_rect(rect: Rect, layer_bounds: Rect, layer: &GraphicsLayer) -> Rect {
30    if layer_affine_is_identity(layer) {
31        return rect;
32    }
33    let scale_x = layer_scale_x(layer);
34    let scale_y = layer_scale_y(layer);
35    let (pivot_x, pivot_y) = layer_rotation_pivot(layer_bounds, layer);
36    Rect {
37        x: pivot_x + (rect.x - pivot_x) * scale_x + layer.translation_x,
38        y: pivot_y + (rect.y - pivot_y) * scale_y + layer.translation_y,
39        width: rect.width * scale_x,
40        height: rect.height * scale_y,
41    }
42}
43
44/// Point counterpart of [`apply_layer_affine_to_rect`]: scale about the layer
45/// pivot, then translate. Rotation/perspective are deliberately excluded — the
46/// affine space is the one SDF shapes are evaluated in, with rotation carried
47/// by the deformed quad instead.
48pub fn apply_layer_affine_to_point(
49    point: Point,
50    layer_bounds: Rect,
51    layer: &GraphicsLayer,
52) -> Point {
53    if layer_affine_is_identity(layer) {
54        return point;
55    }
56    let (pivot_x, pivot_y) = layer_rotation_pivot(layer_bounds, layer);
57    Point::new(
58        pivot_x + (point.x - pivot_x) * layer_scale_x(layer) + layer.translation_x,
59        pivot_y + (point.y - pivot_y) * layer_scale_y(layer) + layer.translation_y,
60    )
61}
62
63fn layer_rotation_pivot(layer_bounds: Rect, layer: &GraphicsLayer) -> (f32, f32) {
64    (
65        layer_bounds.x + layer_bounds.width * layer.transform_origin.pivot_fraction_x,
66        layer_bounds.y + layer_bounds.height * layer.transform_origin.pivot_fraction_y,
67    )
68}
69
70fn layer_has_rotation(layer: &GraphicsLayer) -> bool {
71    layer.rotation_x.abs() > f32::EPSILON
72        || layer.rotation_y.abs() > f32::EPSILON
73        || layer.rotation_z.abs() > f32::EPSILON
74}
75
76fn apply_rotation_and_perspective(
77    point: [f32; 2],
78    pivot: (f32, f32),
79    layer: &GraphicsLayer,
80) -> [f32; 2] {
81    if !layer_has_rotation(layer) {
82        return point;
83    }
84
85    let mut x = point[0] - pivot.0;
86    let mut y = point[1] - pivot.1;
87    let mut z = 0.0f32;
88
89    let (sin_x, cos_x) = layer.rotation_x.to_radians().sin_cos();
90    let (sin_y, cos_y) = layer.rotation_y.to_radians().sin_cos();
91    let (sin_z, cos_z) = layer.rotation_z.to_radians().sin_cos();
92
93    let y_rot_x = y * cos_x - z * sin_x;
94    let z_rot_x = y * sin_x + z * cos_x;
95    y = y_rot_x;
96    z = z_rot_x;
97
98    let x_rot_y = x * cos_y + z * sin_y;
99    let z_rot_y = -x * sin_y + z * cos_y;
100    x = x_rot_y;
101    z = z_rot_y;
102
103    let x_rot_z = x * cos_z - y * sin_z;
104    let y_rot_z = x * sin_z + y * cos_z;
105    x = x_rot_z;
106    y = y_rot_z;
107
108    const CAMERA_DISTANCE_SCALE: f32 = 72.0;
109    let camera_distance = (layer.camera_distance * CAMERA_DISTANCE_SCALE).max(1.0);
110    let denom = (camera_distance - z).max(1.0);
111    let perspective = camera_distance / denom;
112
113    [pivot.0 + x * perspective, pivot.1 + y * perspective]
114}
115
116fn apply_layer_to_point(point: [f32; 2], pivot: (f32, f32), layer: &GraphicsLayer) -> [f32; 2] {
117    let scaled = [
118        pivot.0 + (point[0] - pivot.0) * layer_scale_x(layer),
119        pivot.1 + (point[1] - pivot.1) * layer_scale_y(layer),
120    ];
121    let rotated = apply_rotation_and_perspective(scaled, pivot, layer);
122    [
123        rotated[0] + layer.translation_x,
124        rotated[1] + layer.translation_y,
125    ]
126}
127
128pub fn apply_layer_to_quad(rect: Rect, layer_bounds: Rect, layer: &GraphicsLayer) -> [[f32; 2]; 4] {
129    let quad = [
130        [rect.x, rect.y],
131        [rect.x + rect.width, rect.y],
132        [rect.x, rect.y + rect.height],
133        [rect.x + rect.width, rect.y + rect.height],
134    ];
135    if layer_affine_is_identity(layer) && !layer_has_rotation(layer) {
136        return quad;
137    }
138    let pivot = layer_rotation_pivot(layer_bounds, layer);
139    quad.map(|point| apply_layer_to_point(point, pivot, layer))
140}
141
142pub fn apply_layer_to_rect(rect: Rect, layer_bounds: Rect, layer: &GraphicsLayer) -> Rect {
143    quad_bounds(apply_layer_to_quad(rect, layer_bounds, layer))
144}
145
146pub fn layer_transform_to_parent(
147    local_bounds: Rect,
148    placement: Point,
149    layer: &GraphicsLayer,
150) -> ProjectiveTransform {
151    let placement_rect = Rect {
152        x: placement.x,
153        y: placement.y,
154        width: local_bounds.width,
155        height: local_bounds.height,
156    };
157    ProjectiveTransform::from_rect_to_quad(
158        local_bounds,
159        apply_layer_to_quad(placement_rect, placement_rect, layer),
160    )
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use cranpose_ui_graphics::{Rect, TransformOrigin};
167
168    #[test]
169    fn layer_transform_to_parent_maps_local_bounds_to_positioned_bounds() {
170        let local_bounds = Rect {
171            x: 0.0,
172            y: 0.0,
173            width: 30.0,
174            height: 18.0,
175        };
176        let placement = Point { x: 12.0, y: 9.0 };
177        let transform =
178            layer_transform_to_parent(local_bounds, placement, &GraphicsLayer::default());
179
180        assert_eq!(
181            transform.bounds_for_rect(local_bounds),
182            Rect {
183                x: 12.0,
184                y: 9.0,
185                width: 30.0,
186                height: 18.0,
187            }
188        );
189    }
190
191    #[test]
192    fn layer_transform_to_parent_applies_rotation_about_transform_origin() {
193        let local_bounds = Rect {
194            x: 0.0,
195            y: 0.0,
196            width: 100.0,
197            height: 40.0,
198        };
199        let layer = GraphicsLayer {
200            rotation_z: 90.0,
201            transform_origin: TransformOrigin::CENTER,
202            ..Default::default()
203        };
204
205        let transform = layer_transform_to_parent(local_bounds, Point::default(), &layer);
206        let mapped = transform.bounds_for_rect(local_bounds);
207
208        assert!((mapped.width - 40.0).abs() < 0.01);
209        assert!((mapped.height - 100.0).abs() < 0.01);
210    }
211
212    #[test]
213    fn layer_transform_to_parent_scales_about_transform_origin() {
214        let local_bounds = Rect {
215            x: 0.0,
216            y: 0.0,
217            width: 36.0,
218            height: 36.0,
219        };
220        let placement = Point { x: 54.0, y: 416.0 };
221        let small = layer_transform_to_parent(
222            local_bounds,
223            placement,
224            &GraphicsLayer {
225                scale: 0.85,
226                transform_origin: TransformOrigin::CENTER,
227                ..Default::default()
228            },
229        )
230        .bounds_for_rect(local_bounds);
231        let large = layer_transform_to_parent(
232            local_bounds,
233            placement,
234            &GraphicsLayer {
235                scale: 1.15,
236                transform_origin: TransformOrigin::CENTER,
237                ..Default::default()
238            },
239        )
240        .bounds_for_rect(local_bounds);
241
242        let small_center_y = small.y + small.height * 0.5;
243        let large_center_y = large.y + large.height * 0.5;
244        assert!(
245            (small_center_y - large_center_y).abs() < 0.01,
246            "scale must not move the layer center when transform origin is centered"
247        );
248    }
249}