Web Platform Team Blog

Adobe

decor

Making the web awesome

Winding rules in Canvas

What are winding rules?

Paths are a very basic building block of any graphics library. Every time you draw a path, your browser needs to determine if a point on the canvas falls inside the enclosed curve. When the path is a simple circle or rectangle, this is obvious but when the path intersects itself or has nested paths, it is not always clear.

There are 2 commonly used ways to compute if a point in a path should be filled: ‘non-zero‘ and ‘even-odd‘.

‘non-zero’ winding

This winding rule is most commonly used and was also the only rule that was supported by Canvas 2D.

To determine if a point falls inside the curve, you draw an imaginary line through that point. Next you will count how many times that line crosses the curve before it reaches that point. For every clockwise rotation, you subtract 1 and for every counter-clockwise rotation you add 1.

A point is inside the curve if the total is not equal to zero. Confused? Here is an example to make it more clear:

                                                       non-zero

This is a single path that consists of 2 circles. The outer circle is running counterclockwise and the inner circle is running clockwise.

We have 3 points and want to determine if they fall within the path. The imaginary line in this example goes from bottom left to top right but you can draw it any way you want.

    • point 1. Total = 1 so inside and painted
    • point 2. Total = 1 – 1 = 0  so outside and not painted
    • point 3. Total = 1 – 1 – 1 = -1  so inside and painted

Now, let’s change the winding of the inner circle:

                                                       nonzero_2

    • point 1. Total = 1 so inside and painted
    • point 2. Total = 1 + 1 = 2  so inside and painted
    • point 3. Total = 1 + 1 + 1 = 3  so inside and painted

‘even-odd’ winding

To determine if a point falls inside the path, you once again draw a line through that point. This time, you will simply add the number of times you cross a path. If the total is even, the point is outside; if it’s odd, the point is inside. The winding of the path is ignored. For example:

                                                       evenodd

  • point 1. Total = 1 so inside and painted
  • point 2. Total = 1 + 1 = 2  so outside and  not painted
  • point 3. Total = 1 + 1 + 1 = 3  so inside and painted

‘Even-odd’ winding is easier to grasp for an author since winding is hard to keep in your head. For instance, if you want to make a donut using a big and a small circle with ‘non-zero’ winding, you have to do tricks to change the winding of the inner circle. With ‘even-odd’, you just draw the two circles and fill with ‘even-odd’ winding.

Addition to canvas

As mentioned earlier, Canvas 2D did not have support for ‘even-odd’ winding.

Mozilla implemented a prefixed ‘mozFillRule’ property that set the fill rule in the graphics state. This had a couple of drawbacks:

  1. Adding this to the state will forces the user to always check the winding rule before every fill or clip, or have a convention to set and reset the winding rule that is not as commonly used
  2. Clipping and hit detection is also affected by this rule, so the name ‘fillRule’ is confusing
  3. Adding more parameters to the graphics state introduces some overhead
  4. It’s more work for the author and the environment since you have to make an extra call across the JavaScript boundary
  5. Almost all other graphic languages (such as PDF and SVG) and libraries (such as CoreGraphics, Direct2D and Skia) set the winding at use time.

After a discussion on the mailing lists, we came up with the following API:

enum CanvasWindingRule { "nonzero", "evenodd" };

void fill(optional CanvasWindingRule w = "nonzero");

void clip(optional CanvasWindingRule w = "nonzero");

boolean isPointInPath(unrestricted double x, unrestricted double y, 
                      optional CanvasWindingRule w = "nonzero");


‘fill’, ‘clip’ and ‘isPointInPath’ will now take an optional parameter that specifies what winding rule to apply. If you don’t specify it, you get the old behavior which is ‘non-zero’.

Here is an example script that shows the feature in action:


var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgb(255,0,255)';
ctx.beginPath();
ctx.arc(75, 75, 75, 0, Math.PI*2, true);
ctx.arc(75, 75, 25, 0, Math.PI*2, true);
ctx.fill("evenodd");

and the output will look like:

canvas_evenodd

Implementation status

The underlying graphics APIs in WebKit and Mozilla already had support for winding, so it was easy to wire this up.

You can download a nightly FirefoxWebKit or Chromium build to experiment with the feature. A special thanks goes out to the mozilla and webkit people that made this API progress so quickly!

Please let us know what you think and if you have any ideas for improving Canvas further!

5 Comments

  1. February 03, 2013 at 11:43 pm, T. Reiss said:

    Hi Rik,

    > For every clockwise rotation, you add 1 and for every counter-clockwise rotation you subtract 1.
    Should be “For every counter-clockwise rotation, you add 1 and for every clockwise rotation you subtract 1.”

    • February 04, 2013 at 11:21 am, Rik Cabanier said:

      Good catch! The text didn’t match the example.
      I updated the article.

  2. July 29, 2013 at 11:04 am, Filip said:

    Little bit confused here…

    Non-zero winding:
    “point 1. Total = 1 so inside and painted
    point 2. Total = 1 – 1 = 0 so outside and not painted”

    ok, but now: I cross that inner circle one more time and because of it’s clockwise rotation, I should substract 1, shouldn’t I?
    And so: point 3. Total = 1 – 1 – 1 = -1 ?

    Am I doing something wrong?

    • July 29, 2013 at 5:19 pm, Rik Cabanier said:

      You’re right! I will update the blog.

  3. September 04, 2013 at 3:34 am, How to fill shapes on HTML5 Canvas using EvenOdd winding rule said:

    […] January of 2013, Rik Cabanier posted an article on the Adobe blog announcing that the implementation details had been figured out, and that support for both winding […]