The Winding Order of the Fill Rule
SVG paths, polygons, and polylines can include criss-crossing lines. Paths can also include intersecting or overlapping subpaths. In any of these cases, it can be a bit confusing to determine which sections of a shape are inside or outside.
There are two different options for calculating which parts of a path are “inside” the shape. They are defined as the values of the fill-rule
style property:
-
With a
fill-rule
ofevenodd
, the inside/outside state changes every time you cross a path edge. -
With a
fill-rule
ofnonzero
(the default), it depends on the direction of the path: paths that loop in the same direction make that section “more” inside.
The same two keywords (and the same default) are used in CSS shapes functions and in the SVG clip-rule
property, which determines which parts are inside or outside for SVG shapes that are children of a <clipPath>
.
To show how the fill region is calculated with each fill rule, Example 6-X1 creates four different paths that look suspiciously the same, but have very different structures:
-
a single subpath that crosses itself
-
a single subpath that bumps into itself, then reverses direction without crossing
-
a path with a cut-out subpath that is drawn in the same direction (clockwise) as the outer subpath
-
a path with a cut-out subpath that is drawn in the opposite direction as the outer subpath
With fill-rule: evenodd
, all four paths have the same stroke and fill area, as shown in Figure 6-X1. The outer region of the shape is filled, but the central triangle isn’t.
If you switch to fill-rule: nonzero
, as shown in Figure 6-X2, the crossing-over path and the same-direction subpath switch to being filled in. The other two shapes don’t change.
What’s going on? First the code, then the explanation.
Example 6-X1. Defining paths with winding order in mind
<svg
xmlns=
"http://www.w3.org/2000/svg"
xml:lang=
"en"
viewBox=
"0 0 540 170"
width=
"4in"
height=
"1.25in"
>
<title
>
Fill-rule shapes
</title>
<style
>
path
{
fill
:
lightSkyBlue
;
stroke
:
darkRed
;
stroke-width
:
4
;
fill-rule
:
evenodd
;
}
text
{
fill
:
indigo
;
font
:
bold
18px
Segoe
UI
,
sans-serif
;
}
</style>
<g
>
<text
x=
"70"
y=
"164"
>
one sub-path
</text>
<path
d=
"M 10,5 l 100,10 -80,80 0,-60 80,80 -100,10 z"
/>
<text
x=
"5"
y=
"144"
>
crossing
</text>
<path
d=
"M 150,5 l 100,10 -50,50 -30,-30 0,60 30,-30 50,50 -100,10 z"
/>
<text
x=
"145"
y=
"144"
>
reversing
</text>
</g>
<g
>
<text
x=
"350"
y=
"164"
>
two sub-paths
</text>
<path
d=
"M 290,5 l 100,10 -50,50 50,50 -100,10 z m 50,60 l-30,30 0,-60 z"
/>
<text
x=
"285"
y=
"144"
>
same-direction
</text>
<path
d=
"M 430,5 l 100,10 -50,50 50,50 -100,10 z m 50,60 l -30,-30 0,60 z"
/>
<text
x=
"425"
y=
"144"
>
opposite
</text>
</g>
</svg>
-
This version of the code sets the
evenodd
value offill-rule
. To see the difference, change it tononzero
; or just remove this line, asnonzero
is the default. -
The first shape is a single subpath that starts from the top left and proceeds in a clockwise direction. The path crosses over itself to form a triangular loop in the middle, and then wraps around the outside.
-
The second shape is also a single subpath, but it never crosses itself. Instead, when the paths reach the point where they would intersect, they reverse course, and wrap around the inner triangle in the opposite direction before continuing with the outside edge of the path.
-
The remaining paths are drawn as two subpaths each. Both have an outer subpath that wraps around in a clockwise direction, and an inner triangular subpath. In the same-direction shape, the inner triangle is also drawn in a clockwise direction, starting from the intersection point on the right, then the bottom left, then vertically up (negative y direction), before closing it off.
-
In the final shape, with the opposite-direction subpath, the inner triangle is drawn in a counter clockwise direction, touching the points in the same direction as the “reversing” single-path shape: from the intersection point, going to the top left of the triangle, then down, and finally closed.
The path data in Example 6-X1 uses relative coordinates so you can more easily compare the differences in shapes, and track the direction of each line segment: up and down correspond to negative versus positive y values, while negative versus positive x values map to left versus right.
The example also shows the relative m
move-to command. As we mentioned in the book, this moves the pen relative to the final point of the previous subpath. Here, that’s the implicit point created by the z
close-path commands.
To understand how path direction and fill-rule
options are connected, you need to determine the winding order of different regions.
For any region within the shape, the winding order is calculated from the directions of every path segment you need to cross to reach that point. A point outside the graphic has a winding-order value of 0. Starting from there, every clockwise path boundary you cross over will add one to your winding order. Every counter-clockwise path that you cross over subtracts one from your winding order. The more loops there are in your shape, the more “wound up” you get.
In Figure 6-X3, we show how the winding order is calculated for each version of our shape. Arrowheads mark the directions of each line segment. To make things a little easier to follow, the points have been shifted slightly, to separate the strokes which don’t actually intersect.
If the value of fill-rule
is evenodd
, regions will be filled if their winding order is an odd number; since the winding order changes by 1 every time you cross a path segment, the fill alternates between inside and out. However, if the value of fill-rule
is nonzero
(the default, as shown in Figure 6-X3), regions will be filled unless the winding-order sum cancels out back to zero. Paths that loop in the same direction will add together to create a value farther from zero.
Winding orders are related to the rasterization process we mentioned in Chapter 1 and in the “Understanding Vector Graphics” article. When a computer fills in a vector shape, it doesn’t see the entire shape at once. Instead, it scans across it one row of pixels at a time, looking at each edge independently.
Looking at Figure 6-X3, with the slightly adjusted points, it is now obvious that the central triangle in the “reversing” path is actually just an extension of the outside. Of course it isn’t ever filled in.
But to a rasterizer scanning across the top half of the shape, it looks identical to the shape with the opposite-direction subpath. The edges all follow the same directions. So the opposite-direction subpath is also never filled in.
One final note on fill-rule
: neither of the fill-rule values care if your winding order is positive or negative. If all the paths were reversed, the result would be the same.
Tip
For SVG, the important thing isn’t clockwise versus counter-clockwise, it’s same versus opposite direction. (And even that can be ignored when you set fill-rule
to evenodd
.)
Positive versus negative winding order becomes much more important in mapping, where a single path can wrap all the way around the globe and back again. In that case, it isn’t clear which side of the path is “outside” to start with. For example, if you created a path that traced around the coastlines of every continent, which would be inside the path: the oceans or the land?
Many mapping applications—including the d3.js modules that convert map data to SVG—rely on the rule that clockwise paths enclose content and counter-clockwise paths exclude them.