Nick
Nick

Reputation: 594

Polygon with a hole in the middle with HTML5's canvas

Using the <canvas> tag I need to be able to draw a hole in a polygon.

Right now I have something very simple that uses beginPath() then does lineTo() for each point. It is then filled with fill().

I cannot see any way to have a filled polygon with a unfilled middle though, like a donut. I'm not making a donut but it is suitable for this example.

Is there something I am missing? I would rather not draw it fully filled then have to redraw the middle.

Upvotes: 16

Views: 14660

Answers (4)

Richard
Richard

Reputation: 5101

Draw your shape in a clockwise direction (for example). Then you can increase or decrease the radius and go in an anti-clockwise direction.

EDIT: Here's a very simple example of a donut:

<canvas id="cvs" width="300" height="300">[No canvas support]</canvas>

<script>
    // Get the context
    context = document.getElementById('cvs').getContext('2d');
    
    // Start a new path
    context.beginPath();
    
    // Draw a circle with a radius of 100 in the
    // clockwise direction
    context.arc(150,150,100,0, 2 * Math.PI, false);
    
    // Now draw another circle with a smaller
    // radius and in the anti-clockwise direction
    context.arc(150,150,60,2 * Math.PI, 0, true);
    
    // Fill the resultant shape
    context.fill();
</script>

There a demonstration of the result on this page:

https://www.rgraph.net/canvas/reference/arc.html#how-to-draw-a-donut-shape

To help understand it it can be useful to change the context.fill() to context.stroke() and change both of the 2 * Math.PI to 1.5 * Math.PI This makes the example more clear.

It first draws an arc and then draws a second arc with a smaller radius in reverse - and there's a "connecting line" which is drawn implicitly (ie not by the code) from the end of the first arc to the start of the second. The path doesn't need closing but doing so causes no harm. If you're just stroking the donut and not filling it you might prefer this.

Upvotes: 2

Yas
Yas

Reputation: 5471

You can use evenodd fill rule: fill('evenodd')

Even-odd rule

// properties
// - outer square
var outerLength = 200;
// - inner length
var innerLength = outerLength / 2;

// cnavas
var canvas = document.getElementById('canvas');
var width = canvas.width = document.body.clientWidth;
var height = canvas.height = document.body.clientHeight;
var context = canvas.getContext('2d');

// path
// - outer square
context.rect(
  (width - outerLength) / 2,
  (height - outerLength) / 2,
  outerLength,
  outerLength
);
// - inner square
var x0 = (width - innerLength) / 2;
var y0 = (height - innerLength) / 2;
context.moveTo(x0, y0);
context.rect(
  x0,
  y0,
  innerLength,
  innerLength
);

// draw
// - stroke
context.lineWidth = 10;
context.stroke();
// - fill
context.fillStyle = 'red';
context.fill('evenodd');
html,
body {
  margin: 0;
  height: 100%;
  overflow: hidden;
}
<canvas id="canvas"><canvas>

Upvotes: 11

cuixiping
cuixiping

Reputation: 25381

Your have 2 choices to draw holes with HTML5 canvas.

Choice 1:
Draw outer shape and inner shape in different clock direction.

Outer shape clockwise and inner shape anti clockwise.
Or outer shape clockwise and inner shape anti clockwise.

ctx.beginPath();
//outer shape, clockwise
ctx.moveTo(100,20);
ctx.lineTo(200,200);
ctx.lineTo(20,200);
ctx.closePath();
//inner shape (hole), counter-clockwise
ctx.moveTo(100,100);
ctx.lineTo(70,170);
ctx.lineTo(140,170);
ctx.closePath();
//fill
ctx.fillStyle = "#FF0000";
ctx.fill();

It's a little pain to detect the shape drawing direction when we are coding.

If you need to detect whether a series of dots is clockwise or not, here is a good function:

function isClockwise(dots){
    var sum = 0;
    for(var i=1, n=dots.length; i<n; i++){
        sum += (dots[i][0] - dots[i-1][0]) * (dots[i][1] + dots[i-1][1]);
    }
    sum += (dots[0][0] - dots[n-1][0]) * (dots[0][1] + dots[n-1][1]);
    return sum < 0;
}
console.log(isClockwise([[100,20], [200,200], [20,200]]));  //true
console.log(isClockwise([[100,20], [20,200], [200,200]]));  //false

If your dots is clockwise but you need counter-clockwise, .reverse() your dots array.

var dots = [[100,20], [200,200], [20,200]];
dots.reverse();

Choice 2:
Use 'evenodd' fill rule, draw your shapes in any direction.

This way is much simpler than the choice 1.
see fill() method API:

   void ctx.fill();
   void ctx.fill(fillRule);
   void ctx.fill(path, fillRule);

fillRule can be "nonzero" or "evenodd"
"nonzero": The non-zero winding rule, which is the default rule.
"evenodd": The even-odd winding rule.

ctx.beginPath();
//outer shape, any direction, this sample is clockwise
ctx.moveTo(100,20);
ctx.lineTo(200,200);
ctx.lineTo(20,200);
ctx.closePath();
//inner shape (hole), any direction, this sample is clockwise
ctx.moveTo(100,100);
ctx.lineTo(140,170);
ctx.lineTo(70,170);
ctx.closePath();
//fill
ctx.fillStyle = "#FF0000";
ctx.mozFillRule = 'evenodd'; //for old firefox 1~30
ctx.fill('evenodd'); //for firefox 31+, IE 11+, chrome

enter image description here

Upvotes: 33

diwatu
diwatu

Reputation: 5699

This is what i made it work:

var ctx = canvas.getContext("2d");     
ctx.beginPath();

//polygon1--- usually the outside polygon, must be clockwise
ctx.moveTo(0, 0);
ctx.lineTo(200, 0);
ctx.lineTo(200, 200);
ctx.lineTo(0, 200);
ctx.lineTo(0, 0);
ctx.closePath();

//polygon2 --- usually hole,must be counter-clockwise 
ctx.moveTo(10, 10);
ctx.lineTo(10,100);
ctx.lineTo(100, 100);
ctx.lineTo(100, 10);
ctx.lineTo(10, 10);
ctx.closePath();

//  add as many holes as you want
ctx.fillStyle = "#FF0000";
ctx.strokeStyle = "rgba(0.5,0.5,0.5,0.5)";
ctx.lineWidth = 1;
ctx.fill();
ctx.stroke();

The main idea here is you can only use beginPath once; the outside polygon must be clockwise and holes must be counter-clockwise.

Upvotes: 35

Related Questions