A module for drawing shadows

I remember that when I used the beamer class to create a presentation for the first time (circa 2003!), one of the most impressive features was shadows under the frames. The simplest idea to draw a shadow is to copy the shape of the object, shift it a bit, and draw it in the background with a grayish color.

Simple shadow

We can improve it a bit, by adding a bit of a Gaussian blur …

Shadow with blur

… and perhaps making the shadow slightly bigger than the object …

Shadow with blur and spread

… and maybe adding a bit of transparency …

Shadow with blur and spread

There are various LaTeX packages that provide different variations of the above idea (starting with fancybox, tikz, pgf-blur, and others.)

A while back, Peter Rolf released a ConTeXt module drops, which drew slightly more realistic shadows by drawing both the umbra (the dark part of the shadow) and the penumbra (the lighter part of the shadow).

Shadow with umbra and penumbra

Unlike many of the other modules, drops module used ImageMagick to draw the shadow, which required some book-keeping in the background, but ensured that the rendering was fast (some of the PDF viewers struggle with multiple layers and transparency, which can be annoying during a presentation).

The drops module also provided an interface to draw shadows behind MetaPost paths, but the interface was a bit clunky.

For a while, I wanted to provide a nicer interface around the drops module (an older attempt is on GitHub, posted in response to a question on TeX.SE). But I never touched the actual calculations (which were done in a 1500-line Lua file).

I recently made another attempt at rewriting the module from scratch, emphasizing a cleaner interface, especially for the MetaPost part. The module is called t-shadow and is available on GitHub.

I wanted the following interface:

  1. We define a shadow on the TeX side:

    \defineexternalshadow[name][key=value, key=value]
    
  2. Use the shadow in MetaPost:

    draw externalshadow[preset="name", path = p];
    
  3. At the same time, I wanted to change the values set on the TeX side in MetaPost:

    draw externalshadow[preset="name", key = value, path = p];
    

This was surprisingly easy, using the ability to call Lua from MetaPost. The MetaPost part of the code is:

\startMPdefinitions
    def externalshadow =
        applyparameters "externalshadow" "do_externalshadow"
    enddef ;
 
    vardef do_externalshadow =
        image (
            pushparameters "externalshadow" ;
            if hasparameter "path" :
                newpath p ; p := getparameterpath "path" ;
                newstring fillcolor ; fillcolor := lua.mp.externalshadow_fillcolor() ;
                draw lua.mp.externalshadow_use(
                    xpart llcorner p, ypart llcorner p,
                    xpart urcorner p, ypart urcorner p
                ) ;
                if fillcolor <> "" :
                    fill p withcolor fillcolor ;
                fi ;
            fi ;
            popparameters ;
        )
    enddef ;
\stopMPdefinitions

And then, we can define a function in lua to do all the actual calculations:

function mp.externalshadow_use(xmin, ymin, xmax, ymax)
    local options = getparameterset("externalshadow")
    resolve_options(options)
    local spec    = options_to_spec(options)
    local path    = options.path
 
    local masks   = path_masks(spec, path, xmin, ymin, xmax, ymax)
    local outfile = render_to_file(spec, masks)
 
    local sp_per_bp  = tex.sp("1bp")
    local hoff, voff = placement_sp(spec.direction, spec.offset)
 
    -- ImageMagick expands the PNG for the blur, so center the loaded figure
    -- in MetaPost and then restore the path's logical bounds.
    local cx   = (xmin + xmax) / 2
    local cy   = (ymin + ymax) / 2
    local w_bp = xmax - xmin
    local h_bp = ymax - ymin
 
    return format(
        [[image (
            newpicture fp ; fp := figure("%s") ;
            draw fp shifted (-center fp) shifted (%fbp, %fbp) ;
            setbounds currentpicture to fullsquare xscaled %fbp yscaled %fbp shifted (%fbp, %fbp) ;
        ) shifted (%fbp, %fbp)]],
        outfile,
        cx, cy,
        w_bp, h_bp, cx, cy,
        hoff/sp_per_bp, -voff/sp_per_bp
    )
end

Here, resolve_options is doing the bookkeeping of inheriting default values from the TeX side; path_masks converts the MetaPost path to an SVG path (which is later passed to ImageMagick), and render_to_file calls ImageMagick to create the shadow.

Here is a full usage example:

\usemodule[shadow]

\setupexternalshadow[directory=.cache]

\defineshadowlayer[mp:umbra][blur=0bp,spread=0bp,transparency=0.4]
\defineshadowlayer[mp:penumbra][blur=2bp,spread=0.5bp,transparency=0.6]

\defineexternalshadow[mp:shadow]
  [
    umbra=mp:umbra,
    penumbra=mp:penumbra,
    direction=-45,
    offset=3bp,
  ]

\startMPdefinitions
    % Arrowhead Modifications for TAOCP. Copied from some webpage of Knuth. 
    % I like these arrows better than the default mp arrows..
    vardef arrowhead expr p =
        save q, e, f, g ; path q ; pair e ; pair f ; pair g ;
        e = point length p of p ;
        q = gobble(p shifted -e cutafter makepath(pencircle scaled 2ahlength))
          cuttings ;
        f = point 0 of (q rotated 0.5ahangle) shifted e ;
        g = point length q of (reverse q rotated -0.5ahangle) shifted e ;
        f .. {dir (angle direction length q of (q rotated 0.5ahangle) - 0.3ahangle)} e
          & e {dir (angle direction 0 of ((reverse q) rotated -0.5ahangle) + 0.3ahangle)} .. g
    enddef ;
 
    ahlength := 5mm ;
\stopMPdefinitions

\startMPpage[offset=1dk]
    path c, e, rawhead, head ;
 
    c := (0cm, -3.2cm)
      .. controls (1.4cm, -1.8cm) and (2.8cm, -4.6cm) ..
         (4.2cm, -3.0cm)
      .. controls (5.5cm, -1.7cm) and (6.7cm, -4.1cm) ..
         (8.0cm, -2.8cm) ;
    
    e := envelope (makepen fullcircle) scaled 1bp of c ;
 
    rawhead := arrowhead c ;
    head := envelope (makepen fullcircle) scaled 1bp of rawhead ;
 
    draw externalshadow [
        path = e,
        preset="mp:shadow",
        fillcolor = "black"
    ] ;
    draw externalshadow [
        path = head,
        preset="mp:shadow",
        fillcolor = "black"
    ] ;
\stopMPpage

which gives

A metapost path with shadow