1use cranpose_ui_graphics::{Color, CornerRadii, GraphicsLayer, Rect, RoundedCornerShape};
7
8use crate::layer_transform::layer_uniform_scale;
9
10const MIN_LAYER_SHADOW_SCALE: f32 = 0.1;
11const AMBIENT_SHADOW_ALPHA: f32 = 0.039;
15const SPOT_SHADOW_ALPHA: f32 = 0.19;
16const LIGHT_Y: f32 = 0.0;
18const LIGHT_Z: f32 = 500.0;
19const LIGHT_RADIUS: f32 = 800.0;
20const LIGHT_Z_REFERENCE: f32 = 450.0;
23const MAX_AMBIENT_REACH_PIXELS: f32 = 150.0;
26const FALLOFF_TEXELS: f32 = 128.0;
29const FALLOFF_FLOOR: f32 = 0.018;
30
31#[derive(Clone, Copy, Debug, PartialEq)]
34pub struct ShadowLight {
35 pub x: f32,
37 pub y: f32,
39 pub z: f32,
41 pub radius: f32,
43 pub pixels_per_unit: f32,
46}
47
48impl ShadowLight {
49 pub fn for_window(width: f32, height: f32, units_per_dp: f32, pixels_per_unit: f32) -> Self {
54 let units_per_dp = units_per_dp.max(f32::EPSILON);
55 let window_dp = width.min(height).max(0.0) / units_per_dp;
56 let adjustment = (window_dp / LIGHT_Z_REFERENCE + 2.0) / 3.0;
57 Self {
58 x: width / 2.0,
59 y: LIGHT_Y * units_per_dp,
60 z: LIGHT_Z * units_per_dp * adjustment,
61 radius: LIGHT_RADIUS * units_per_dp,
62 pixels_per_unit,
63 }
64 }
65
66 pub fn in_space(self, origin_x: f32, origin_y: f32, scale: f32) -> Self {
69 let scale = scale.max(MIN_LAYER_SHADOW_SCALE);
70 Self {
71 x: (self.x - origin_x) / scale,
72 y: (self.y - origin_y) / scale,
73 z: self.z / scale,
74 radius: self.radius / scale,
75 pixels_per_unit: self.pixels_per_unit * scale,
76 }
77 }
78}
79
80#[derive(Clone, Copy, Debug, PartialEq)]
87pub struct ShadowRRect {
88 pub bounds: Rect,
90 pub radii: [f32; 4],
93 pub umbra_inset: f32,
95 pub distance_correction: f32,
97 pub hole: Option<ShadowHole>,
99}
100
101#[derive(Clone, Copy, Debug, PartialEq)]
104pub struct ShadowHole {
105 pub inset: f32,
107 pub radius: f32,
109}
110
111impl ShadowRRect {
112 fn new(outer: Rect, radii: [f32; 4], blur: f32, inset_width: f32) -> Option<Self> {
116 let half_side = 0.5 * outer.width.min(outer.height);
117 if half_side <= 0.0 || blur <= 0.0 {
118 return None;
119 }
120 let largest = radii.into_iter().fold(0.0, f32::max);
121 let circle = (outer.width - outer.height).abs() <= f32::EPSILON * outer.width
122 && largest >= half_side;
123 let (umbra_inset, hole) = if circle {
124 let inner = half_side - inset_width;
125 let hole = (inner > 0.0).then_some(ShadowHole {
126 inset: inset_width,
127 radius: inner,
128 });
129 (half_side, hole)
130 } else {
131 let umbra = largest.max(blur);
132 let hole = (inset_width <= half_side).then(|| ShadowHole {
133 inset: inset_width.max(umbra.min(half_side)),
134 radius: 0.0,
135 });
136 (umbra.min(half_side), hole)
137 };
138 Some(Self {
139 bounds: outer,
140 radii: if circle { [half_side; 4] } else { radii },
141 umbra_inset,
142 distance_correction: umbra_inset / blur,
143 hole: hole.filter(|hole| hole.inset < half_side),
144 })
145 }
146
147 pub fn scaled(self, scale: f32) -> Self {
149 Self {
150 bounds: Rect {
151 x: self.bounds.x * scale,
152 y: self.bounds.y * scale,
153 width: self.bounds.width * scale,
154 height: self.bounds.height * scale,
155 },
156 radii: self.radii.map(|radius| radius * scale),
157 umbra_inset: self.umbra_inset * scale,
158 distance_correction: self.distance_correction,
159 hole: self.hole.map(|hole| ShadowHole {
160 inset: hole.inset * scale,
161 radius: hole.radius * scale,
162 }),
163 }
164 }
165
166 pub fn translated(self, dx: f32, dy: f32) -> Self {
168 Self {
169 bounds: self.bounds.translate(dx, dy),
170 ..self
171 }
172 }
173
174 pub fn hole_rect(&self) -> Option<Rect> {
176 self.hole.map(|hole| Rect {
177 x: self.bounds.x + hole.inset,
178 y: self.bounds.y + hole.inset,
179 width: self.bounds.width - 2.0 * hole.inset,
180 height: self.bounds.height - 2.0 * hole.inset,
181 })
182 }
183
184 pub fn coverage(&self, x: f32, y: f32) -> f32 {
188 let bounds = self.bounds;
189 let right_edge = bounds.x + bounds.width;
190 let bottom_edge = bounds.y + bounds.height;
191 if x < bounds.x || y < bounds.y || x >= right_edge || y >= bottom_edge {
192 return 0.0;
193 }
194 if let (Some(hole), Some(rect)) = (self.hole, self.hole_rect())
195 && rounded_rect_contains(rect, hole.radius, x, y)
196 {
197 return 0.0;
198 }
199 let umbra = self.umbra_inset;
200 let left = bounds.x + umbra - x;
201 let right = x - (right_edge - umbra);
202 let top = bounds.y + umbra - y;
203 let bottom = y - (bottom_edge - umbra);
204 let corner = match (left > right, top > bottom) {
205 (true, true) => 0,
206 (false, true) => 1,
207 (false, false) => 2,
208 (true, false) => 3,
209 };
210 let offset = fan_offset(
211 left.max(right).max(0.0),
212 top.max(bottom).max(0.0),
213 umbra,
214 self.radii[corner],
215 );
216 shadow_falloff(self.distance_correction * (1.0 - offset))
217 }
218}
219
220fn rounded_rect_contains(rect: Rect, radius: f32, x: f32, y: f32) -> bool {
221 let half_width = 0.5 * rect.width;
222 let half_height = 0.5 * rect.height;
223 let dx = (x - rect.x - half_width).abs() - (half_width - radius);
224 let dy = (y - rect.y - half_height).abs() - (half_height - radius);
225 let outside = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
226 outside + dx.max(dy).min(0.0) - radius < 0.0
227}
228
229pub fn fan_offset(along: f32, across: f32, umbra: f32, radius: f32) -> f32 {
236 let (a, b) = if along >= across {
237 (along, across)
238 } else {
239 (across, along)
240 };
241 if a <= 0.0 || umbra <= 0.0 {
242 return 0.0;
243 }
244 let length = (2.0 * (radius * radius + umbra * umbra)).sqrt();
245 let outer = [(radius - umbra) / length, (-radius - umbra) / length];
246 let straight = umbra - radius;
247 let [x, y] = if b * umbra <= a * straight {
248 let toward_curve = if straight > 0.0 { b / straight } else { 0.0 };
249 let toward_edge = a / umbra - toward_curve;
250 [
251 toward_curve * outer[0],
252 -toward_edge + toward_curve * outer[1],
253 ]
254 } else {
255 let diagonal = umbra / (std::f32::consts::SQRT_2 * (radius - umbra) - radius);
256 let toward_curve = (a - b) / radius;
257 let toward_corner = a / umbra - toward_curve;
258 [
259 toward_curve * outer[0] + toward_corner * diagonal,
260 toward_curve * outer[1] + toward_corner * diagonal,
261 ]
262 };
263 (x * x + y * y).sqrt()
264}
265
266pub fn shadow_falloff(x: f32) -> f32 {
269 let texel = ((x * FALLOFF_TEXELS - 0.5) / (FALLOFF_TEXELS - 1.0)).clamp(0.0, 1.0);
270 let distance = 1.0 - texel;
271 ((-4.0 * distance * distance).exp() - FALLOFF_FLOOR).max(0.0)
272}
273
274#[derive(Clone, Copy, Debug, Default, PartialEq)]
276pub struct LayerShadowGeometry {
277 pub ambient: Option<(ShadowRRect, Color)>,
279 pub spot: Option<(ShadowRRect, Color)>,
281}
282
283impl LayerShadowGeometry {
284 pub fn passes(&self) -> impl Iterator<Item = (ShadowRRect, Color)> {
286 self.ambient.into_iter().chain(self.spot)
287 }
288}
289
290fn outset(rect: Rect, amount: f32) -> Rect {
291 Rect {
292 x: rect.x - amount,
293 y: rect.y - amount,
294 width: rect.width + amount * 2.0,
295 height: rect.height + amount * 2.0,
296 }
297}
298
299fn corner_radii(radii: CornerRadii) -> [f32; 4] {
300 [
301 radii.top_left,
302 radii.top_right,
303 radii.bottom_right,
304 radii.bottom_left,
305 ]
306}
307
308fn pass_color(color: Color, share: f32, caster_alpha: f32) -> Option<Color> {
313 let share_byte = (share * 255.0).floor();
314 let color_byte = (color.a().clamp(0.0, 1.0) * 255.0).round();
315 let byte = (color_byte * share_byte / 255.0 * caster_alpha).floor();
316 (byte > 0.0).then(|| Color(color.r(), color.g(), color.b(), byte / 255.0))
317}
318
319fn caster_occludes(layer: &GraphicsLayer) -> bool {
322 layer.alpha >= 1.0
323 && layer.rotation_x.abs() <= f32::EPSILON
324 && layer.rotation_y.abs() <= f32::EPSILON
325 && layer.rotation_z.abs() <= f32::EPSILON
326}
327
328fn spot_stray(caster: Rect, spot: Rect, caster_radius: f32, spot_radius: f32) -> f32 {
332 if caster_radius <= 0.0 {
333 return (spot.x - caster.x)
334 .abs()
335 .max((spot.y - caster.y).abs())
336 .max((spot.x + spot.width - caster.x - caster.width).abs())
337 .max((spot.y + spot.height - caster.y - caster.height).abs());
338 }
339 let grown = spot_radius - caster_radius;
340 let upper = [spot.x - caster.x + grown, spot.y - caster.y + grown];
341 let lower = [
342 spot.x + spot.width - caster.x - caster.width - grown,
343 spot.y + spot.height - caster.y - caster.height - grown,
344 ];
345 let length = |[x, y]: [f32; 2]| x * x + y * y;
346 length(upper).max(length(lower)).sqrt() + grown
347}
348
349pub fn layer_shadow_geometry(
353 layer: &GraphicsLayer,
354 transformed_bounds: Rect,
355 caster: Option<RoundedCornerShape>,
356 light: ShadowLight,
357) -> LayerShadowGeometry {
358 if layer.shadow_elevation <= 0.0 {
359 return LayerShadowGeometry::default();
360 }
361
362 let scale = layer_uniform_scale(layer).max(MIN_LAYER_SHADOW_SCALE);
363 let height = layer.shadow_elevation * scale;
364 let caster_alpha = layer.alpha.clamp(0.0, 1.0);
365 let radii = caster.map_or([0.0; 4], |shape| corner_radii(shape.radii()));
366 let occludes = caster_occludes(layer);
367 let pixels_per_unit = light.pixels_per_unit.max(f32::EPSILON);
368
369 let reach = (height * 0.5).min(MAX_AMBIENT_REACH_PIXELS / pixels_per_unit);
372 let ambient_outer = outset(transformed_bounds, reach);
373 let ambient = pass_color(
374 layer.ambient_shadow_color,
375 AMBIENT_SHADOW_ALPHA,
376 caster_alpha,
377 )
378 .and_then(|color| {
379 let blur = reach * (1.0 + height * pixels_per_unit / 128.0);
380 let inset = if occludes { reach } else { ambient_outer.width };
381 ShadowRRect::new(ambient_outer, radii.map(|r| r + reach), blur, inset)
382 .map(|shadow| (shadow, color))
383 });
384
385 let depth = light.z - height;
389 let z_ratio = if depth > f32::EPSILON {
390 (height / depth).clamp(0.0, 0.95)
391 } else {
392 0.95
393 };
394 let spot_scale = 1.0 + z_ratio;
395 let spot_blur = light.radius * z_ratio;
396 let projected = Rect {
397 x: transformed_bounds.x + z_ratio * (transformed_bounds.x - light.x),
398 y: transformed_bounds.y + z_ratio * (transformed_bounds.y - light.y),
399 width: transformed_bounds.width * spot_scale,
400 height: transformed_bounds.height * spot_scale,
401 };
402 let spot =
403 pass_color(layer.spot_shadow_color, SPOT_SHADOW_ALPHA, caster_alpha).and_then(|color| {
404 let caster_radius = radii.into_iter().fold(0.0, f32::max);
405 let inset = spot_blur
406 + if occludes {
407 spot_blur.max(spot_stray(
408 transformed_bounds,
409 projected,
410 caster_radius,
411 caster_radius * spot_scale,
412 ))
413 } else {
414 projected.width
415 };
416 ShadowRRect::new(
417 outset(projected, spot_blur),
418 radii.map(|r| r * spot_scale + spot_blur),
419 2.0 * spot_blur,
420 inset,
421 )
422 .map(|shadow| (shadow, color))
423 });
424
425 LayerShadowGeometry { ambient, spot }
426}
427
428#[cfg(test)]
429#[path = "tests/layer_shadow_tests.rs"]
430mod tests;