mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-27 11:56:50 -06:00
This commit is contained in:
@@ -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:?}");
|
||||
}
|
||||
+66
-25
@@ -423,16 +423,18 @@ pub fn draw_node(ctx: &Ctx, pix: &mut Pixmap, id: NodeId, parent_xf: Affine, dep
|
||||
let shape = node_outline(ctx.doc, n);
|
||||
// Background blur and glass: the whole backdrop under the layer (every surface below this
|
||||
// one, composited), through the shape, under the content.
|
||||
if backdrop::has_effects(n)
|
||||
&& let Some(sh) = &backdrop::outline(ctx.doc, n)
|
||||
{
|
||||
let pad = backdrop::pad(n, ctx.scale);
|
||||
let p = i32::try_from(pad).unwrap_or(0);
|
||||
if let Some(back) = backdrop::backdrop(pix, ctx.under, lx.saturating_sub(p), ly.saturating_sub(p), lw + pad * 2, lh + pad * 2)
|
||||
&& let Some(b) = backdrop::effects(n, sh, &back, pad, lw, lh, lxf, ctx.scale)
|
||||
{
|
||||
layer.draw_pixmap(0, 0, b.as_ref(), &PixmapPaint::default(), Transform::identity(), None);
|
||||
}
|
||||
let back_shape = if backdrop::has_effects(n) { backdrop::outline(ctx.doc, n) } else { None };
|
||||
let read_backdrop = |pix: &Pixmap| back_shape.as_ref().and_then(|sh| backdrop_layer(ctx, pix, n, sh, (lx, ly), (lw, lh), lxf));
|
||||
// With drop shadows too, the layer casts them from what it paints, not from the backdrop, and
|
||||
// the backdrop is read again once they lie under it (a shadow showing behind is seen through
|
||||
// it). Until then, the layer's content sees the backdrop read first under it (#255).
|
||||
let casts = back_shape.is_some() && n.effects.iter().any(|e| e.visible && matches!(e.kind, EffectKind::DropShadow(_)));
|
||||
// Kept apart, the first read is only what backdrop effects inside the content see: without
|
||||
// any, the backdrop (blur or glass, the costly part) is computed once, after the shadows.
|
||||
let nested = || ctx.doc.subtree(id).iter().skip(1).any(|c| ctx.doc.node(*c).is_some_and(backdrop::has_effects));
|
||||
let first_back = if !casts || nested() { read_backdrop(pix) } else { None };
|
||||
if !casts && let Some(b) = &first_back {
|
||||
layer.draw_pixmap(0, 0, b.as_ref(), &PixmapPaint::default(), Transform::identity(), None);
|
||||
}
|
||||
let op = n.opacity.clamp(0.0, 1.0);
|
||||
{
|
||||
@@ -448,9 +450,12 @@ pub fn draw_node(ctx: &Ctx, pix: &mut Pixmap, id: NodeId, parent_xf: Affine, dep
|
||||
}
|
||||
};
|
||||
let has_inner = shape.is_some() && n.effects.iter().any(|e| e.visible && matches!(e.kind, EffectKind::InnerShadow(_)));
|
||||
// Backdrops further down see this layer composited onto `pix`.
|
||||
// Backdrops further down see this layer composited onto `pix` (over its backdrop, when
|
||||
// that is kept apart for now).
|
||||
let under = Under { opacity: op, blend: n.blend, ..Under::new(pix, (lx, ly), ctx.under) };
|
||||
draw_content(&Ctx { under: Some(&under), ..*ctx }, &mut layer, n, lxf, depth, has_inner.then_some(&inner as &dyn Fn(&mut Pixmap)));
|
||||
let over_back = first_back.as_ref().filter(|_| casts).map(|b| Under::new(b, (0, 0), Some(&under)));
|
||||
let content_under = over_back.as_ref().unwrap_or(&under);
|
||||
draw_content(&Ctx { under: Some(content_under), ..*ctx }, &mut layer, n, lxf, depth, has_inner.then_some(&inner as &dyn Fn(&mut Pixmap)));
|
||||
}
|
||||
// A group is no outline of its own: its inner shadows are cast from around what its layers
|
||||
// draw together, over them.
|
||||
@@ -465,24 +470,60 @@ pub fn draw_node(ctx: &Ctx, pix: &mut Pixmap, id: NodeId, parent_xf: Affine, dep
|
||||
}
|
||||
}
|
||||
// Layer blur, noise and texture, in list order, over everything else.
|
||||
if !casts {
|
||||
layer_effects(ctx, &mut layer, n, id, lxf);
|
||||
}
|
||||
// Drop shadows go under the layer, in effect-list order.
|
||||
{
|
||||
// Kept apart, the backdrop isn't in the layer yet: the shadows are cast from the content
|
||||
// with the layer effects it will get (a copy only when it gets some).
|
||||
let layer_fx =
|
||||
n.effects.iter().any(|e| e.visible && matches!(e.kind, EffectKind::LayerBlur(_) | EffectKind::Noise(_) | EffectKind::Texture(_)));
|
||||
let content = (casts && layer_fx).then(|| {
|
||||
let mut c = layer.clone();
|
||||
layer_effects(ctx, &mut c, n, id, lxf);
|
||||
c
|
||||
});
|
||||
let caster = shadow::Caster::new(ctx.doc, n, content.as_ref().unwrap_or(&layer), lxf, ctx.scale);
|
||||
for e in n.effects.iter().filter(|e| e.visible) {
|
||||
if let EffectKind::DropShadow(s) = &e.kind
|
||||
&& let Some(sh) = shadow::drop_shadow(&caster, s)
|
||||
{
|
||||
composite(pix, (lx, ly), &sh, op, s.blend);
|
||||
}
|
||||
}
|
||||
}
|
||||
if casts {
|
||||
// The backdrop with the shadows under it, the content over it, then the layer effects.
|
||||
if let Some(mut b) = read_backdrop(pix) {
|
||||
b.draw_pixmap(0, 0, layer.as_ref(), &PixmapPaint::default(), Transform::identity(), None);
|
||||
layer = b;
|
||||
}
|
||||
layer_effects(ctx, &mut layer, n, id, lxf);
|
||||
}
|
||||
composite(pix, (lx, ly), &layer, op, n.blend);
|
||||
}
|
||||
|
||||
/// Node `n`'s backdrop effects through its backdrop outline `sh`, for its `size` layer at `at` on
|
||||
/// `pix` (local → layer pixels through `lxf`): what `pix` holds there now, over what lies under it.
|
||||
fn backdrop_layer(ctx: &Ctx, pix: &Pixmap, n: &Node, sh: &shadow::Outline, at: (i32, i32), size: (u32, u32), lxf: Affine) -> Option<Pixmap> {
|
||||
let ((lx, ly), (lw, lh)) = (at, size);
|
||||
let pad = backdrop::pad(n, ctx.scale);
|
||||
let p = i32::try_from(pad).unwrap_or(0);
|
||||
let back = backdrop::backdrop(pix, ctx.under, lx.saturating_sub(p), ly.saturating_sub(p), lw + pad * 2, lh + pad * 2)?;
|
||||
backdrop::effects(n, sh, &back, pad, lw, lh, lxf, ctx.scale)
|
||||
}
|
||||
|
||||
/// Node `n`'s layer blur, noise and texture on its `layer`, in list order.
|
||||
fn layer_effects(ctx: &Ctx, layer: &mut Pixmap, n: &Node, id: NodeId, lxf: Affine) {
|
||||
for e in n.effects.iter().filter(|e| e.visible) {
|
||||
match &e.kind {
|
||||
EffectKind::LayerBlur(b) => blur::apply(&mut layer, b, (n.width, n.height), lxf, ctx.scale),
|
||||
EffectKind::Noise(nz) => grain::noise(&mut layer, nz, lxf, id.0 as u32),
|
||||
EffectKind::Texture(t) => grain::texture(&mut layer, t, lxf, ctx.scale, id.0 as u32),
|
||||
EffectKind::LayerBlur(b) => blur::apply(layer, b, (n.width, n.height), lxf, ctx.scale),
|
||||
EffectKind::Noise(nz) => grain::noise(layer, nz, lxf, id.0 as u32),
|
||||
EffectKind::Texture(t) => grain::texture(layer, t, lxf, ctx.scale, id.0 as u32),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Drop shadows go under the layer, in effect-list order.
|
||||
let caster = shadow::Caster::new(ctx.doc, n, &layer, lxf, ctx.scale);
|
||||
for e in n.effects.iter().filter(|e| e.visible) {
|
||||
if let EffectKind::DropShadow(s) = &e.kind
|
||||
&& let Some(sh) = shadow::drop_shadow(&caster, s)
|
||||
{
|
||||
composite(pix, (lx, ly), &sh, op, s.blend);
|
||||
}
|
||||
}
|
||||
composite(pix, (lx, ly), &layer, op, n.blend);
|
||||
}
|
||||
|
||||
fn draw_outline(ctx: &Ctx, pix: &mut Pixmap, n: &Node, xf: Affine, depth: usize) {
|
||||
|
||||
@@ -425,6 +425,8 @@ pub fn png_insert_chunk(png: Vec<u8>, kind: &[u8; 4], data: &[u8]) -> Vec<u8> {
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod backdrop_shadow_tests;
|
||||
#[cfg(test)]
|
||||
mod backdrop_tests;
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user