Javascript canvas - 矩形中的相交圆孔或如何合并多个圆弧路径

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

我的问题很简单。这是 "How can I draw a hole in a shape?" 问题的变体,经典答案是 "Simply draw both shapes in the same path, but draw the solid clockwise and the "hole“逆时针”。这很好,但我需要的 "hole" 通常是一个复合形状,由多个圆圈组成。

视觉描述: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();

编辑:

已经提出了多种解决方案,我觉得我的问题有误导性。所以这里有更多信息: 我需要矩形区域作为阴影。这是我正在制作的游戏的屏幕截图(希望这不违反规则):http://i.imgur.com/tJRjMXC.png.

@markE:

@hobberwickey: 那是静态背景,不是实际的 canvas 内容。但是,我可以像使用 "source-atop" 一样使用 clip() ,但那样效率很低。

我目前实施的解决方案:http://jsfiddle.net/d_panayotov/ewdyfnj5/。我只是在 canvas 主要内容上绘制了裁剪矩形(在内存 canvas 中)。有faster/better解决方案吗?

我几乎不敢发布这个答案的第一部分,因为它很简单,但为什么不在纯色背景上填上 2 个圆圈呢?

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>

或者..."knockout"(擦除)双圆...

如果您希望 2 个圆圈 "knockout" 蓝色像素向下,以便双圆圈透明并显示下面的网页背景,那么您可以对 "knockout" 圆圈使用合成: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>

另一方面...

如果需要将那些双圆像素隔离为包含路径,则可以使用合成绘制到双圆中,而不绘制到蓝色背景中。

这是另一个例子:

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>

{ 给出补充答案的额外想法}

技术要点: xor 合成通过仅翻转像素上的 alpha 值来工作,但不会同时将像素的 r、g、b 部分清零像素。在某些情况下,xored 像素的 alpha 将被取消归零并且 rgb 将再次显示。最好使用 'destination-out' 合成,其中像素值 (r,g,b,a) 的所有部分都归零,这样它们就不会意外 return 困扰你。

确保... 即使在您的示例中它并不重要,您也应该始终以 maskCtx.beginPath() 开始您的路径绘制命令。这标志着任何先前绘图的结束和新路径的开始。

一个选项:我看到你使用同心圆在你的圆心造成更大的"reveal"。如果你想要更渐进的显示,那么你可以用裁剪阴影(或径向渐变)而不是同心圆来去除内存中的圆。

除此之外,覆盖内存 canvas 的解决方案应该可以正常工作(以用于内存 canvas 的内存为代价)。

祝你游戏顺利!

更简单的是使用剪裁和完整的圆圈。除非出于某种原因,您需要使用一条路径来执行此操作。

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)
}

编辑:为示例添加背景以便更好地演示

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

如果我没理解错的话:你想在游戏顶部有一个面具的外观,以便两个相交的圆圈突出显示,而其他一切都变暗?

我建议保持简单 - 创建一个屏幕外的 canvas,在透明的黑色背景上去掉圆圈。

然后在需要时将屏幕外 canvas 绘制到游戏顶部即可。这比为每一帧重新合成要高效得多 - 做一次并重复使用。

演示

面具显示在下面的演示 window 中(滚动它或使用整页查看全部)。通常你会创建一个屏幕外 canvas 并使用它。

// 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>