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:
100 100 moveto
150 100 lineto
<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.
A Bezier path segment with high curvature at the end. Web browsers differ on the rendering. (SVG)
Rendering of above path: Firefox (left/top), Chrome (right/bottom). (PNG)
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.
A Bezier path segment with high curvature at the end. Stroke constructed by offsetting path. Red: original path, blue: offset paths. (SVG)
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.
Two same size circular paths with different stroke widths. When one-half the stroke width exceeds the circle radius (right circle), web browsers differ in their rendering. (SVG)
Rendering of a circular path when one-half the stroke width is greater than the radius in: Firefox (left/top), Chrome (right/bottom). (PNG)
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:
Stroking the path. The arrows indicate the direction of the offset paths. If the drawing is not animated, view the image by itself. (SVG)
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.
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:
Top: the original path. Middle: left: stroke inside; right: stroke outside. Bottom: left: stroke to left; right: stroke to right.
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.
Top to bottom: Default stroke. Fill area (in gray). Inside (according to SVG specification?). Outside (implemented here by masking). Inside (another interpretation). Outside (according to SVG specification?). Stroke on left (round end cap in pink).
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?
Round end caps. Top to bottom: Default stroke. Stroke alignment ‘outside’, end-cap radius doubled. Stroke alignment ‘outside’, end-cap radius same as normal.
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.
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.
An excerpt from a submarine cable map showing the use of offset paths to shade around land masses. Note also the use of inside strokes to define country boundaries.
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
- Offset paths may require breaking apart
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.
Here is a
simple example path with offsets both inside and outside:
Left: insets, right: outsets. Red path is original.
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
The same original path as in the above figure. Left: the light blue region is created by stroking the original path. As can be seen it matches the corresponding outset (blue) and inset (green) paths. Right: The
raw offset paths used to construct the visible outset and inset paths. In this case, the outset path is constructed from the raw outset path (blue) and the inset path is constructed from the raw inset path (green).
Cusp loops and overlaps have been removed.
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:
Left: The left-offset path (blue) and the right-offset path (green), relative to the path’s direction (clock-wise). Right: The resulting outset path (blue) and inset path (green).
Here’s an example where Inkscape’s
Linked Offset function gets it wrong:
The resulting outset path (blue) and inset path (green) as found by Inkscape’s
Linked Offset function.
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.
Left: Outset path with three different types of joins: ‘bevel’, ’round’, and ‘arcs’. Right: Outset paths with various offsets and with the ‘arcs’ line join. Note: the ‘arcs’ line join fails for the outer most path as the generated arcs do not intersect; this results in falling back to a ‘miter’ line join.
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.
- 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+.