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 {
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
44pub 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 cranpose_ui_graphics::{Rect, TransformOrigin};
166
167 use super::*;
168
169 #[test]
170 fn layer_transform_to_parent_maps_local_bounds_to_positioned_bounds() {
171 let local_bounds = Rect {
172 x: 0.0,
173 y: 0.0,
174 width: 30.0,
175 height: 18.0,
176 };
177 let placement = Point { x: 12.0, y: 9.0 };
178 let transform =
179 layer_transform_to_parent(local_bounds, placement, &GraphicsLayer::default());
180
181 assert_eq!(
182 transform.bounds_for_rect(local_bounds),
183 Rect {
184 x: 12.0,
185 y: 9.0,
186 width: 30.0,
187 height: 18.0,
188 }
189 );
190 }
191
192 #[test]
193 fn layer_transform_to_parent_applies_rotation_about_transform_origin() {
194 let local_bounds = Rect {
195 x: 0.0,
196 y: 0.0,
197 width: 100.0,
198 height: 40.0,
199 };
200 let layer = GraphicsLayer {
201 rotation_z: 90.0,
202 transform_origin: TransformOrigin::CENTER,
203 ..Default::default()
204 };
205
206 let transform = layer_transform_to_parent(local_bounds, Point::default(), &layer);
207 let mapped = transform.bounds_for_rect(local_bounds);
208
209 assert!((mapped.width - 40.0).abs() < 0.01);
210 assert!((mapped.height - 100.0).abs() < 0.01);
211 }
212
213 #[test]
214 fn layer_transform_to_parent_scales_about_transform_origin() {
215 let local_bounds = Rect {
216 x: 0.0,
217 y: 0.0,
218 width: 36.0,
219 height: 36.0,
220 };
221 let placement = Point { x: 54.0, y: 416.0 };
222 let small = layer_transform_to_parent(
223 local_bounds,
224 placement,
225 &GraphicsLayer {
226 scale: 0.85,
227 transform_origin: TransformOrigin::CENTER,
228 ..Default::default()
229 },
230 )
231 .bounds_for_rect(local_bounds);
232 let large = layer_transform_to_parent(
233 local_bounds,
234 placement,
235 &GraphicsLayer {
236 scale: 1.15,
237 transform_origin: TransformOrigin::CENTER,
238 ..Default::default()
239 },
240 )
241 .bounds_for_rect(local_bounds);
242
243 let small_center_y = small.y + small.height * 0.5;
244 let large_center_y = large.y + large.height * 0.5;
245 assert!(
246 (small_center_y - large_center_y).abs() < 0.01,
247 "scale must not move the layer center when transform origin is centered"
248 );
249 }
250}