M1: drop shadows under a background blur (#255)

A layer with a background blur (or glass) drew its blurred backdrop into
its own pixels before casting its drop shadows from them, so a translucent
fill cast a shadow as dark as an opaque one, and with "show behind
transparent areas" the shadow was cast from the backdrop's pixels. The
backdrop was also read before the shadows were drawn, so a shadow showing
behind was never seen through the blur.

When a layer has both, it now casts its shadows from what it paints (with
the layer blur, noise and texture it will get), draws them, and only then
reads the backdrop, which goes under its content; the content still sees
the first backdrop under it while it's drawn. Layers without both draw as
before.
This commit is contained in:
Barzabel
2026-10-14 18:45:15 +03:00
parent 833f0d2c27
commit 698e249fbf
3 changed files with 142 additions and 25 deletions
@@ -0,0 +1,79 @@
//! A drop shadow under a layer with a background blur (#255): the shadow is cast by what the layer
//! paints (a translucent fill casts a faint one), not by the blurred backdrop drawn through its
//! shape, and the blur reads what lies under the layer with its own shadow there, so a shadow
//! that shows behind transparent areas is seen, blurred, through the layer.
use figcraft_color::{Color, Paint};
use figcraft_doc::{Blur, Document, Effect, EffectKind, NodeId, NodeKind, Shadow};
use super::*;
/// A white 240 × 200 frame with blue and green stripes and, on top, a pane at (60, 40) 120 × 80
/// filled 20% black, with a hard black drop shadow 40 down (below it and under its lower half)
/// and, when `blur`, a background blur.
fn scene(show_behind: bool, blur: bool) -> (Document, NodeId) {
let mut d = Document::new("shadow behind blur");
let p = d.first_page().unwrap();
let f = d.create(p, NodeKind::Frame, "Frame", 0.0, 0.0, 240.0, 200.0).unwrap();
d.get_mut(f).unwrap().fills = vec![Paint::solid(Color::from_hex("FFFFFF").unwrap())];
for (i, x) in [20.0, 80.0, 140.0, 200.0].into_iter().enumerate() {
let s = d.create(f, NodeKind::Rectangle, "Stripe", x, 0.0, 20.0, 200.0).unwrap();
d.get_mut(s).unwrap().fills = vec![Paint::solid(Color::from_hex(if i % 2 == 0 { "2040E0" } else { "20C060" }).unwrap())];
}
let pane = d.create(f, NodeKind::Rectangle, "Pane", 60.0, 40.0, 120.0, 80.0).unwrap();
let n = d.get_mut(pane).unwrap();
n.fills = vec![Paint::solid(Color::new(0.0, 0.0, 0.0, 0.2))];
let shadow = Shadow { color: Color::new(0.0, 0.0, 0.0, 1.0), offset: (0.0, 40.0), radius: 0.0, show_behind, ..Shadow::default() };
n.effects = vec![Effect { kind: EffectKind::DropShadow(shadow), visible: true }];
if blur {
n.effects.push(Effect { kind: EffectKind::BackgroundBlur(Blur::uniform(8.0)), visible: true });
}
(d, f)
}
fn px(p: &Pixmap, x: u32, y: u32) -> [u8; 4] {
let i = ((y * p.width() + x) * 4) as usize;
[p.data()[i], p.data()[i + 1], p.data()[i + 2], p.data()[i + 3]]
}
/// The largest channel difference between `a` and `b` over the shadow below the pane
/// (60..180 × 122..160).
fn max_diff_below_pane(a: &Pixmap, b: &Pixmap) -> u8 {
let mut worst = 0;
for y in 122..160 {
for x in 60..180 {
worst = worst.max(px(a, x, y).iter().zip(px(b, x, y)).map(|(c, d)| c.abs_diff(d)).max().unwrap());
}
}
worst
}
#[test]
fn a_background_blur_doesnt_change_the_shadow_its_layer_casts() {
for show_behind in [false, true] {
let (with_blur, f) = scene(show_behind, true);
let (without, _) = scene(show_behind, false);
let got = render_node(&with_blur, f, 1.0).unwrap();
let want = render_node(&without, f, 1.0).unwrap();
let diff = max_diff_below_pane(&got, &want);
assert!(diff <= 2, "show behind {show_behind}: the shadow below the pane differs by {diff}");
// A 20% fill casts a faint shadow: the white between the stripes stays light.
let light = px(&got, 120, 140);
assert!(light[0] > 180, "show behind {show_behind}: {light:?}");
}
}
#[test]
fn a_shadow_showing_behind_is_seen_through_the_background_blur() {
let (d, f) = scene(true, true);
let got = render_node(&d, f, 1.0).unwrap();
// Between the stripes, the pane's top (no shadow under it) and its lower half (its own shadow
// under it, from y = 80).
let (top, low) = (px(&got, 120, 50), px(&got, 120, 110));
assert!(top[0] > low[0] + 20, "the shadow under the pane doesn't show through: top {top:?}, lower half {low:?}");
// Hidden behind the layer, it doesn't.
let (d, f) = scene(false, true);
let got = render_node(&d, f, 1.0).unwrap();
let (top, low) = (px(&got, 120, 50), px(&got, 120, 110));
assert!(top[0].abs_diff(low[0]) <= 2, "a hidden shadow shows: top {top:?}, lower half {low:?}");
}