HTML5的画布中带有中间孔的多边形

16

使用<canvas>标签,我需要能够在多边形中画一个洞。

现在我有一些非常简单的东西,它使用beginPath()方法,然后对每个点做lineTo()。然后用fill()填充。

但是,我无法想到任何一种方法来填充多边形并留出未填充的中间区域,就像一个甜甜圈一样。 虽然我不是在制作甜甜圈,但这个例子很适合说明问题。

是否有什么我遗漏的地方? 我宁愿不完全填充然后重新绘制中间部分。


1
为什么你不想做一个甜甜圈呢?甜甜圈可是很棒的!请参考此链接以绘制形状:https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes - jazzytomato
我同意甜甜圈值得一画。这个链接:http://www.w3schools.com/tags/ref_canvas.asp也很有用。 - GameAlchemist
2
仅供澄清,我正在绘制一个具有数百个点的复杂多边形。甜甜圈只是在多边形中间有一个洞的比喻。 - Nick
4个回答

34

这是我让它工作的方法:

var ctx = canvas.getContext("2d");     
ctx.beginPath();

//polygon1--- usually the outside polygon, must be clockwise
ctx.moveTo(0, 0);
ctx.lineTo(200, 0);
ctx.lineTo(200, 200);
ctx.lineTo(0, 200);
ctx.lineTo(0, 0);
ctx.closePath();

//polygon2 --- usually hole,must be counter-clockwise 
ctx.moveTo(10, 10);
ctx.lineTo(10,100);
ctx.lineTo(100, 100);
ctx.lineTo(100, 10);
ctx.lineTo(10, 10);
ctx.closePath();

//  add as many holes as you want
ctx.fillStyle = "#FF0000";
ctx.strokeStyle = "rgba(0.5,0.5,0.5,0.5)";
ctx.lineWidth = 1;
ctx.fill();
ctx.stroke();

这里的主要思想是只能使用一次beginPath;外部多边形必须是顺时针方向,孔洞必须是逆时针方向。


它的功能非常完美,但我现在想知道是否有可能按顺时针或逆时针顺序排序点。我找到了一些算法,但它们都不能处理非凸多边形。 - Zoran Marjanovic
1
在我看来,外部多边形的方向不一定要顺时针,孔的方向也不一定要逆时针 - 它们只需要相反即可。至少在 Chrome 中,上下文假定第一个输入多边形的方向是“正确”的。 - Marcin Kaczmarek
如果有人渲染 GeoJSON 多边形,而无需重新排序点,那么它可以正常工作。我猜编辑器已经适当地重新排序了它们。 - culebrón

32

使用HTML5画布绘制孔洞的方法有两种选择。

选择1:
以不同的时钟方向绘制外部形状和内部形状。

外部形状为顺时针方向,内部形状为逆时针方向。
或者外部形状为逆时针方向,内部形状为顺时针方向。

ctx.beginPath();
//outer shape, clockwise
ctx.moveTo(100,20);
ctx.lineTo(200,200);
ctx.lineTo(20,200);
ctx.closePath();
//inner shape (hole), counter-clockwise
ctx.moveTo(100,100);
ctx.lineTo(70,170);
ctx.lineTo(140,170);
ctx.closePath();
//fill
ctx.fillStyle = "#FF0000";
ctx.fill();

在编程时,检测形状绘制方向有些麻烦。

如果您需要检测一系列点是否为顺时针方向,请使用以下好用的函数:

function isClockwise(dots){
    var sum = 0;
    for(var i=1, n=dots.length; i<n; i++){
        sum += (dots[i][0] - dots[i-1][0]) * (dots[i][1] + dots[i-1][1]);
    }
    sum += (dots[0][0] - dots[n-1][0]) * (dots[0][1] + dots[n-1][1]);
    return sum < 0;
}
console.log(isClockwise([[100,20], [200,200], [20,200]]));  //true
console.log(isClockwise([[100,20], [20,200], [200,200]]));  //false

如果您的点是顺时针的,但您需要逆时针,请.reverse()您的点数组。

var dots = [[100,20], [200,200], [20,200]];
dots.reverse();
选择2:
使用“evenodd”填充规则,在任何方向上绘制您的形状。
这种方式比选择1简单得多。
请参见fill()方法 API
   void ctx.fill();
   void ctx.fill(fillRule);
   void ctx.fill(path, fillRule);

fillRule 可以是 "nonzero""evenodd"
"nonzero": 非零环绕规则,也是默认规则。
"evenodd": 奇偶环绕规则。

ctx.beginPath();
//outer shape, any direction, this sample is clockwise
ctx.moveTo(100,20);
ctx.lineTo(200,200);
ctx.lineTo(20,200);
ctx.closePath();
//inner shape (hole), any direction, this sample is clockwise
ctx.moveTo(100,100);
ctx.lineTo(140,170);
ctx.lineTo(70,170);
ctx.closePath();
//fill
ctx.fillStyle = "#FF0000";
ctx.mozFillRule = 'evenodd'; //for old firefox 1~30
ctx.fill('evenodd'); //for firefox 31+, IE 11+, chrome

这里输入图片描述


11

你可以使用 奇偶规则 来进行填充:fill('evenodd')

奇偶规则

// properties
// - outer square
var outerLength = 200;
// - inner length
var innerLength = outerLength / 2;

// cnavas
var canvas = document.getElementById('canvas');
var width = canvas.width = document.body.clientWidth;
var height = canvas.height = document.body.clientHeight;
var context = canvas.getContext('2d');

// path
// - outer square
context.rect(
  (width - outerLength) / 2,
  (height - outerLength) / 2,
  outerLength,
  outerLength
);
// - inner square
var x0 = (width - innerLength) / 2;
var y0 = (height - innerLength) / 2;
context.moveTo(x0, y0);
context.rect(
  x0,
  y0,
  innerLength,
  innerLength
);

// draw
// - stroke
context.lineWidth = 10;
context.stroke();
// - fill
context.fillStyle = 'red';
context.fill('evenodd');
html,
body {
  margin: 0;
  height: 100%;
  overflow: hidden;
}
<canvas id="canvas"><canvas>


1

以顺时针方向绘制您的形状(例如)。然后,您可以增加或减小半径并逆时针方向移动。

编辑:这里是一个非常简单的甜甜圈示例:

<canvas id="cvs" width="300" height="300">[No canvas support]</canvas>

<script>
    // Get the context
    context = document.getElementById('cvs').getContext('2d');
    
    // Start a new path
    context.beginPath();
    
    // Draw a circle with a radius of 100 in the
    // clockwise direction
    context.arc(150,150,100,0, 2 * Math.PI, false);
    
    // Now draw another circle with a smaller
    // radius and in the anti-clockwise direction
    context.arc(150,150,60,2 * Math.PI, 0, true);
    
    // Fill the resultant shape
    context.fill();
</script>

这个页面上有结果展示的演示:

https://www.rgraph.net/canvas/reference/arc.html#how-to-draw-a-donut-shape

为了更好地理解它,将context.fill()更改为context.stroke(),并将两个2 * Math.PI都更改为1.5 * Math.PI可能会有所帮助。这使示例更加清晰。
它首先绘制一个弧形,然后反向绘制一个半径较小的第二个弧形 - 并且有一个“连接线”,它是从第一个弧形的末端到第二个弧形的开始隐式绘制的(即不是由代码绘制的)。路径不需要关闭,但这样做也没有坏处。如果您只是描边甜甜圈而不填充它,您可能更喜欢这种方法。

不能只是一个链接,也不能解释为空。 - Aprillion
1
解释在第一句话。 - Richard
1
感谢您发布答案!请务必仔细阅读有关自我推广的FAQ。还请注意,每次链接到您自己的网站/产品时,必须发布免责声明。 - Andrew Barber
不清楚如何以更大的半径缩放和绘制任意多边形的形状? 多边形没有“半径”! 如何缩放这样的多边形? - Aleksey Kontsevich

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接