cranpose_render_common/
layer_shadow.rs1use cranpose_ui_graphics::{GraphicsLayer, Rect};
2
3use crate::layer_transform::layer_uniform_scale;
4
5const MIN_LAYER_SHADOW_SCALE: f32 = 0.1;
6const MIN_LAYER_SHADOW_SPREAD: f32 = 0.8;
7const MIN_AMBIENT_BLUR_RADIUS: f32 = 0.5;
8const MIN_SPOT_BLUR_RADIUS: f32 = 0.5;
9const AMBIENT_SPREAD_FACTOR: f32 = 0.24;
10const SPOT_OFFSET_X_FACTOR: f32 = 0.18;
11const SPOT_OFFSET_Y_FACTOR: f32 = 0.62;
12const AMBIENT_BLUR_FACTOR: f32 = 0.95;
13const SPOT_BLUR_FACTOR: f32 = 0.72;
14const SPOT_SPREAD_FACTOR: f32 = 0.72;
15const AMBIENT_ALPHA_FACTOR: f32 = 0.44;
16const SPOT_ALPHA_FACTOR: f32 = 0.62;
17
18#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct LayerShadowPass {
20 pub rect: Rect,
21 pub blur_radius: f32,
22 pub alpha: f32,
23}
24
25#[derive(Clone, Copy, Debug, Default, PartialEq)]
26pub struct LayerShadowGeometry {
27 pub ambient: Option<LayerShadowPass>,
28 pub spot: Option<LayerShadowPass>,
29}
30
31pub fn layer_shadow_geometry(
32 layer: &GraphicsLayer,
33 transformed_bounds: Rect,
34) -> LayerShadowGeometry {
35 if layer.shadow_elevation <= 0.0 {
36 return LayerShadowGeometry::default();
37 }
38
39 let scale = layer_uniform_scale(layer).max(MIN_LAYER_SHADOW_SCALE);
40 let elevation = layer.shadow_elevation * scale;
41 let spread = (elevation * AMBIENT_SPREAD_FACTOR).max(MIN_LAYER_SHADOW_SPREAD);
42 let ambient_alpha = (layer.ambient_shadow_color.a() * AMBIENT_ALPHA_FACTOR).clamp(0.0, 1.0);
43 let spot_alpha = (layer.spot_shadow_color.a() * SPOT_ALPHA_FACTOR).clamp(0.0, 1.0);
44
45 let ambient = (ambient_alpha > f32::EPSILON).then_some(LayerShadowPass {
46 rect: Rect {
47 x: transformed_bounds.x - spread,
48 y: transformed_bounds.y - spread,
49 width: transformed_bounds.width + spread * 2.0,
50 height: transformed_bounds.height + spread * 2.0,
51 },
52 blur_radius: (elevation * AMBIENT_BLUR_FACTOR).max(MIN_AMBIENT_BLUR_RADIUS),
53 alpha: ambient_alpha,
54 });
55
56 let spot_spread = spread * SPOT_SPREAD_FACTOR;
57 let spot = (spot_alpha > f32::EPSILON).then_some(LayerShadowPass {
58 rect: Rect {
59 x: transformed_bounds.x + elevation * SPOT_OFFSET_X_FACTOR - spot_spread,
60 y: transformed_bounds.y + elevation * SPOT_OFFSET_Y_FACTOR - spot_spread,
61 width: transformed_bounds.width + spot_spread * 2.0,
62 height: transformed_bounds.height + spot_spread * 2.0,
63 },
64 blur_radius: (elevation * SPOT_BLUR_FACTOR).max(MIN_SPOT_BLUR_RADIUS),
65 alpha: spot_alpha,
66 });
67
68 LayerShadowGeometry { ambient, spot }
69}
70
71#[cfg(test)]
72mod tests {
73 use cranpose_ui_graphics::Color;
74
75 use super::*;
76
77 #[test]
78 fn layer_shadow_geometry_returns_none_for_zero_elevation() {
79 let geometry = layer_shadow_geometry(
80 &GraphicsLayer::default(),
81 Rect::from_size(Default::default()),
82 );
83 assert!(geometry.ambient.is_none());
84 assert!(geometry.spot.is_none());
85 }
86
87 #[test]
88 fn layer_shadow_geometry_matches_shadow_model() {
89 let layer = GraphicsLayer {
90 shadow_elevation: 10.0,
91 ambient_shadow_color: Color(0.2, 0.3, 0.4, 0.8),
92 spot_shadow_color: Color(0.7, 0.6, 0.5, 0.9),
93 ..GraphicsLayer::default()
94 };
95 let transformed_bounds = Rect {
96 x: 20.0,
97 y: 30.0,
98 width: 40.0,
99 height: 12.0,
100 };
101 let geometry = layer_shadow_geometry(&layer, transformed_bounds);
102
103 let ambient = geometry.ambient.expect("ambient shadow pass");
104 assert_eq!(
105 ambient.rect,
106 Rect {
107 x: 17.6,
108 y: 27.6,
109 width: 44.8,
110 height: 16.8,
111 }
112 );
113 assert!((ambient.blur_radius - 9.5).abs() < 1e-6);
114 assert!((ambient.alpha - 0.352).abs() < 1e-6);
115
116 let spot = geometry.spot.expect("spot shadow pass");
117 assert_eq!(
118 spot.rect,
119 Rect {
120 x: 20.071999,
121 y: 34.472,
122 width: 43.456,
123 height: 15.455999,
124 }
125 );
126 assert!((spot.blur_radius - 7.2).abs() < 1e-6);
127 assert!((spot.alpha - 0.558).abs() < 1e-6);
128 }
129}