Winding Rules in HTML’s Canvas

Feb 01 2013 Published by under JavaScript

Send to Kindle

This is another one that a lot of people should already know, but I’m still betting that a few will find useful. Winding refers to the direction a path is drawn in – clockwise or counterclockwise. In many cases, this makes no difference at all, but there are cases where this is very important and can become a very useful tool.

Just realized that most of the examples I’m showing here could have been done just using a fat stroke instead of fills. My fault for poor examples. I was just trying to show simple shapes. This is still a very valid technique and useful for more complex shapes, such as the last example.

The most common use for this is drawing a shape with a hole in it. Let’s say we wanted to draw a black triangle with an empty hole in the center. We can simply draw a black triangle with a smaller white triangle on top of it.

  context.beginPath();
  context.moveTo(300, 100);
  context.lineTo(500, 400);
  context.lineTo(100, 400);
  context.lineTo(300, 100);
  context.fill();
  
  context.fillStyle = "white";
  context.beginPath();
  context.moveTo(300, 150);
  context.lineTo(450, 370);
  context.lineTo(150, 370);
  context.lineTo(300, 150);
  context.fill();

If the background is also white, this looks just fine.

wind_01

But what if we first draw something in the background, like this red bar?

  context.fillStyle = "red";
  context.fillRect(10, 200, 580, 100);
  context.fillStyle = "black";
  
  context.beginPath();
  context.moveTo(300, 100);
  context.lineTo(500, 400);
  context.lineTo(100, 400);
  context.lineTo(300, 100);
  context.fill();
  
  context.fillStyle = "white";
  context.beginPath();
  context.moveTo(300, 150);
  context.lineTo(450, 370);
  context.lineTo(150, 370);
  context.lineTo(300, 150);
  context.fill();

wind_02

The illusion is shattered. I once tried to change the second fill style from “white” to fully transparent with “rgba(255, 255, 255, 0)” thinking that it would overlay the black with transparency, but no, that does not work. Even if it did, it would also wipe out the red underneath it, so forget that.

The trick is to draw the second triangle within the same beginPath() / fill() section, and draw it in the opposite winding direction. Like so:

  context.fillStyle = "red";
  context.fillRect(10, 200, 580, 100);
  context.fillStyle = "black";
  
  context.beginPath();
  
  // first triangle (clockwise)
  context.moveTo(300, 100);
  context.lineTo(500, 400);
  context.lineTo(100, 400);
  context.lineTo(300, 100);

  // second, smaller triangle (counterclockwise)
  context.moveTo(300, 150);
  context.lineTo(150, 370);
  context.lineTo(450, 370);
  context.lineTo(300, 150);

  context.fill();

Notice that in the second triangle I swapped the first two lineTo statements, so it draws the triangle from the top, to bottom left, bottom right, back to top. This is counterclockwise, while the first triangle is drawn in the opposite direction. Because of this, the second triangle cuts out or subtracts from the first. This gives you the following:

wind_03

This works well for drawing lines with shapes. What about rectangles? Well, it won’t work with fillRect, as fillRect does it’s own internal beginPath() and fill() as part of its operations, so each call to it will draw completely separately, as in the first example. So you have to use the rect method. By default, all rects are drawn with the same winding, but there is a way to reverse the winding – just reverse either the width or height. An example will help:

  context.fillStyle = "red";
  context.fillRect(10, 100, 580, 100);
  context.fillStyle = "black";

  context.beginPath();
  context.rect(50, 50, 200, 200);
  context.rect(75, 225, 150, -150);
  context.fill();

Normally, you’d draw the second rect at an x, y of 75, 75, with a width and height of 150. But this would have the same winding as the first. Reversing either the width or height to become a negative value reverses the winding. Reversing both will send it back to the original. Note that you have to change the position on the axis you reversed the size on. So a position of 75, 75 becomes 75, 225, with a size of 150, -150. This gives you the following:

wind_04

Of course, you might be able to use clearRect, but that would wipe out any background as well.

How about arcs? If you’ve used these, you’ve seen that last boolean parameter and probably just ignore it or use whatever value you’ve gotten used to. I always use false. But if you want to punch a circle from another circle, to make a ring, for example, just draw them with opposite values for that last param:

  context.fillStyle = "red";
  context.fillRect(10, 100, 580, 100);
  context.fillStyle = "black";

  context.beginPath();
  context.arc(150, 150, 100, 0, Math.PI * 2, true);
  context.arc(150, 150, 70, 0, Math.PI * 2, false);
  context.fill();

wind_05

And of course you can mix and match these different shapes, though it might take some trial and error to get the different windings going in the right directions. Here’s a triangle with a punched out circle:

  context.fillStyle = "red";
  context.fillRect(10, 200, 580, 100);
  context.fillStyle = "black";
  
  context.beginPath();
  
  context.moveTo(300, 100);
  context.lineTo(500, 400);
  context.lineTo(100, 400);
  context.lineTo(300, 100);

  context.arc(300, 300, 70, 0, Math.PI * 2, true);

  context.fill(); 

wind_06

Well, that’s the basics on winding. Once you get it down, it can be a pretty powerful way of composing complex shapes.

Send to Kindle

One response so far

Leave a Reply