The issue I have is very straightforward. This is a variation of the "How can I draw a hole in a shape?" question, to which the classic answer is "Simply draw both shapes in the same path, but draw the solid clockwise and the "hole" counterclockwise." That's great but the "hole" I need is often a compound shape, consisting of multiple circles.
Visual description: https://i.sstatic.net/UijEi.png.
jsfiddle: http://jsfiddle.net/d_panayotov/44d7qekw/1/
context = document.getElementsByTagName('canvas')[0].getContext('2d');
// green background
context.fillStyle = "#00FF00";
context.fillRect(0,0,context.canvas.width, context.canvas.height);
context.fillStyle = "#000000";
context.globalAlpha = 0.5;
//rectangle
context.beginPath();
context.moveTo(0, 0);
context.lineTo(context.canvas.width, 0);
context.lineTo(context.canvas.width, context.canvas.height);
context.lineTo(0, context.canvas.height);
//first circle
context.moveTo(context.canvas.width / 2 + 20, context.canvas.height / 2);
context.arc(context.canvas.width / 2 + 20, context.canvas.height / 2, 50, 0, Math.PI*2, true);
//second circle
context.moveTo(context.canvas.width / 2 - 20, context.canvas.height / 2);
context.arc(context.canvas.width / 2 - 20, context.canvas.height / 2, 50, 0, Math.PI*2, true);
context.closePath();
context.fill();
EDIT:
Multiple solutions have been proposed and I feel that my question has been misleading. So here's more info: I need the rectangle area to act as a shade. Here's a screenshot from the game I'm making (hope this is not against the rules): https://i.sstatic.net/gXwU0.png.
@markE:
@hobberwickey: That's a static background, not actual canvas content. I can however use clip() the same way I would use "source-atop" but that would be inefficient.
The solution that I have implemented right now: http://jsfiddle.net/d_panayotov/ewdyfnj5/. I'm simply drawing the clipped rectangle (in an in-memory canvas) over the main canvas content. Is there a faster/better solution?
I almost dread posting the first part of this answer because of its simplicity, but why not just fill 2 circles on a solid background?
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var r=50;
ctx.fillStyle='rgb(0,174,239)';
ctx.fillRect(0,0,cw,ch);
ctx.fillStyle='white'
ctx.beginPath();
ctx.arc(cw/2-r/2,ch/2,r,0,Math.PI*2);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.arc(cw/2+r/2,ch/2,r,0,Math.PI*2);
ctx.closePath();
ctx.fill();
body{ background-color: ivory; }
#canvas{border:1px solid red;}
<canvas id="canvas" width=400 height=168></canvas>
Alternatively...to "knockout" (erase) the double-circles...
If you want the 2 circles to "knockout" the blue pixels down so the double-circles are transparent & reveal the webpage background underneath, then you can use compositing to "knockout" the circles: context.globalCompositeOperation='destination-out
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var r=50;
// draw the blue background
// The background will be visible only outside the double-circles
ctx.fillStyle='rgb(0,174,239)';
ctx.fillRect(0,0,cw,ch);
// use destination-out compositing to "knockout"
// the double-circles and thereby revealing the
// ivory webpage background below
ctx.globalCompositeOperation='destination-out';
// draw the double-circles
// and effectively "erase" the blue background
ctx.fillStyle='white'
ctx.beginPath();
ctx.arc(cw/2-r/2,ch/2,r,0,Math.PI*2);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.arc(cw/2+r/2,ch/2,r,0,Math.PI*2);
ctx.closePath();
ctx.fill();
// always clean up! Set compositing back to its default
ctx.globalCompositeOperation='source-over';
body{ background-color: ivory; }
#canvas{border:1px solid red;}
<canvas id="canvas" width=400 height=168></canvas>
On the other hand...
If you need to isolate those double-circle pixels as a containing path, then you can use compositing to draw into the double-circles without drawing into the blue background.
Here's another example:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var r=50;
var img=new Image();
img.onload=start;
img.src="https://dl.dropboxusercontent.com/u/139992952/multple/mm.jpg";
function start(){
// fill the double-circles with any color
ctx.fillStyle='white'
ctx.beginPath();
ctx.arc(cw/2-r/2,ch/2,r,0,Math.PI*2);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.arc(cw/2+r/2,ch/2,r,0,Math.PI*2);
ctx.closePath();
ctx.fill();
// set compositing to source-atop
// New drawings are only drawn where they
// overlap existing (non-transparent) pixels
ctx.globalCompositeOperation='source-atop';
// draw your new content
// The new content will be visible only inside the double-circles
ctx.drawImage(img,0,0);
// set compositing to destination-over
// New drawings will be drawn "behind"
// existing (non-transparent) pixels
ctx.globalCompositeOperation='destination-over';
// draw the blue background
// The background will be visible only outside the double-circles
ctx.fillStyle='rgb(0,174,239)';
ctx.fillRect(0,0,cw,ch);
// always clean up! Set compositing back to its default
ctx.globalCompositeOperation='source-over';
}
body{ background-color: ivory; }
#canvas{border:1px solid red;}
<canvas id="canvas" width=400 height=168></canvas>
{ Additional thoughts given addition to answer }
A technical point: xor
compositing works by flipping just the alpha values on pixels but does not also zero-out the r,g,b portion of the pixel. In some cases, the alphas of the xored pixels will be un-zeroed and the rgb will again display. It's better to use 'destination-out' compositing where all parts of the pixel value (r,g,b,a) are zeroed out so they don't accidentally return to haunt you.
Be sure... Even though it's not critical in your example, you should always begin your path drawing commands with maskCtx.beginPath()
. This signals the end of any previous drawing and the beginning of a new path.
One option: I see you're using concentric circles to cause greater "reveal" at the center of your circles. If you want a more gradual reveal, then you could knockout your in-memory circles with a clipped-shadow (or radial gradient) instead of concentric circles.
Other than that, you solution of overlaying an in-memory canvas should work well (at the cost of the memory used for the in-memory canvas).
Good luck with your game!