Skip to main content

cranpose_render_common/
layer_transform.rs

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