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

Posted

技术标签:

【中文标题】Javascript画布 - 矩形中的相交圆孔或如何合并多个圆弧路径【英文标题】:Javascript canvas - intersecting circle holes in rectangle or how to merge multiple arc paths 【发布时间】:2015-06-06 08:53:10 【问题描述】:

我遇到的问题非常简单。这是“如何在形状上画一个洞?”的变体。问题,经典答案是“只需在同一条路径上绘制两个形状,但顺时针绘制实体,逆时针绘制“孔”。这很好,但我需要的“洞”通常是一个复合形状,由多个圆圈组成。

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

矩形应该能够具有小于 1.0 的 alpha。 “孔”中显示的内容是应用阴影之前在画布上绘制的内容。

@markE:

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

@hobberwickey: 那是一个静态背景,而不是实际的画布内容。但是,我可以像使用“source-atop”一样使用 clip(),但这会效率低下。

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

【问题讨论】:

用一个简单的例子更新了我的问题。 我认为你必须在同一路径上制作一个包含两个方舟的形状...... 寻找圆-圆交点和计算复合周长的复杂性超过了整个项目的复杂性。在确定这是唯一的选择之前,我不想重新发明***。 鉴于您对问题的补充,我在回答中添加了一些想法。祝你的项目好运! :-) 【参考方案1】:

我几乎害怕发布这个答案的第一部分,因为它很简单,但为什么不在纯色背景上填充 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; 
#canvasborder:1px solid red;
<canvas id="canvas" width=400 height=168></canvas>

或者...“淘汰”(擦除)双圈...

如果您希望 2 个圆圈向下“剔除”蓝色像素,使双圆圈透明并显示下方的网页背景,那么您可以使用合成“剔除”这些圆圈: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; 
#canvasborder: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; 
#canvasborder:1px solid red;
<canvas id="canvas" width=400 height=168></canvas>

除了答案之外的其他想法

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

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

一种选择:我看到您正在使用同心圆来在您的圆圈中心产生更大的“显示”。如果您想要更渐进的显示,那么您可以使用剪切阴影(或径向渐变)而不是同心圆来淘汰内存中的圆圈。

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

祝你的游戏好运!

【讨论】:

首先感谢您的宝贵时间和广泛的回答!我目前正在撰写回复,因为我有一些问题。我最初认为destination-out 正是我需要的解决方案。 感谢您的建议。将用“destination-out”替换“xor”。老实说,我更喜欢圆形而不是径向渐变。 我在第二个例子中添加了一个径向渐变来模拟那些想要工作的人的褪色效果:jsfiddle.net/48dhmovg【参考方案2】:

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

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/

【讨论】:

@hobberwickey...剪辑在逻辑上更容易理解,但与使用合成相比,context.clip 是一项昂贵的操作。 ;-) 是的,但优化并不总是必要的。如果你要剪掉 3 或 4 个圆圈,而整个过程需要 1 毫秒才能运行,那为什么要优化? 是的...在这种简单的情况下,剪裁很好。 :-) 即使对于简单的情况,我也倾向于在编写代码时考虑到优化……它只是强化了我心中的“最佳实践”。 ;-p【参考方案3】:

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

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

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

演示

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

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

【讨论】:

感谢您的好主意!这可能是真正有益的。不幸的是,当玩家移动时需要更新灯光,所以在我的情况下,这种“缓存”仅在玩家“火炬”熄灭时才适用。如果我在 html 中定义蒙版画布会有什么不同吗? @DeanPanayotov html/css 也是完全有效的(在许多情况下它可以更快),只要你得到你想要的结果(希望它可以在不同的浏览器上运行良好) /系统)。 @DeanPanayotov 您还可以为每个场景使用多个掩码,或者在实际需要时更新一个掩码。无论如何,祝你的游戏好运! :)

以上是关于Javascript画布 - 矩形中的相交圆孔或如何合并多个圆弧路径的主要内容,如果未能解决你的问题,请参考以下文章

画布中的线条和矩形

直线与矩形相交的特例

画布中的矩形大小错误

Android使用ontouch在画布上绘制矩形

画布 canvas 的相关内容

HTML5 Canvas - 如何在画布中的图像上绘制矩形