1use std::ops::Range;
7
8use cranpose_ui_graphics::{Color, CornerRadii, GraphicsLayer, Rect, RoundedCornerShape};
9
10use crate::layer_transform::layer_uniform_scale;
11
12const MIN_LAYER_SHADOW_SCALE: f32 = 0.1;
13const AMBIENT_SHADOW_ALPHA: f32 = 0.039;
17const SPOT_SHADOW_ALPHA: f32 = 0.19;
18const LIGHT_Y: f32 = 0.0;
20const LIGHT_Z: f32 = 500.0;
21const LIGHT_RADIUS: f32 = 800.0;
22const LIGHT_Z_REFERENCE: f32 = 450.0;
25const MAX_AMBIENT_REACH_PIXELS: f32 = 150.0;
28const FALLOFF_TEXELS: f32 = 128.0;
31const FALLOFF_FLOOR: f32 = 0.018;
32
33#[derive(Clone, Copy, Debug, PartialEq)]
36pub struct ShadowLight {
37 pub x: f32,
39 pub y: f32,
41 pub z: f32,
43 pub radius: f32,
45 pub pixels_per_unit: f32,
48}
49
50impl ShadowLight {
51 pub fn for_window(width: f32, height: f32, units_per_dp: f32, pixels_per_unit: f32) -> Self {
56 let units_per_dp = units_per_dp.max(f32::EPSILON);
57 let window_dp = width.min(height).max(0.0) / units_per_dp;
58 let adjustment = (window_dp / LIGHT_Z_REFERENCE + 2.0) / 3.0;
59 Self {
60 x: width / 2.0,
61 y: LIGHT_Y * units_per_dp,
62 z: LIGHT_Z * units_per_dp * adjustment,
63 radius: LIGHT_RADIUS * units_per_dp,
64 pixels_per_unit,
65 }
66 }
67
68 pub fn in_space(self, origin_x: f32, origin_y: f32, scale: f32) -> Self {
71 let scale = scale.max(MIN_LAYER_SHADOW_SCALE);
72 Self {
73 x: (self.x - origin_x) / scale,
74 y: (self.y - origin_y) / scale,
75 z: self.z / scale,
76 radius: self.radius / scale,
77 pixels_per_unit: self.pixels_per_unit * scale,
78 }
79 }
80}
81
82#[derive(Clone, Copy, Debug, PartialEq)]
89pub struct ShadowRRect {
90 pub bounds: Rect,
92 pub radii: [f32; 4],
95 pub umbra_inset: f32,
97 pub distance_correction: f32,
99 pub hole: Option<ShadowHole>,
101}
102
103#[derive(Clone, Copy, Debug, PartialEq)]
106pub struct ShadowHole {
107 pub inset: f32,
109 pub radius: f32,
111}
112
113impl ShadowRRect {
114 fn new(outer: Rect, radii: [f32; 4], blur: f32, inset_width: f32) -> Option<Self> {
118 let half_side = 0.5 * outer.width.min(outer.height);
119 if half_side <= 0.0 || blur <= 0.0 {
120 return None;
121 }
122 let largest = radii.into_iter().fold(0.0, f32::max);
123 let circle = (outer.width - outer.height).abs() <= f32::EPSILON * outer.width
124 && largest >= half_side;
125 let (umbra_inset, hole) = if circle {
126 let inner = half_side - inset_width;
127 let hole = (inner > 0.0).then_some(ShadowHole {
128 inset: inset_width,
129 radius: inner,
130 });
131 (half_side, hole)
132 } else {
133 let umbra = largest.max(blur);
134 let hole = (inset_width <= half_side).then(|| ShadowHole {
135 inset: inset_width.max(umbra.min(half_side)),
136 radius: 0.0,
137 });
138 (umbra.min(half_side), hole)
139 };
140 Some(Self {
141 bounds: outer,
142 radii: if circle { [half_side; 4] } else { radii },
143 umbra_inset,
144 distance_correction: umbra_inset / blur,
145 hole: hole.filter(|hole| hole.inset < half_side),
146 })
147 }
148
149 pub fn scaled(self, scale: f32) -> Self {
151 Self {
152 bounds: Rect {
153 x: self.bounds.x * scale,
154 y: self.bounds.y * scale,
155 width: self.bounds.width * scale,
156 height: self.bounds.height * scale,
157 },
158 radii: self.radii.map(|radius| radius * scale),
159 umbra_inset: self.umbra_inset * scale,
160 distance_correction: self.distance_correction,
161 hole: self.hole.map(|hole| ShadowHole {
162 inset: hole.inset * scale,
163 radius: hole.radius * scale,
164 }),
165 }
166 }
167
168 pub fn translated(self, dx: f32, dy: f32) -> Self {
170 Self {
171 bounds: self.bounds.translate(dx, dy),
172 ..self
173 }
174 }
175
176 pub fn hole_rect(&self) -> Option<Rect> {
178 self.hole.map(|hole| Rect {
179 x: self.bounds.x + hole.inset,
180 y: self.bounds.y + hole.inset,
181 width: self.bounds.width - 2.0 * hole.inset,
182 height: self.bounds.height - 2.0 * hole.inset,
183 })
184 }
185
186 pub fn coverage(&self, x: f32, y: f32) -> f32 {
190 let bounds = self.bounds;
191 let right_edge = bounds.x + bounds.width;
192 let bottom_edge = bounds.y + bounds.height;
193 if x < bounds.x || y < bounds.y || x >= right_edge || y >= bottom_edge {
194 return 0.0;
195 }
196 if self.hole_holds(x, y) {
197 return 0.0;
198 }
199 let (along, from_left) = self.along(x);
200 let (across, from_top) = self.across(y);
201 self.falloff((along, from_left), (across, from_top))
202 }
203
204 pub fn row_coverage(&self, columns: Range<i32>, y: f32, out: &mut [f32]) {
208 let bounds = self.bounds;
209 let right_edge = bounds.x + bounds.width;
210 let bottom_edge = bounds.y + bounds.height;
211 let row_inside = !(y < bounds.y || y >= bottom_edge);
212 let across = self.across(y);
213 let mut last: Option<((u32, bool), f32)> = None;
214 for (slot, column) in out.iter_mut().zip(columns) {
215 let x = column as f32 + 0.5;
216 if !row_inside || x < bounds.x || x >= right_edge || self.hole_holds(x, y) {
217 *slot = 0.0;
218 continue;
219 }
220 let (along, from_left) = self.along(x);
221 let key = (along.to_bits(), from_left);
222 *slot = match last {
223 Some((seen, coverage)) if seen == key => coverage,
224 _ => {
225 let coverage = self.falloff((along, from_left), across);
226 last = Some((key, coverage));
227 coverage
228 }
229 };
230 }
231 }
232
233 fn hole_holds(&self, x: f32, y: f32) -> bool {
235 match (self.hole, self.hole_rect()) {
236 (Some(hole), Some(rect)) => rounded_rect_contains(rect, hole.radius, x, y),
237 _ => false,
238 }
239 }
240
241 fn along(&self, x: f32) -> (f32, bool) {
244 let umbra = self.umbra_inset;
245 let left = self.bounds.x + umbra - x;
246 let right = x - (self.bounds.x + self.bounds.width - umbra);
247 (left.max(right).max(0.0), left > right)
248 }
249
250 fn across(&self, y: f32) -> (f32, bool) {
253 let umbra = self.umbra_inset;
254 let top = self.bounds.y + umbra - y;
255 let bottom = y - (self.bounds.y + self.bounds.height - umbra);
256 (top.max(bottom).max(0.0), top > bottom)
257 }
258
259 fn falloff(&self, (along, from_left): (f32, bool), (across, from_top): (f32, bool)) -> f32 {
262 let corner = match (from_left, from_top) {
263 (true, true) => 0,
264 (false, true) => 1,
265 (false, false) => 2,
266 (true, false) => 3,
267 };
268 let offset = fan_offset(along, across, self.umbra_inset, self.radii[corner]);
269 shadow_falloff(self.distance_correction * (1.0 - offset))
270 }
271
272 pub fn row_key(&self, y: f32) -> ShadowRowKey {
277 let bounds = self.bounds;
278 let bottom_edge = bounds.y + bounds.height;
279 let (across, upper) = self.across(y);
280 let hole = match (self.hole, self.hole_rect()) {
281 (Some(hole), Some(rect)) => {
282 let dy = overhang(rect.y, rect.height, hole.radius, y);
283 if dy < 0.0 {
286 HoleRow::Straight
287 } else {
288 HoleRow::Curved(dy.to_bits())
289 }
290 }
291 _ => HoleRow::None,
292 };
293 ShadowRowKey {
294 inside: !(y < bounds.y || y >= bottom_edge),
295 across: across.to_bits(),
296 upper,
297 hole,
298 }
299 }
300}
301
302#[derive(Clone, Copy, Debug, PartialEq, Eq)]
304pub struct ShadowRowKey {
305 inside: bool,
306 across: u32,
309 upper: bool,
310 hole: HoleRow,
311}
312
313#[derive(Clone, Copy, Debug, PartialEq, Eq)]
314enum HoleRow {
315 None,
316 Straight,
317 Curved(u32),
319}
320
321fn overhang(start: f32, length: f32, radius: f32, at: f32) -> f32 {
324 let half = 0.5 * length;
325 (at - start - half).abs() - (half - radius)
326}
327
328fn rounded_rect_contains(rect: Rect, radius: f32, x: f32, y: f32) -> bool {
329 let dx = overhang(rect.x, rect.width, radius, x);
330 let dy = overhang(rect.y, rect.height, radius, y);
331 let outside = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
332 outside + dx.max(dy).min(0.0) - radius < 0.0
333}
334
335pub fn fan_offset(along: f32, across: f32, umbra: f32, radius: f32) -> f32 {
342 let (a, b) = if along >= across {
343 (along, across)
344 } else {
345 (across, along)
346 };
347 if a <= 0.0 || umbra <= 0.0 {
348 return 0.0;
349 }
350 let length = (2.0 * (radius * radius + umbra * umbra)).sqrt();
351 let outer = [(radius - umbra) / length, (-radius - umbra) / length];
352 let straight = umbra - radius;
353 let [x, y] = if b * umbra <= a * straight {
354 let toward_curve = if straight > 0.0 { b / straight } else { 0.0 };
355 let toward_edge = a / umbra - toward_curve;
356 [
357 toward_curve * outer[0],
358 -toward_edge + toward_curve * outer[1],
359 ]
360 } else {
361 let diagonal = umbra / (std::f32::consts::SQRT_2 * (radius - umbra) - radius);
362 let toward_curve = (a - b) / radius;
363 let toward_corner = a / umbra - toward_curve;
364 [
365 toward_curve * outer[0] + toward_corner * diagonal,
366 toward_curve * outer[1] + toward_corner * diagonal,
367 ]
368 };
369 (x * x + y * y).sqrt()
370}
371
372pub fn shadow_falloff(x: f32) -> f32 {
375 let texel = ((x * FALLOFF_TEXELS - 0.5) / (FALLOFF_TEXELS - 1.0)).clamp(0.0, 1.0);
376 let distance = 1.0 - texel;
377 ((-4.0 * distance * distance).exp() - FALLOFF_FLOOR).max(0.0)
378}
379
380#[derive(Clone, Copy, Debug, Default, PartialEq)]
382pub struct LayerShadowGeometry {
383 pub ambient: Option<(ShadowRRect, Color)>,
385 pub spot: Option<(ShadowRRect, Color)>,
387}
388
389impl LayerShadowGeometry {
390 pub fn passes(&self) -> impl Iterator<Item = (ShadowRRect, Color)> {
392 self.ambient.into_iter().chain(self.spot)
393 }
394}
395
396fn outset(rect: Rect, amount: f32) -> Rect {
397 Rect {
398 x: rect.x - amount,
399 y: rect.y - amount,
400 width: rect.width + amount * 2.0,
401 height: rect.height + amount * 2.0,
402 }
403}
404
405fn corner_radii(radii: CornerRadii) -> [f32; 4] {
406 [
407 radii.top_left,
408 radii.top_right,
409 radii.bottom_right,
410 radii.bottom_left,
411 ]
412}
413
414fn pass_color(color: Color, share: f32, caster_alpha: f32) -> Option<Color> {
419 let share_byte = (share * 255.0).floor();
420 let color_byte = (color.a().clamp(0.0, 1.0) * 255.0).round();
421 let byte = (color_byte * share_byte / 255.0 * caster_alpha).floor();
422 (byte > 0.0).then(|| Color(color.r(), color.g(), color.b(), byte / 255.0))
423}
424
425fn caster_occludes(layer: &GraphicsLayer) -> bool {
428 layer.alpha >= 1.0
429 && layer.rotation_x.abs() <= f32::EPSILON
430 && layer.rotation_y.abs() <= f32::EPSILON
431 && layer.rotation_z.abs() <= f32::EPSILON
432}
433
434fn spot_stray(caster: Rect, spot: Rect, caster_radius: f32, spot_radius: f32) -> f32 {
438 if caster_radius <= 0.0 {
439 return (spot.x - caster.x)
440 .abs()
441 .max((spot.y - caster.y).abs())
442 .max((spot.x + spot.width - caster.x - caster.width).abs())
443 .max((spot.y + spot.height - caster.y - caster.height).abs());
444 }
445 let grown = spot_radius - caster_radius;
446 let upper = [spot.x - caster.x + grown, spot.y - caster.y + grown];
447 let lower = [
448 spot.x + spot.width - caster.x - caster.width - grown,
449 spot.y + spot.height - caster.y - caster.height - grown,
450 ];
451 let length = |[x, y]: [f32; 2]| x * x + y * y;
452 length(upper).max(length(lower)).sqrt() + grown
453}
454
455pub fn layer_shadow_geometry(
459 layer: &GraphicsLayer,
460 transformed_bounds: Rect,
461 caster: Option<RoundedCornerShape>,
462 light: ShadowLight,
463) -> LayerShadowGeometry {
464 if layer.shadow_elevation <= 0.0 {
465 return LayerShadowGeometry::default();
466 }
467
468 let scale = layer_uniform_scale(layer).max(MIN_LAYER_SHADOW_SCALE);
469 let height = layer.shadow_elevation * scale;
470 let caster_alpha = layer.alpha.clamp(0.0, 1.0);
471 let radii = caster.map_or([0.0; 4], |shape| corner_radii(shape.radii()));
472 let occludes = caster_occludes(layer);
473 let pixels_per_unit = light.pixels_per_unit.max(f32::EPSILON);
474
475 let reach = (height * 0.5).min(MAX_AMBIENT_REACH_PIXELS / pixels_per_unit);
478 let ambient_outer = outset(transformed_bounds, reach);
479 let ambient = pass_color(
480 layer.ambient_shadow_color,
481 AMBIENT_SHADOW_ALPHA,
482 caster_alpha,
483 )
484 .and_then(|color| {
485 let blur = reach * (1.0 + height * pixels_per_unit / 128.0);
486 let inset = if occludes { reach } else { ambient_outer.width };
487 ShadowRRect::new(ambient_outer, radii.map(|r| r + reach), blur, inset)
488 .map(|shadow| (shadow, color))
489 });
490
491 let depth = light.z - height;
495 let z_ratio = if depth > f32::EPSILON {
496 (height / depth).clamp(0.0, 0.95)
497 } else {
498 0.95
499 };
500 let spot_scale = 1.0 + z_ratio;
501 let spot_blur = light.radius * z_ratio;
502 let projected = Rect {
503 x: transformed_bounds.x + z_ratio * (transformed_bounds.x - light.x),
504 y: transformed_bounds.y + z_ratio * (transformed_bounds.y - light.y),
505 width: transformed_bounds.width * spot_scale,
506 height: transformed_bounds.height * spot_scale,
507 };
508 let spot =
509 pass_color(layer.spot_shadow_color, SPOT_SHADOW_ALPHA, caster_alpha).and_then(|color| {
510 let caster_radius = radii.into_iter().fold(0.0, f32::max);
511 let inset = spot_blur
512 + if occludes {
513 spot_blur.max(spot_stray(
514 transformed_bounds,
515 projected,
516 caster_radius,
517 caster_radius * spot_scale,
518 ))
519 } else {
520 projected.width
521 };
522 ShadowRRect::new(
523 outset(projected, spot_blur),
524 radii.map(|r| r * spot_scale + spot_blur),
525 2.0 * spot_blur,
526 inset,
527 )
528 .map(|shadow| (shadow, color))
529 });
530
531 LayerShadowGeometry { ambient, spot }
532}
533
534#[cfg(test)]
535#[path = "tests/layer_shadow_tests.rs"]
536mod tests;