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.
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 =
() => {
gsap
.timeline({
defaults: {
duration: 1,
overwrite: true,
attr: {
stroke: () => colors[gsap.utils.random(0, colors.length - 1, 1)],
},
},
})
.fromTo(
"#g1 path",
{ drawSVG: () => (Math.random > 0.5 ? "100% 100%" : 0) },
{ drawSVG: "0% 100%", stagger: { amount: 5, from: "random" } },
0,
)
.fromTo(
"#g2 path",
{ drawSVG: () => (Math.random > 0.5 ? "100% 100%" : 0) },
{ drawSVG: "0% 100%", stagger: { amount: 4, from: "random" } },
"-=3",
);
};
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.
gsap.timeline({
defaults: {
duration: 1, // 1 second
overwrite: true,
attr: {
stroke: () => colors[gsap.utils.random(0, colors.length - 1, 1)],
},
},
});
Overwrite
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.
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.
DrawSVG
Now let’s take a look at the fromTo()
tween.
.fromTo(
"#g1 path",
{ drawSVG: () => (Math.random > 0.5 ? "100% 100%" : 0) },
{ drawSVG: "0% 100%", stagger: { amount: 5, from: "random" } },
0
)
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.
Stagger
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.