Box shadow in Makie

Hi!
I am a big fan of de-marking boxes from their background using box shadows. Is there a way to do that in Makie?
Thanks for the help!

After studying a bit how to use mesh and thanks to the help of @asinghvi17 on slack, I manage to get a solution that works!

This was generated using the following code:

Summary
using CairoMakie, Colors

function generate_arc(radius, θ₀, θ₁, n_points=50)
    @assert n_points ≥ 2 "n_points must be ≥ 2"
    θs = range(start=θ₀, stop=θ₁, length=n_points)
    vertices = [[0.0; cos.(θs)...] [0.0;sin.(θs)]]
    faces = [ones(Int, n_points-1) 2:(n_points) 3:(n_points+1)]
    return vertices .* radius, faces
end

function generate_box_shadow(width, height, extent=0.1*(width+height)/2; n_points=5, dark=RGBA(0.0, 0.0, 0.0, 0.5), light=RGBA(0.0, 0.0, 0.0, 0.0))
    bottom_left_vertices, bottom_left_faces = generate_arc(extent, π, 3π/2, n_points)
    bottom_right_vertices, bottom_right_faces = generate_arc(extent, -π/2, 0.0, n_points)
    top_right_vertices, top_right_faces = generate_arc(extent, 0.0, π/2, n_points)
    top_left_vertices, top_left_faces = generate_arc(extent, π/2, π, n_points)
    bottom_right_vertices = bottom_right_vertices + repeat([width 0], n_points+1)
    top_right_vertices = top_right_vertices + repeat([width height], n_points+1)
    top_left_vertices = top_left_vertices + repeat([0 height], n_points+1)
    vertices = [bottom_left_vertices; bottom_right_vertices; top_right_vertices; top_left_vertices]
    face1 = [1 n_points+1 n_points+3]
    face2 = [1 n_points+3 n_points+2]
    faces = [
        bottom_left_faces; 
        face1;
        face2;
        bottom_right_faces .+ (n_points+1);
        face1 .+ (n_points+1);
        face2 .+ (n_points+1);
        top_right_faces .+ 2(n_points+1);
        face1 .+ 2(n_points+1);
        face2 .+ 2(n_points+1);
        top_left_faces .+ 3(n_points+1);
        [3(n_points+1)+1 4(n_points+1) 2];
        [3(n_points+1)+1 2 1];
    ]
    colors = repeat([dark; fill(light, n_points);], 4)
    return vertices, faces, colors
end

function shadow(obj; extent=5, dark=RGBA(0.0, 0.0, 0.0, 0.5), padding=(5,5,5,5), meshkw=(;))
    padding_left, padding_right, padding_bottom, padding_top = padding
    inner_box = map(viewport(obj), obj.layoutobservables.protrusions) do v, protrusions
        x,y = v.origin
        w,h = v.widths
        left = protrusions.left
        right = protrusions.right
        top = protrusions.top
        bottom = protrusions.bottom
        Rect2{Int}(
            (x - left - padding_left, y - bottom - padding_bottom), 
            (
                w +  left + right + padding_left + padding_right , 
                h +  bottom + top + padding_bottom + padding_top
                )
        )
    end
    outer_box = map(inner_box) do v
        x,y = v.origin
        w,h = v.widths
        Rect2{Int}(
            (x - extent, y - extent), 
            ( w +  2extent, h +  2extent)
        )
    end
    mesh = map(inner_box) do v
        widths = v.widths
        vertices, faces, colors = generate_box_shadow(widths[1], widths[2], extent; n_points=5, dark)
        vertices = vertices .+ repeat([extent extent], size(vertices, 1))
        (;vertices, faces, colors)
    end
    vertices = map(mesh) do m
        m.vertices
    end
    faces = map(mesh) do m
        m.faces
    end
    colors = map(mesh) do m
        m.colors
    end
    t = obj.scene
    while !isnothing(t.parent)
        t = t.parent
    end
    Box(t, color=:white, bbox=inner_box, strokevisible=false)
    inset = LScene(t,
        scenekw = (camera = campixel!,),
        bbox=outer_box,
    )
    mesh!(inset, vertices, faces; color = colors, meshkw...)
    translate!(inset.scene, 0, 0, 100)
    inset
end

let
    fig, ax, p = lines(-3π..3π, sin)
    ax_inset = Axis(fig[1, 1],
        width=Relative(0.2),
        height=Relative(0.2),
        halign=0.15,
        valign=0.9,
        spinewidth=0.2,
    )
    translate!(ax_inset.blockscene, 0, 0, 150)
    lines!(ax_inset, -3π..3π, cos)
    s = shadow(ax_inset, extent=5, padding=(5, 10, 5, 5), meshkw=(rasterize=4,), dark=RGBA(0.1, 0.1, 0.1, 0.25))
    display(fig)
end
2 Likes

The problem is that meshes in CairoMakie are drawn triangle after triangle so you get ugly seams when you have transparency involved, as the pixels get more saturated there.

Yep, I noticed… I guess I could render the shadow in GLMakie then display it as an image, or use GLMakie for this specific figure.

Edit: or by accepting to become a criminal and offsetting the position of the nodes by \varepsilon to avoid seams

This looks awesome.
Are you planning to package this up to make it more accessible?

I can make PR to Makie’s repo, but would it be accepted despite the mesh artifact in CairoMakie? Which parameters would be interesting to keep / to add?