Path stroking and offsetting are two intertwined topics; stroking is often implemented by path offsetting. This post explores some of the problems encountered with these path operations.
Stroking: It’s not as easy as it looks.
What could be easier that stroking a path? It’s a fundamental concept in all graphics libraries. You construct a path:
in PostScript:
newpath
100 100 moveto
150 100 lineto
10 setlinewidth
stroke
in SVG:
<path d="M 100,100 150,100" stroke-width="10"/>
and voila, you have a horizontal path, 50 pixels long, that is 10 pixels wide.
Hmm, if only it were that easy. It turns out that stroking an arbitrary path can be quite complicated. Different graphics libraries can give quite different results.
There are two different ways to stroke a path. The first method is to pass a line segment of length ‘stroke-width’, centered on and perpendicular to the path, from one end to the other. Any pixels the line crosses are part of the stroke. This seems to be what Firefox does. (An equivalent method is to pass a circle of diameter ‘stroke-width’ centered on the path and then clip the semi-circles at the ends.) The second method is to construct two paths, offset by half the ‘stroke-width’ on each side of the original path and then fill the area between the two paths. This seems to be what Chrome does.
Rendering engines appear to fall into one of these two camps:
- Sweep a line:
- Firefox, Adobe Reader
- Offset paths:
- Chrome, Inkscape (Cairo), Opera (Presto), Evince, Batik, rsvg
The difference can be also be seen in circular paths.
When using the
Offset paths
method, an inner path is always created. As the direction of this path is the same regardless of the stroke width, one cannot differentiate between the case where the stroke width is less than one-half the radius and the case where it is not. This can be seen in the animation below:
Interestingly, some renderers draw a filled circle when one-half the ‘stroke-width’ is greater than the radius for an SVG <circle> (i.e. not a circular <path>) while others still draw a doughnut. However, for the SVG <rect> element, the rectangles are always drawn filled if the ‘stroke-width’ is greater than either the ‘width’ or ‘height’ (at least in the renderers I tested).
So what does the SVG specification say about how to stroke a path? Nothing…! One can look to PostScript and PDF on which SVG is partially based for a hint on what it should say. The PostScript and PDF specifications say the same thing. From the PDF 1.7 reference:
The S operator paints a line along the current
path. The stroked line follows each straight or curved segment
in the path, centered on the segment with sides parallel to
it. Each of the path’s subpaths is treated separately…
This seems to indicate that the
sweeping the line
technique is what is expected and indeed, Adobe’s own product, Adobe Reader, appears to do just that.
Stroke Alignment
Designers often want more control over how a stroke is positioned: only on the inside, only on the outside, or some arbitrary ratio of the two. The new SVG ‘
stroke-alignment‘ property offers this control. For a closed path, it is relatively easy to figure out how this property should behave:
For an open path, it is not quite so easy. What is
inside
, what is
outside
? One can define the terms by looking at what is filled: inside is in the fill, outside is not in the fill. With this definition, a single straight line segment would render nothing for an ‘inside’ stroke and a stroke on both sides for an ‘outside’ stroke. The
SVG specification has a slightly different definition for ‘outside’ (see figure). For an open path it may make more sense to talk about left/right rather than inside/outside.
Handling line joins is fairly straight forward. End caps, at least ’round’ ones, are another matter. Does one draw half an end cap? Or does the radius of the end cap match the width of the (shifted) stroke?
The ‘stroke-alignment’ property was recently removed from the SVG 2 specification draft and moved into a separate
SVG Strokes module, partly due to the difficulty in specifying exactly how it should behave.
The ‘stroke-alignment’ ‘inside’/’outside’ values can be simulated via other methods. The new ‘
paint-order‘ property allows one to paint the stroke before the fill and thus simulating stroking only the outside of the path (this only works for opaque fill). A mask can also be used to simulate stroking the outside of path. A clip path can be used to simulate stroking the inside of a path.
Offset Paths
We’ve seen that offsetting a path can be used for constructing strokes. What about offsetting a path for the purpose of creating a new path? This is quite useful in mapping. For example you might want to show multiple bus routes going along a road with different offsets for each route. More stylistically, you could produce the
shadowing
seen around land masses in older, hand-drawn maps.
Offsetting paths is in practice extremely tricky! Here are a few of the problems:
- Offsets of Bezier segments are not Beziers; in fact they are 10th-order polynomials. In practice, one can do a pretty good job of estimating the offset by breaking up a Bezier path into smaller segments.
- Offset paths can have loops at
cusps
.
- Offset paths may require breaking apart
left
and right
offset paths and recombining to form outset and inset paths. It can be difficult to get this right.
Entire scientific papers are written on this topic.[1]
Here is a
simple
example path with offsets both inside and outside:
In this case, the outsets correspond to the outer edge of a stroked path with appropriate width when the ‘stroke-linejoin’ type is ’round’. The insets correspond to the inner edge of such strokes. Taking a closer look at the offset paths shows a number of
cusp
loops:
Determining what is outset or inset becomes more difficult as a path loops back on itself. Both the outset and inset paths can consist of parts of both the right-offset and left-offset paths as shown below:
Here’s an example where Inkscape’s
Linked Offset
function gets it wrong:
The previous examples assumed that the line joins for
outside
joins are rounded. It would be desirable to be able to specify the type of join to use. This can maintain the
feel
of the original path.
Allowing more freedom to define stroke position and being able to offset strokes are highly desirable features for designers, but as this post shows, they are not so simple to implement. Before we can add such features to SVG, we need to define robust algorithms for generating proper offset paths.
References
- An offset algorithm for polyline curves Xu-Zheng Liu, Jun-Hai Yong, Guo-Qin Zheng, Jia-Guang Sun.
An image for the sole purpose of having a good PNG image to show in Google+ which doesn’t support SVG images, bad Google+.
It seems logical that Firefox and Adobe Reader render it right even though specifications say nothing about stroking a path. Imagine a self-intersecting path that has stroke width much less than its radii of curvature. It does not produce something like “evenodd” rule for “fill-rule”, it acts like “nonzero”. Your examples for circle with half stroke-width higher than its radius and high curvature path fall in the same category. It’s stroke effectively produces a shape that is self-intersecting, and there’s no reason for rendering it differently from more trivial self-intersections.
I guess SVG specs should be more specific about it (no pun intended). If they ever allow for something like evenodd/nonero options, if should default to “nonzero” kind when not specified, because it’s what trivial self-intersections produce.
An interesting possibility is to have different fill-rules for stroke and fill, combined with transparencies, paint-order (stroke over fill, fill over stroke) and stroke-alignment.
Now, how do strokes behave with respect to fills? Can stroke-fill intersection have fill-rules like nonzero/evenodd of their own? I think it’s overcomplicating things, but still?
Also, what markers should behave like? I assume they should just be overlaid on top of each other (do they assume transparency?).