Painting SVG Paths with Masks

Featuring Tom Miller

SVG graphics have an incredible amount of benefits over their raster counterparts. They’re tiny files, scale infinitely, can be animated and edited with code, and a whole lot more.

However, you can’t get the same textured feel that raster graphics can provide.

When we combine the strengths of vector and raster, we can create some charming effects. That’s exactly what Tom Miller did for his Silkscreen Squiggles CodePen.

Click anywhere on the CodePen to run it again.

In short:

  • 73 paths that get a thin stroke drawn, each with a random color.
  • 73 identical paths get a thick stroke drawn with random colors.
  • Both groups of paths are masked by the same raster image.

Let’s dig into the details!

Raster Mask

The key here is using a great mask with an alpha layer. Here’s the PNG Tom is using, set on a dark background.

Painted squiggles in white

You could achieve the basic shapes with just SVG paths, but it’s the paintbrush texture that makes this mask great.

Both groups have image as the mask, so they apply the alpha (transparency) in the same way you see above.

Here’s the pen with the mask removed.

So we see the difference a good raster masks makes, now let’s get into how Tom is drawing these paths!

Drawing with JavaScript

Beyond the elegant mask, most of the magic happens in just a few lines of JavaScript.

Let’s take a look and break it down.

var colors = ["#80475e", "#cc5a71", "#c89b7b", "#f0f757"];

window.onload =
  window.onclick =
  window.ontouchend =
    () => {
          defaults: {
            duration: 1,
            overwrite: true,
            attr: {
              stroke: () => colors[gsap.utils.random(0, colors.length - 1, 1)],
          "#g1 path",
          { drawSVG: () => (Math.random > 0.5 ? "100% 100%" : 0) },
          { drawSVG: "0% 100%", stagger: { amount: 5, from: "random" } },
          "#g2 path",
          { drawSVG: () => (Math.random > 0.5 ? "100% 100%" : 0) },
          { drawSVG: "0% 100%", stagger: { amount: 4, from: "random" } },

The first line sets the color palette.

Then the next line is a terse way to set the same anonymous function on three different events. onload makes it animate after the DOM is ready, onclick and ontouchend handle user inputs.

Now let’s dig into the GSAP.

GSAP Animations

First he creates a GSAP timeline and cuts back on duplicating code by setting some defaults.

  defaults: {
    duration: 1, // 1 second
    overwrite: true,
    attr: {
      stroke: () => colors[gsap.utils.random(0, colors.length - 1, 1)],


GSAP’s overwrite setting is interesting, and the GSAP docs explain it well.

Overwriting refers to how GSAP handles conflicts between multiple tweens on the same properties of the same targets at the same time.

GSAP’s 3 Overwrite Modes

false (default): No overwriting occurs and multiple tweens can try to animate the same properties of the same target at the same time. One way to think of it is that the tweens remain “fighting each other” until one ends.

true: Any existing tweens that are animating the same target (regardless of which properties are being animated) will be killed immediately.

"auto": Only the conflicting parts of an existing tween will be killed. If tween1 animates the x and rotation properties of a target and then tween2 starts animating only the x property of the same targets and overwrite: "auto" is set on the second tween, then the rotation part of tween1 will remain but the x part of it will be killed.

GSAP Conflict Docs

Usually you don’t need to change the default, but here Tom chooses true which makes sure everything gets wiped out if new tweens are created.

In this version I removed overwrite: true. Try clicking 5 times in quick succession and see why it’s needed.

The new tweens don’t overwrite the old ones, so it’s chaos.

Stroke color

Tom is setting a default attribute for stroke as a function instead of a value.

attr: {
    stroke: () => colors[gsap.utils.random(0, colors.length - 1, 1)],

This function will get called on every path as it gets animated, returning a random color each time.


Now let’s take a look at the fromTo() tween.

  "#g1 path",
        { drawSVG: () => (Math.random > 0.5 ? "100% 100%" : 0) },
        { drawSVG: "0% 100%", stagger: { amount: 5, from: "random" } },

Because there are 73 paths that fulfill the "#g1 path" query, GSAP will be animating an array of all those elements.

The first object is the state we’re animating from. The drawSVG property sets how much of the path is filled, with a beginning and end value. A fully drawn path is "0%, 100%".

By giving a value of 0, we set the start and stop both to the very beginning. With 100%, 100% we also get no path drawn, because the start and stop are at the same place.

The difference is we’re now at the end of the line, rather than the beginning.


Staggering a group of animations means they won’t happen at the same time, but one after another.

  stagger: { amount: 5, from: "random" }

The amount property is the total amount of seconds that get split among the staggers. This animation will take 5 seconds to start all of the tweens.

GSAP has a great feature to set which position in the array the animations will begin. Our options are "start", "center", "edges", "random", or "end".

It’s important to note that random doesn’t choose a random position to begin but instead randomizes the entire array and order of the animations.

Second group of paths

There’s not much different about the second group of paths. They’re the exact same path values, just given a larger stroke-width and start about 3 seconds into the animation.

Mostly they throw another coat of paint onto the piece and fill in any gaps the thinner paths might have left.

Wrap Up

We touched on some great GSAP techniques, plus took a closer look at the mask technique Tom used.

I love the combination of vector and raster graphics, and hope you see how much a single PNG can add to an SVG animation.

If you enjoyed this, check out when Tom taught us some great scroll animations on stream, or check out the rest of his CodePen.