How Vector Graphics Work
I have worked with vecor graphics for more than 15 years, but some mechanisms were not clear to me. Now, I think I have figured it out and I would like to explain it to you.
Vector graphics usually consists of points, which are connected with straight or curved lines (Bézier curves, circular arcs). Together, they define a contour of some shape. There are commands such as moveTo, lineTo, curveTo, arcTo and closePath, which let us define points of the contour, and connections between these points. A path might consist of any number of these commands in any order. If we take any path and shuffle the commands randomly, it still defines a valid path.
Such vector contours can be stroked or filled with color. It is clear, that if we fill a circle or a square, we will get a circle or a square. But many cases are not clear at all.
- When one shape intersects another, will the intersection be filled or not?
- When a shape intersects itself, will the intersection be filled or not?
- When I make a small circle inside a big one, will it cut a hole or do nothing?
- Does it matter, if I draw a big circle first and a smaller after, or a small one first?
- Does it matter, if a contour is drawn in a clockwise or anti-clockwise direction, when making holes?
- What are the fill orders like "evenodd" and "nonzero"? Are they boolean operations?
These mechanisms of filling vector shapes were defined in 60s and 70s, and probably will be used for many centuries. They are used in many standards (pretty much everywhere where vector graphics is used):
- "2d" context of HTML5 Canvas
- PostScript (EPS and PDF)
- OpenType / TrueType fonts
- Flash Player and ActionScript programming
- Adobe Photoshop / Illustrator / XD, Sketch App, Figma
Let's demonstrate our explanation on a specific example. For simplicity, let's just use polygons (no curves).
SVG code: M 100 100 L 360 100 L 200 300 L 200 200 L 360 400 L 100 400 Z M 50 50 L 150 50 L 150 150 L 50 150 Z M 50 200 L 50 300 L 150 300 L 150 200 Z M 120 320 L 180 320 L 180 380 L 120 380 Z M 220 320 L 220 380 L 280 380 L 280 320 Z M 120 120 L 120 180 L 180 180 L 180 120 ZRun SVG
First, we drew a shape, that looks like a letter "K" (a polygon with six points). Then, we drew two large squares. And then, three small squares. The direction of drawing is visualized by red arrows. Now, we want to fill this shape. Can you determine, which parts will be filled, and which will remain empty?
At the beginning, there is a large infinite plane. Our drawing "cuts" the plane into multiple parts. There is a large infinite part on the outside, and many smaller, finite parts. It is not possible to get from one part to another without crossing a line (contour). In our example, there are 11 parts. Now, we have to give a number, a rank, to each part (and to all points inside that part).
At the beginning, the empty area has a rank 0. When a shape is drawn clockwise, all points inside that shape have a rank increased by 1. Similarly, when it is drawn anti-clockwise, all points inside it have a rank decreased by 1.
Imagine two intersecting circles, drawn clockwise (four parts). The outer area has a rank 0, their intersection has a rank 2, and the two remaining parts have a rank 1. If a second circle was drawn anti-clockwise, their intersection would have a rank 0, and the rest of the second circle would have a rank -1.
Alternative definition. Put your finger (or a pen) into a specific part, and set a counter to zero. Now, drag it outside (to the infinite part). Each time you cross a contour "going right", increase the counter by 1, and each time you cross a contour "going left", decrease the counter by 1. At the end, you will get the rank of that part, in which you started. Fun fact: it does not matter in which direction you go, or how many contours you cross, you will always get the same number.
Let's determine ranks for our vector shape.
Now, the solution is very simple: if we fill a shape with the nonzero rule, only parts with non-zero rank will be filled(...-3, -2, -1, 1, 2, 3, ..). If we use the evenodd rule, only parts with odd rank will be filled (... -3, -1, 1, 3, 5, ...).
Knowing this, we can deduce several properties:
- The order, in which we draw contours, is not important (as it does not change the final rank of each part).
- Reversing all contours (CW to ACW and ACW to CW) is not important (it will change every rank R to -R, but both fill rules will work identically)
- If we use the "evenodd" rule, the direction of drawing is not important (if we reverse any contour, it will add 2 or subtract 2 from all ranks inside it, but does not change the "oddness")
An Important Note
We described our mechanism in a plane, where the X axis goes right, and the Y axis goes down. When Y goes up, the terms clockwise and anti-clockwise (and also left and right) are exchanged. Points (0,0), (100,0), (50,50) make a triangle going clockwise when Y goes down, or anti-clockwise when Y goes up. But this triangle always has a rank 1, so it is always filled the same way (i.e. only the coordinates define how a shape is filled, not where axes go).