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.
- 矩形的 alpha 值应小于 1.0。
- 显示在 "holes" 中的内容是应用阴影之前在 canvas 上绘制的任何内容。
@markE:
- 或者..."knockout"(擦除)双圆...-
"destination-out" 将 canvas 内容替换为设置的背景。
http://jsfiddle.net/d_panayotov/ab21yfgd/ - 孔是蓝色而不是绿色。
- 另一方面……-
"source-atop" 要求在定义剪贴蒙版后绘制内容。在我的情况下,这将是低效的(光被绘制为同心圆,阴影区域仍然可见)。
@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)
}
编辑:为示例添加背景以便更好地演示
如果我没理解错的话:你想在游戏顶部有一个面具的外观,以便两个相交的圆圈突出显示,而其他一切都变暗?
我建议保持简单 - 创建一个屏幕外的 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>
我的问题很简单。这是 "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.
- 矩形的 alpha 值应小于 1.0。
- 显示在 "holes" 中的内容是应用阴影之前在 canvas 上绘制的任何内容。
@markE:
- 或者..."knockout"(擦除)双圆...- "destination-out" 将 canvas 内容替换为设置的背景。 http://jsfiddle.net/d_panayotov/ab21yfgd/ - 孔是蓝色而不是绿色。
- 另一方面……- "source-atop" 要求在定义剪贴蒙版后绘制内容。在我的情况下,这将是低效的(光被绘制为同心圆,阴影区域仍然可见)。
@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)
}
编辑:为示例添加背景以便更好地演示
如果我没理解错的话:你想在游戏顶部有一个面具的外观,以便两个相交的圆圈突出显示,而其他一切都变暗?
我建议保持简单 - 创建一个屏幕外的 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>