Javascript画布 - 在矩形中交叉圆孔或如何合并多个弧形路径

时间:2015-04-01 15:53:41

标签: javascript canvas path clipping

我遇到的问题非常简单。这是"我如何画一个形状的孔?"问题,经典答案是"简单地在同一条路径中绘制两个形状,但顺时针绘制实心和"孔"逆时针&#34。这很棒,但是"洞"我需要的是复合形状,由多个圆圈组成。

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

  • 矩形应该能够使alpha小于1.0。
  • 内容,显示在"孔"是应用阴影之前在画布上绘制的内容。

@markE:

  • 或者......到"淘汰赛" (擦除)双圈... - "目的地出"用设置的背景替换画布内容。 http://jsfiddle.net/d_panayotov/ab21yfgd/ - 洞是蓝色而不是绿色。
  • 另一方面...... - "源极 - 顶上"要求在定义剪贴蒙版后绘制内容。在我的情况下这将是低效的(光被绘制为同心圆,阴影区域仍然可见)。

@hobberwickey: 这是一个静态背景,而不是实际的画布内容。然而,我可以使用clip(),就像我使用的那样#34; source-atop"但那效率很低。

我现在实施的解决方案:http://jsfiddle.net/d_panayotov/ewdyfnj5/。我只是在主画布内容上绘制剪切的矩形(在内存中的画布中)。有没有更快/更好的解决方案?

3 个答案:

答案 0 :(得分:2)

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

enter image description here

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>

或者......“敲掉”(擦除)双圈......

如果你想让2个圆圈“击倒”蓝色像素,那么双圆圈是透明的&amp;揭示下面的网页背景,然后你可以使用合成来“淘汰”圈子:context.globalCompositeOperation='destination-out

enter image description here

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>

另一方面......

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

这是另一个例子:

enter image description here

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像素的alphas将被取消归零,rgb将再次显示。最好使用'destination-out'合成,其中像素值(r,g,b,a)的所有部分都被清零,这样它们就不会意外地返回困扰你。

确保... 尽管在您的示例中并不重要,但您应始终使用maskCtx.beginPath()开始路径绘制命令。这表示任何先前绘图的结束和新路径的开始。

一个选项:我看到你正在使用同心圆在你圈子的中心引起更大的“揭示”。如果你想要一个更渐进的显示,那么你可以用剪切阴影(或径向渐变)而不是同心圆来淘汰你的记忆中的圆圈。

除此之外,覆盖内存中画布的解决方案应该运行良好(以内存中画布使用的内存为代价)。

祝你好运!

答案 1 :(得分:0)

更简单的方法就是使用剪辑和完整的圆圈。除非出于某种原因,你需要用一条路径来做这件事。

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/

答案 2 :(得分:0)

如果我理解正确的话:你想在游戏顶部看到一个面具的外观,这样两个相交的圆圈突出显示,而其他一切都变暗了?

我建议保持简单 - 创建一个离屏画布,在透明的黑色背景上敲出圆圈。

然后在需要时在游戏顶部绘制离屏画布。这比为每个帧重新组合要高得多 - 做一次并重复使用。

演示

掩码显示在下面的演示窗口中(滚动它或使用整页查看全部)。通常你会创建一个离屏画布并使用它。

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