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