7 minute read
Making noisy SVGs
Adding noise texture with only code
One of my ongoing fixations with the web is how improvements in technology inform web design. In an earlier post this year I wrote about my theory that the increasing pixel density of displays galvanized the shift from photography to vector illustrations in the early to mid 2010s.
In recent years there has been a design trend that runs counter to this. Illustrations still rule in web design, but instead of clean, flat shapes there has been an emergence of texture, usually as part of a design’s lighting or shading and usually a “noisy” or grainy texture.
Studio Vellekoop & LeónI’m not hip enough to know if this style has been assigned a pithy label, but I do enjoy it. At the same time, I find it frustrating — because as far as I can tell (the lack of examples I have seen in the wild supports this theory) there isn’t an easy way to replicate these illustrations with SVGs. They can probably be exported as such from illustration programs but it’s likely these applied textures are raster or if they are vector, quite large in size.
SVGs are an interesting subject to me, because the specification for them is dense and there are usually multiple ways to accomplish an effect. Most SVGs are simply exported directly from a graphics program and have a lot of inefficiencies etc… which has led to the emergence of tools like SVGO.
I’m not an expert on the subject (I feel like the older I get, the less I feel like I can claim that about anything) but in recent years have taken to hand tuning and occasionally writing my own SVGs from scratch so I am not a complete novice either.
I decided to give creating an SVG using this illustration style a shot. My goal was to use the regular SVG specification to write a simple illustration that had this noisy, textured shadow effect. An additional goal was for this illustration to be moderately flexible, something that could easily be turned into a component in Svelte or React and changed for different shapes and colors. For more direct inspiration, I looked to the designer Josh Warren who has experimented a lot with using this style and simple geometry to nice effect:
Josh WarrenBasic shape and gradient
Drawing a circle or square in an SVG is really easy. There are primitives for them.
<svg viewbox="0 0 100 100">
<circle cx="50" cy="50" r="50" />
</svg>
The tricky part is everything else. If you want to apply a gradient, you can specify that as the fill
- but what if you want a gradient that changes with the base color you specify? So you don’t have to define a custom gradient with specific colors if you want to reuse the component. Then you’ve entered mask territory. The way masks work in SVGs is that you use the “colors” black and white to how you want a mask to work. It’s a little confusing, but I’ve tried to keep the code example simple.
(As an aside, I have to give huge kudos to MDN’s excellent documentation, this project could not have been done without it!)
This can actually be optimized a little more. We know we’re going to use the same circle shape multiple times, so we can define it once in <defs>
, assign it an id and simply reference it with the use
element. This makes the code a little more DRY:
<svg viewbox="0 0 100 100">
<defs>
<circle id="shape" cx="50" cy="50" r="50" />
<mask id="gradient">
<linearGradient id="fade">
<stop offset="0%" stop-color="black" stop-opacity="0.5" />
<stop offset="75%" stop-color="white" stop-opacity="1" />
</linearGradient>
<use href="#shape" fill="url('#fade')" />
</mask>
</defs>
<use href="#shape" mask="url(#gradient)" />
</svg>
So we’ve got the shape and a gradient going, but how do we add a texture to that gradient? The answer is with a filter. MDN’s documentation on SVG Filters goes into better detail on the nuts and bolts of how filters work. I also found this excellent article that goes into great detail about creating textures for SVGs.
To create noise, I used the <feTurbulence>
filter which is explicitly for generating artificial textures but required quite a bit of fiddling to get to my liking. Then, I had to use other filter effects to eliminate color variance and blend naturally with the fill color selected, and finally apply the filter to the circle.
Result
<svg viewbox="0 0 100 100">
<defs>
<circle id="shape" cx="50" cy="50" r="50" />
<filter id="noise">
<feTurbulence
type="fractalNoise"
baseFrequency="19.5"
numOctaves="10"
result="turbulence"
/>
<feComposite operator="in" in="turbulence" in2="SourceAlpha" result="composite"/>
<feColorMatrix in="composite" type="luminanceToAlpha" />
<feBlend in="SourceGraphic" in2="composite" mode="color-burn" />
</filter>
<mask id="gradient">
<linearGradient id="fade">
<stop offset="0%" stop-color="black" stop-opacity="0.6" />
<stop offset="65%" stop-color="white" stop-opacity="0.9" />
<stop offset="75%" stop-color="white" stop-opacity="1" />
</linearGradient>
<use href="#shape" fill="url('#fade')" />
</mask>
</defs>
<use href="#shape" fill="hsl(337, 92%, 69%)" mask="url(#gradient)" filter="url('#noise')" />
</svg>
Adding a nice fill color and making the gradient a little more subtle and there we go!
With some additional playing around I was able to create an illustration I was quite happy with. It’s not perfect, and notably renders differently in Safari than other browsers but it achieves what I set out to accomplish.
You can also check out the code in the repository.
This method could certainly be improved with additional experimenting. It’s still a far cry from the complexity of some of the illustrations I linked, but I think it could potentially be refined to be usable in more situations.
Addendum Dec 7, 2023
I submitted this post to Hacker News where it received some reception, and was pointed out that CSS Tricks posted a great article written by Jimmy Chion about this subject in 2021. It’s a great article, and the techniques we arrive at are similar.
I was not aware of this post at the time of this writing but am now linking to it here for supplementary reading.