I have a long time project: a basic vector graphic tool which runs in browser and uses SVG and Javascript (maybe you have seen somekind of these elsewhere). The tool has only very limited set of functions, because the audience is restricted and the purpose is very specific and in fact there are not allowed to be other functionality than what is explicitly allowed (you know). One missed feature is eroding (also known as inset or thin) and dilating (outset, thicken, bolden) polygons and other graphical elements.
I have used Adobe Illustrator's Offset Path Effect many times and with it I can easily make copies of graphical objects that are thinned or thickened, without affecting the original object, which therefore can be nearly whatever supported by the program.
I have tried to get the same functionality to function in SVG, but without success.
I have tried the following:
- dilate and erode filters, but with not satisfying results (please see the image here)
- Server-side Python's Shapely library, but this workaround is too slow and allows to inset or outset only the basic polygons (description here)
- to find javascript library / code / function, which could alter the path data of graphical elements, but found nothing for javascript
So is there any meaningful way to implement this like Offset Path Effect and how?
This is an "Answer your own question – share your knowledge, Q&A-style" -style answer, but if you have some better answer, please freely use your keyboard.
I have used SO only a few days, so please don't downvote me to the gap. I got an interesting workaround idea to this issue, which is based on variable-width strokes and masks.
But let's start at your (or my) first idea. When we are going to erode (thin) graphical objects in SVG, the obvious first thought is to use erode filter:
But because erode filter (and dilate as well) uses pixel data (the rasterized path) the result is not good looking in all cases. In fact I have never seen a good-looking erode when used to filter vector objects. See the hat and mouth:
The dilate filter has similar problems (the nose is not nice and the baseball cap is scrappy and some other inconsistencies):
All users of Adobe Illustrator know the nice path effects, which can be used to apply various path operations to shapes (objects). These effects doesn't change the original path data, they only create a modified copy of object. One of the most usable is Offset Path Effect, which can be used to set off from the selected object by a specified distance (or something similar). SVG:s erode and dilate filters have similarities with Illustrator's Offset Path Effect, but the quality is as a vector operation (versus bitmap) high.
SVG format in it's current state, lacks support for Illustrator-like Offset Path, but it's possible to get the same functionality using variable-width strokes and masks as noted here.
Let's dive into the world of SVG masks. The dilate (or outset path or thicken) is possible to achieve by simply increasing a stroke width, but erode (or inset path or thinning) needs something more, for example masks. In SVG, any graphics object or 'g' element can be used as an alpha mask for compositing the current object into the background (W3C SVG 1.1 Recommendation).
The above means that not only object's fill can be used as a mask, but also a stroke. And adjusting the width of the stroke of the path that is used as a mask, we can control how much of the current object (into which the mask is applied using mask attribute) is masked out.
Let's get an example of using mask. First we define a path in SVG:s defs element:
<defs>
<path id="head_path" d="M133.833,139.777c1 ...clip... 139.777z"/>
</defs>
When we define a path in defs element, it eliminates the need for repeating the same data in other parts of document. The path's id attribute is used to refer to the path from some point(s) of the document.
Now we can use this path data in mask:
<defs>
...
<mask id="myMask" maskUnits="userSpaceOnUse">
<use xlink:href="#head_path" fill="#FFFFFF" stroke="#000000"
stroke-width="18" stroke-linecap="round" stroke-linejoin="round"/>
</mask>
...
</defs>
The 'use' element references to the 'path' element, whose id is 'head_path' and indicates that the graphical content (in this case only the path data) of 'head_path' element is included at this mask. The stroke-width that is defined on the above 'use' element will be the amount of offset (erode) effect. This amount is masked out of the element in case, which we are going to draw next.
Okay, let's draw first the 'head' without masking to see how beautiful it is:
...
</defs>
<use x="5" y="5" xlink:href="#head_path" fill="#4477FF" stroke="black"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
This produces the following shape:
Now test, what we can achieve using mask:
...
</defs>
<use x="5" y="5" xlink:href="#head_path" fill="#22EE22" stroke="black"
stroke-width="21" stroke-linecap="round" stroke-linejoin="round"
mask="url(#myMask)"/>
The above 'use' element is instructed to use 'myMask' as a mask and 'head_path' as a graphical content. The mask effect is applied to the 'use' element and the following shape is drawn:
If we stack both on top of each, we can compare the original head to the masked head:
Not bad at all? Let's compare the first attempt using SVG erode filtered version to the masked version:
The left one is erode-filtered and the right one is masked to imitate Illustrator-like Offset Path Effect. No odd artifacts in the hat and mouth!
How about dilate then? Is there a way to remove the path unfidelity on nose and scrappiness of the baseball cap? Sure. And the method is really simple but sort of hack. Fortunately there is no need to use masks. Instead we can adjust stroke-width to achieve the desired effect. And because the stroke is already used for boldening, to get a black stroke around boldened shape (if ever wanted), we have to add an additional copy of element with a little wider stroke and lay it out below the boldened shape:
<!-- To get the black stroke -->
<use x="220" y="5" xlink:href="#head_path" fill="red" stroke="black"
stroke-width="24" stroke-linecap="round" stroke-linejoin="round"/>
<!-- To get the boldened shape -->
<use x="220" y="5" xlink:href="#head_path" fill="red" stroke="red"
stroke-width="21" stroke-linecap="round" stroke-linejoin="round"/>
This produces the following shape:
Here both the original shape and the one with our custom Offset Path Effect applied:
How our custom boldening compares to dilate filter:
The left one (above) is dilated using SVG:s dilate filter, the right one is boldened using our custom Offset Path Effect. Pretty nice, I like. Path follows faithfully the original path at the given distance and no signs of scrappiness on baseball cap.
And finally let's pull all the wires together:
The left one (above) uses dilate/erode filter of SVG and the right one uses Illustrator-imitated Offset Path Effect, which is achieved using SVG mask and thicker strokes. Which one you would choose?
Conclusion: We are not forced to use Javascript or other scripts to thicken or thin graphical elements in SVG. Erode and Dilate filters of SVG may have some using purposes, but they are not well suitable for high-quality path "modifications". Masks are a little complicated to use, but after few experiments you get familiar with them. I really hope that SVG in the future would support Offset Path Effect natively, without using this like hacks.
I jsfiddled the shapes used in these examples for you to play with filters and masks: http://jsfiddle.net/7Y4am/
(Test at least to change stroke widths!)
(Sorry my bad English, which get native speakers to laugh until die, but please remember, I belong to the 94% of humanity, who does not speak English natively. But fortunately we have Google Translate.)
This package allows one to perform SVG path offsets: https://www.npmjs.com/package/@flatten-js/polygon-offset and the readme offers extensive info on the algorithm used.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With