Stroke of chance
With SVG filters you update the appearance of other elements. Through a set of primitives, a sequence of inputs and outputs you can introduce noise functions, lighting effects, and much, much more.
The topic is vast, and opens the door to fantastic changes. But also, practical touches.
Consider an outline, tightly wrapped around the letters of a bright <text> element.
<text filter="url(#outline)" fill="hsl(0 0% 99%)">
<!-- ... -->
</text> A sequence
As prefaced, you build a filter with primitives, elements nested in a common <filter> element.
<filter id="outline">
<!-- ... -->
</filter> Each primitive has a function, a way to modify the input graphic. To create an outline, for instance, you need to first expand the shapes beyond their original size. feMorphology, with the operator attribute set to dilate helps you to achieve just that.
<feMorphology operator="dilate" radius="1" /> The radius attribute argues for the size of the expansion. But without diving into the often puzzling world of filter units, know that the value is not relative to the size of the text. In this manner, the growth is uniform, across the entirety of the wider-than-taller figure.
We care solely of the excess, the portions stretching past the input string and for this, we have access to feComposition. The primitive takes two inputs with the in and in2 attributes. On top of these values, it considers an operator, which details the type of composition. The value of out, in this instance, suits our needs best.
<feComposite in2="SourceGraphic" operator="out" /> In goes the result of the previous primitive, the implicit value assumed by the in attribute. In goes the source graphic, explicitly mentioned through the value of SourceGraphic. With the chosen operator, the function takes the parts of the first image which do not overlap with the second. You end up carving a stroke, and you are almost done.
Not quite done however. You do have a stroke, but a stroke only, with the color of the input graphic as well.
For the color, the growth needs enough contrast to stand out. You certainly wouldn’t want to find a bright blob. Again.
And you can trust another primitive to set the base.
<feFlood flood-color="hsl(0 0% 25%)" /> feFlood, with a given flood-color, looks deceptively simple. In its effect, then, it is equally straightforward.
The primitive works to paint with the color over the entirety of the rectangular area containing the input text. On its own, the element does little but obscure your view, but as part another element, the function is exactly what you need.
Remember feComposition? With the out operator we picked the parts of one input exceeding those of the other. With the in operator we can achieve the opposite, and consider the overlapping portions only. We do need to be more explicit, however.
For one input we can point to the output of the previous primitive, feFlood, but for the partner value we need a reference, a way to single out the stroke.
Promptly, you can name the output of every primitive with the result attribute.
<feComposite result="stroke" in2="SourceGraphic" operator="out" />
<feFlood result="color" flood-color="hsl(0 0% 25%)" /> Name the result of the carving operation as stroke. For good measure, mark the result of the flooding function as color and you have them, the necessary ingredients for the new element.
<feComposite in="color" in2="stroke" operator="in" /> In goes the color-flooded rectangle, in goes the stroke. The result, a preciously tinted outline.
There’s just one thing missing, the input text. Indeed, the sequence of primitives builds a colored outline, but it also replaces the text with the output image. To bring the letters back, in their unfiltered version, you need the contribution of one last element, feMerge.
<feMerge>
<feMergeNode />
<feMergeNode in="SourceGraphic" />
</feMerge> In the body of the element, and with instances of <feMergeNode>, you point to individual layers. Here the result of the last primitive, the colored stroke, and then SourceGraphic itself. feMerge adds the two in sequence, finalizing the effect.
There is a slight problem with the visual. If you pay close attention to the letters, especially the more rotund characters, you might notice that the outline is irregular, at times choppy. Unfortunately, this is a side effect of feMorphology, of the very first primitive. The reason is quite technical, but in a few words, the expansion works on pixel values. The filter considers the individual squares to decide whether or not to color the surrounding pixels. Painting with squares, the stroke cannot help but be rough around the edges.
Was this work all for nought? Certainly not. Immediately, the sequence helps you practice with filters and the logic of the nested primitives. On top of this, you know how to add an outline around text, and the operation is possible on other shapes as well, be it a circle, a rectangle, or a complex figure drawn with a <path> element. Moreover, you are not limited to SVG elements. You can very well apply the filter to HTML elements as well.
On HTML elements and even on longer sentences, daring to break in multiple rows.
We achieved a lot even with the imperfect result. That being said, you have the option of keeping everything in the body of the <svg> element, there are alternatives, even simple ones.
An alternative
Let’s go back to the <text> node with a given color.
<text fill="hsl(0 0% 99%)">
<!-- ... -->
</text> Past the fill, you can set a color for the outline with the stroke attribute.
<text fill="hsl(0 0% 99%)" stroke="hsl(0 0% 25%)">
<!-- ... -->
</text> There’s only one hiccup with the code, due to the stroke’s own nature. As the stroke is drawn above the fill, and midway through, the outline overwhelms the shapes below. To the rescue, the unsung hero that is the paint-order attribute.
<text paint-order="stroke" fill="hsl(0 0% 99%)" stroke="hsl(0 0% 25%)" stroke-width="2">
<!-- ... -->
</text> With its value you dictate the order of operations. Add stroke and the stroke is painted before the solid fill which follows. Add stroke and the effect is complete.
Use a filter and you find a first, reasonable solution. Stick to SVG elements and the stroke attribute might be all that you need. You can still have a filter pitch in though. Once you understand the sequence, you can set up many more features. Delicate or exaggerated as they might be.