Skip to main content

cranpose_ui_graphics/
layer_transform.rs

1use crate::{
2    GraphicsLayer, Point, Rect,
3    transform::{ProjectiveTransform, quad_bounds},
4};
5
6#[doc(hidden)]
7pub fn layer_scale_x(layer: &GraphicsLayer) -> f32 {
8    layer.scale * layer.scale_x
9}
10
11#[doc(hidden)]
12pub fn layer_scale_y(layer: &GraphicsLayer) -> f32 {
13    layer.scale * layer.scale_y
14}
15
16/// Returns the smaller of the layer's horizontal and vertical scales.
17pub fn layer_uniform_scale(layer: &GraphicsLayer) -> f32 {
18    layer_scale_x(layer).min(layer_scale_y(layer))
19}
20
21#[doc(hidden)]
22pub fn layer_affine_is_identity(layer: &GraphicsLayer) -> bool {
23    layer.scale == 1.0
24        && layer.scale_x == 1.0
25        && layer.scale_y == 1.0
26        && layer.translation_x == 0.0
27        && layer.translation_y == 0.0
28}
29
30/// Scales and translates a rectangle around the layer's transform origin.
31pub fn apply_layer_affine_to_rect(rect: Rect, layer_bounds: Rect, layer: &GraphicsLayer) -> Rect {
32    if layer_affine_is_identity(layer) {
33        return rect;
34    }
35    let scale_x = layer_scale_x(layer);
36    let scale_y = layer_scale_y(layer);
37    let (pivot_x, pivot_y) = layer_rotation_pivot(layer_bounds, layer);
38    Rect {
39        x: pivot_x + (rect.x - pivot_x) * scale_x + layer.translation_x,
40        y: pivot_y + (rect.y - pivot_y) * scale_y + layer.translation_y,
41        width: rect.width * scale_x,
42        height: rect.height * scale_y,
43    }
44}
45
46/// Point counterpart of [`apply_layer_affine_to_rect`]: scale about the layer
47/// pivot, then translate. Rotation/perspective are deliberately excluded — the
48/// affine space is the one SDF shapes are evaluated in, with rotation carried
49/// by the deformed quad instead.
50pub fn apply_layer_affine_to_point(
51    point: Point,
52    layer_bounds: Rect,
53    layer: &GraphicsLayer,
54) -> Point {
55    if layer_affine_is_identity(layer) {
56        return point;
57    }
58    let (pivot_x, pivot_y) = layer_rotation_pivot(layer_bounds, layer);
59    Point::new(
60        pivot_x + (point.x - pivot_x) * layer_scale_x(layer) + layer.translation_x,
61        pivot_y + (point.y - pivot_y) * layer_scale_y(layer) + layer.translation_y,
62    )
63}
64
65fn layer_rotation_pivot(layer_bounds: Rect, layer: &GraphicsLayer) -> (f32, f32) {
66    (
67        layer_bounds.x + layer_bounds.width * layer.transform_origin.pivot_fraction_x,
68        layer_bounds.y + layer_bounds.height * layer.transform_origin.pivot_fraction_y,
69    )
70}
71
72#[doc(hidden)]
73pub fn layer_scales_or_rotates(layer: &GraphicsLayer) -> bool {
74    layer_scale_x(layer) != 1.0 || layer_scale_y(layer) != 1.0 || layer_has_rotation(layer)
75}
76
77fn layer_has_rotation(layer: &GraphicsLayer) -> bool {
78    layer.rotation_x.abs() > f32::EPSILON
79        || layer.rotation_y.abs() > f32::EPSILON
80        || layer.rotation_z.abs() > f32::EPSILON
81}
82
83fn rotation_axes(layer: &GraphicsLayer) -> [[f32; 3]; 2] {
84    let (sin_x, cos_x) = layer.rotation_x.to_radians().sin_cos();
85    let (sin_y, cos_y) = layer.rotation_y.to_radians().sin_cos();
86    let (sin_z, cos_z) = layer.rotation_z.to_radians().sin_cos();
87    [
88        [cos_z * cos_y, sin_z * cos_y, -sin_y],
89        [
90            cos_z * sin_x * sin_y - sin_z * cos_x,
91            sin_z * sin_x * sin_y + cos_z * cos_x,
92            sin_x * cos_y,
93        ],
94    ]
95}
96
97fn camera_distance(layer: &GraphicsLayer) -> f32 {
98    const CAMERA_DISTANCE_SCALE: f32 = 72.0;
99    (layer.camera_distance * CAMERA_DISTANCE_SCALE).max(1.0)
100}
101
102fn apply_rotation_and_perspective(
103    point: [f32; 2],
104    pivot: (f32, f32),
105    layer: &GraphicsLayer,
106) -> [f32; 2] {
107    if !layer_has_rotation(layer) {
108        return point;
109    }
110    let (x, y) = (point[0] - pivot.0, point[1] - pivot.1);
111    let [x_axis, y_axis] = rotation_axes(layer);
112    let [turned_x, turned_y, turned_z] = [0, 1, 2].map(|axis| x * x_axis[axis] + y * y_axis[axis]);
113    let camera_distance = camera_distance(layer);
114    let perspective = camera_distance / (camera_distance - turned_z).max(1.0);
115    [
116        pivot.0 + turned_x * perspective,
117        pivot.1 + turned_y * perspective,
118    ]
119}
120
121fn apply_layer_to_point(point: [f32; 2], pivot: (f32, f32), layer: &GraphicsLayer) -> [f32; 2] {
122    let scaled = [
123        pivot.0 + (point[0] - pivot.0) * layer_scale_x(layer),
124        pivot.1 + (point[1] - pivot.1) * layer_scale_y(layer),
125    ];
126    let rotated = apply_rotation_and_perspective(scaled, pivot, layer);
127    [
128        rotated[0] + layer.translation_x,
129        rotated[1] + layer.translation_y,
130    ]
131}
132
133/// Transforms a rectangle's four corners through scale, rotation and perspective.
134pub fn apply_layer_to_quad(rect: Rect, layer_bounds: Rect, layer: &GraphicsLayer) -> [[f32; 2]; 4] {
135    let quad = [
136        [rect.x, rect.y],
137        [rect.x + rect.width, rect.y],
138        [rect.x, rect.y + rect.height],
139        [rect.x + rect.width, rect.y + rect.height],
140    ];
141    if layer_affine_is_identity(layer) && !layer_has_rotation(layer) {
142        return quad;
143    }
144    let pivot = layer_rotation_pivot(layer_bounds, layer);
145    quad.map(|point| apply_layer_to_point(point, pivot, layer))
146}
147
148/// Returns the axis-aligned bounds after the complete layer transform.
149pub fn apply_layer_to_rect(rect: Rect, layer_bounds: Rect, layer: &GraphicsLayer) -> Rect {
150    quad_bounds(apply_layer_to_quad(rect, layer_bounds, layer))
151}
152
153/// Combines a graphics layer with the node's placement in its parent.
154pub fn layer_transform_to_parent(
155    local_bounds: Rect,
156    placement: Point,
157    layer: &GraphicsLayer,
158) -> ProjectiveTransform {
159    if !layer_scales_or_rotates(layer) {
160        return ProjectiveTransform::translation(
161            placement.x + layer.translation_x,
162            placement.y + layer.translation_y,
163        );
164    }
165    let (pivot_x, pivot_y) = layer_rotation_pivot(local_bounds, layer);
166    let [x_axis, y_axis] = rotation_axes(layer);
167    let (scale_x, scale_y) = (layer_scale_x(layer), layer_scale_y(layer));
168    let camera_distance = camera_distance(layer);
169    let turn = ProjectiveTransform::from_homogeneous([
170        [x_axis[0] * scale_x, y_axis[0] * scale_y, 0.0],
171        [x_axis[1] * scale_x, y_axis[1] * scale_y, 0.0],
172        [
173            -x_axis[2] * scale_x / camera_distance,
174            -y_axis[2] * scale_y / camera_distance,
175            1.0,
176        ],
177    ]);
178    let placed = ProjectiveTransform::translation(-pivot_x, -pivot_y)
179        .then(turn)
180        .then(ProjectiveTransform::translation(pivot_x, pivot_y))
181        .then(ProjectiveTransform::translation(
182            placement.x + layer.translation_x,
183            placement.y + layer.translation_y,
184        ));
185    ProjectiveTransform::from_homogeneous(placed.matrix())
186}
187
188/// Extends an ancestor mapping from untransformed layout coordinates to window
189/// coordinates with this node's graphics layer. `origin` and `bounds` describe
190/// the node before any ancestor transforms; `bounds` is relative to `origin`.
191pub fn layer_transform_to_window(
192    parent: ProjectiveTransform,
193    origin: Point,
194    bounds: Rect,
195    layer: &GraphicsLayer,
196) -> ProjectiveTransform {
197    if layer_affine_is_identity(layer) && !layer_has_rotation(layer) {
198        return parent;
199    }
200    let bounds = Rect {
201        x: bounds.x + origin.x,
202        y: bounds.y + origin.y,
203        ..bounds
204    };
205    layer_transform_to_parent(bounds, Point::default(), layer).then(parent)
206}