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
17pub fn apply_layer_affine_to_rect(rect: Rect, layer_bounds: Rect, layer: &GraphicsLayer) -> Rect {
18    let offset_x = rect.x - layer_bounds.x;
19    let offset_y = rect.y - layer_bounds.y;
20    let scale_x = layer_scale_x(layer);
21    let scale_y = layer_scale_y(layer);
22    Rect {
23        x: layer_bounds.x + offset_x * scale_x + layer.translation_x,
24        y: layer_bounds.y + offset_y * scale_y + layer.translation_y,
25        width: rect.width * scale_x,
26        height: rect.height * scale_y,
27    }
28}
29
30fn layer_rotation_pivot(layer_bounds: Rect, layer: &GraphicsLayer) -> (f32, f32) {
31    (
32        layer_bounds.x + layer_bounds.width * layer.transform_origin.pivot_fraction_x,
33        layer_bounds.y + layer_bounds.height * layer.transform_origin.pivot_fraction_y,
34    )
35}
36
37fn layer_has_rotation(layer: &GraphicsLayer) -> bool {
38    layer.rotation_x.abs() > f32::EPSILON
39        || layer.rotation_y.abs() > f32::EPSILON
40        || layer.rotation_z.abs() > f32::EPSILON
41}
42
43fn apply_rotation_and_perspective(
44    point: [f32; 2],
45    pivot: (f32, f32),
46    layer: &GraphicsLayer,
47) -> [f32; 2] {
48    if !layer_has_rotation(layer) {
49        return point;
50    }
51
52    let mut x = point[0] - pivot.0;
53    let mut y = point[1] - pivot.1;
54    let mut z = 0.0f32;
55
56    let (sin_x, cos_x) = layer.rotation_x.to_radians().sin_cos();
57    let (sin_y, cos_y) = layer.rotation_y.to_radians().sin_cos();
58    let (sin_z, cos_z) = layer.rotation_z.to_radians().sin_cos();
59
60    let y_rot_x = y * cos_x - z * sin_x;
61    let z_rot_x = y * sin_x + z * cos_x;
62    y = y_rot_x;
63    z = z_rot_x;
64
65    let x_rot_y = x * cos_y + z * sin_y;
66    let z_rot_y = -x * sin_y + z * cos_y;
67    x = x_rot_y;
68    z = z_rot_y;
69
70    let x_rot_z = x * cos_z - y * sin_z;
71    let y_rot_z = x * sin_z + y * cos_z;
72    x = x_rot_z;
73    y = y_rot_z;
74
75    const CAMERA_DISTANCE_SCALE: f32 = 72.0;
76    let camera_distance = (layer.camera_distance * CAMERA_DISTANCE_SCALE).max(1.0);
77    let denom = (camera_distance - z).max(1.0);
78    let perspective = camera_distance / denom;
79
80    [pivot.0 + x * perspective, pivot.1 + y * perspective]
81}
82
83pub fn apply_layer_to_quad(rect: Rect, layer_bounds: Rect, layer: &GraphicsLayer) -> [[f32; 2]; 4] {
84    let affine_rect = apply_layer_affine_to_rect(rect, layer_bounds, layer);
85    let affine_layer_bounds = apply_layer_affine_to_rect(layer_bounds, layer_bounds, layer);
86    let pivot = layer_rotation_pivot(affine_layer_bounds, layer);
87    let quad = [
88        [affine_rect.x, affine_rect.y],
89        [affine_rect.x + affine_rect.width, affine_rect.y],
90        [affine_rect.x, affine_rect.y + affine_rect.height],
91        [
92            affine_rect.x + affine_rect.width,
93            affine_rect.y + affine_rect.height,
94        ],
95    ];
96
97    quad.map(|point| apply_rotation_and_perspective(point, pivot, layer))
98}
99
100pub fn apply_layer_to_rect(rect: Rect, layer_bounds: Rect, layer: &GraphicsLayer) -> Rect {
101    quad_bounds(apply_layer_to_quad(rect, layer_bounds, layer))
102}
103
104pub fn layer_transform_to_parent(
105    local_bounds: Rect,
106    placement: Point,
107    layer: &GraphicsLayer,
108) -> ProjectiveTransform {
109    let placement_rect = Rect {
110        x: placement.x,
111        y: placement.y,
112        width: local_bounds.width,
113        height: local_bounds.height,
114    };
115    ProjectiveTransform::from_rect_to_quad(
116        local_bounds,
117        apply_layer_to_quad(placement_rect, placement_rect, layer),
118    )
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use cranpose_ui_graphics::{Rect, TransformOrigin};
125
126    #[test]
127    fn layer_transform_to_parent_maps_local_bounds_to_positioned_bounds() {
128        let local_bounds = Rect {
129            x: 0.0,
130            y: 0.0,
131            width: 30.0,
132            height: 18.0,
133        };
134        let placement = Point { x: 12.0, y: 9.0 };
135        let transform =
136            layer_transform_to_parent(local_bounds, placement, &GraphicsLayer::default());
137
138        assert_eq!(
139            transform.bounds_for_rect(local_bounds),
140            Rect {
141                x: 12.0,
142                y: 9.0,
143                width: 30.0,
144                height: 18.0,
145            }
146        );
147    }
148
149    #[test]
150    fn layer_transform_to_parent_applies_rotation_about_transform_origin() {
151        let local_bounds = Rect {
152            x: 0.0,
153            y: 0.0,
154            width: 100.0,
155            height: 40.0,
156        };
157        let layer = GraphicsLayer {
158            rotation_z: 90.0,
159            transform_origin: TransformOrigin::CENTER,
160            ..Default::default()
161        };
162
163        let transform = layer_transform_to_parent(local_bounds, Point::default(), &layer);
164        let mapped = transform.bounds_for_rect(local_bounds);
165
166        assert!((mapped.width - 40.0).abs() < 0.01);
167        assert!((mapped.height - 100.0).abs() < 0.01);
168    }
169}