简体   繁体   中英

Javascript canvas - intersecting circle holes in rectangle or how to merge multiple arc paths

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: http://i.imgur.com/9SuMSWT.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): http://i.imgur.com/tJRjMXC.png .

  • the rectangle should be able to have alpha less than 1.0.
  • the contents, displayed in the "holes" are whatever is drawn on the canvas before applying the shade.

@markE:

  • Alternatively...to "knockout" (erase) the double-circles... - "destination-out" replaces the canvas content with the set background. http://jsfiddle.net/d_panayotov/ab21yfgd/ - The holes are blue instead of green.
  • On the other hand... - "source-atop" requires content to be drawn after defining the clipping mask. This in my case would be inefficient (Light is drawn as concentric circles, shaded area still visible).

@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!

Even easier is just to use clipping and complete circles. Unless there's some reason you NEED to do this with a single path.

var cutoutCircle = function(x, y, r, ctx){
  ctx.save()
  ctx.arc(x, y, r, 0, Math.PI * 2, false) 
  ctx.clip()
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
  ctx.restore();
}

var myCircles = [{x: 75, y: 100, r: 50}, {x: 125, y: 100, r: 50}], 
    ctx = document.getElementById("canvas").getContext('2d');

ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
for (var i=0; i<myCircles.length; i++){
  cutoutCircle(myCircles[i].x, myCircles[i].y, myCircles[i].r, ctx)
}

EDIT: added background to example for better a better demonstration

http://jsfiddle.net/v9qven9w/1/

If I understand you correctly: you want to have the appearance of a mask on top of the game, so that the two intersecting circles highlights, while everything else is dimmed?

I would suggest keep it simple - create an off-screen canvas with the circles knocked out on a transparent black background.

Then just draw that off-screen canvas in on top of your game when you need it. This is far more performant than to re-composite for each frame - do it one time and reuse.

Demo

The mask is shown in the demo window below (scroll it or use full page to see all). Normally you would create an off-screen canvas instead and use that.

 // create mask // for off-screen, use createElement("canvas") var mask = document.getElementById("mask"), ctxm = mask.getContext("2d"), w = mask.width, h = mask.height, x, y, radius = 80; ctxm.fillStyle = "rgba(0,0,0,0.5)"; ctxm.fillRect(0, 0, w, h); // fill mask with 50% transp. black ctxm.globalCompositeOperation = "destination-out"; // knocks out background ctxm.fillStyle = "#000"; // some solid color x = w / 2 - radius/1.67; y = h / 2; ctxm.moveTo(x, y); // circle 1 ctxm.arc(x, y, radius, 0, Math.PI*2); x = w / 2 + radius/1.67; ctxm.moveTo(x, y); // circle 2 ctxm.arc(x, y, radius, 0, Math.PI*2); ctxm.fill(); // knock em' out, DONE! // ----- Use mask for the game, pseudo action below ------ var canvas = document.getElementById("game"), ctx = canvas.getContext("2d"); (function loop() { ctx.fillStyle = "#742"; ctx.fillRect(0, 0, w, h); // clear background ctx.fillStyle = "#960"; for(x = 0; x < w; x += 8) // some random action ctx.fillRect(x, h * Math.random(), 8, 8); ctx.drawImage(mask, 0, 0); // use MASK on top requestAnimationFrame(loop) })(); 
 <canvas id="mask" width=500 height=220></canvas> <canvas id="game" width=500 height=220></canvas> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM