canvas绘制多边形,并判断某个点是否在区域内

看过前面文章的朋友估计知道,我写了个canvas的Ycc库,库里面的UI容器全部使用长方形来表示的。各个UI虽然可以重载containDot方法来判断某个点是否在区域内,但是作为底层库,怎么能不支持不规则图形呢?于是就有了这篇文章,专门介绍canvas绘制多边形,并且判断某个点是否在区域内,这样的不规则图形UI才能很方便的响应舞台的事件。

一、绘制多边形

三角形

canvas绘制多边形其实很简单,只需要调用canvas自带的一系列api即可,如下便是绘制一个三角形的代码:

var coordinates = [
   {x:50,y:100},
   {x:250,y:100},
   {x:100,y:150},
];
var start = coordinates[0];
ctx.save();
ctx.fillStyle = "red";
ctx.beginPath();
ctx.moveTo(start.x,start.y);
for(var i=0;i<coordinates.length;i++){
   var dot = coordinates[i];
   ctx.lineTo(dot.x,dot.y);
}
ctx.closePath();
ctx.fill();
ctx.restore();

效果如下:

image.png

代码中最需要理解的东西就是路径了,beginPath()路径开始,closePath()路径闭合,最后再使用fill()方法填充路径,这个三角形就绘制好了。

五角星

上面这段代码是一个最简单的多边形绘制,试想,如果多边形的边有重叠的情况,又会发生什么呢?比如,一个五角星。

我们稍微修改一下上面代码如下:

var coordinates = [
   {x:50,y:100},
   {x:250,y:100},
   {x:100,y:150},
   {x:150,y:50},
   {x:200,y:150},
];
var start = coordinates[0];
ctx.save();
ctx.strokeStyle = "blue";
ctx.beginPath();
ctx.moveTo(start.x,start.y);
for(var i=0;i<coordinates.length;i++){
   var dot = coordinates[i];
   ctx.lineTo(dot.x,dot.y);
}
ctx.closePath();
ctx.stroke();
ctx.restore();

为了看见效果,这里改用stroke()绘制图形的轮廓,如下:

image.png

图上的箭头标注了路径的行动轨迹,其实就是平常我们没事干随手画的五角星啦。

看到这里,我们来思考一个问题。

对于三角形,我们很容易的就能清楚的知道哪些点在三角形内部,哪些点在外部。

而对于下面这个五角星,五角星最里边的五边形区域,你觉得是在五角星内部,还是五角星外部呢?这便是我们下面需要讨论的问题。

二、判断某个点在多边形内部

问题假设:有一个点P,有一个多边形A,我们要判断A是否包含P。

基础知识–光线投射法

原理:

1、从点P出发,任意引一条射线(模拟光线)。

2、记录该条射线与多边形A的边相交点的个数。

3、判断交点的个数,若为偶数表示在图形外,若为奇数表示在图像内。

这里直接把我写的JS代码贴出来供大家参考

/**
 * @param dot {{x,y}} 需要判断的点
 * @param  coordinates {{x,y}[]} 多边形点坐标的数组,为保证图形能够闭合,起点和终点必须相等。
 *        比如三角形需要四个点表示,第一个点和最后一个点必须相同。 
 */
function judge(dot,coordinates) {
   var x = dot.x,y=dot.y;
   var crossNum = 0;
   for(var i=0;i<coordinates.length-1;i++){
      var start = coordinates[i];
      var end = coordinates[i+1];
      
      // 起点、终点斜率不存在的情况
      if(start.x===end.x) {
         // 因为射线向右水平,此处说明不相交
         if(x>start.x) continue;
         
         if((end.y>start.y&&y>=start.y && y<=end.y) || (end.y<start.y&&y>=end.y && y<=start.y)){
            crossNum++;
         }
         continue;
      }
      // 斜率存在的情况,计算斜率
      var k=(end.y-start.y)/(end.x-start.x);
      // 交点的x坐标
      var x0 = (y-start.y)/k+start.x;
      // 因为射线向右水平,此处说明不相交
      if(x>x0) continue;
      
      if((end.x>start.x&&x0>=start.x && x0<=end.x) || (end.x<start.x&&x0>=end.x && x0<=start.x)){
         crossNum++;
      }
   }
   
   return crossNum%2===1;
};

上面这种方法,对于大多数多边形来说没有问题,问题就在于多边形边相交的情况,比如我们上面的五角星。

对于五角星采用上面的方法,最中心的区域是不会被归为图形内的。这个时候就需要对判断方法进行改变了。

光线投射法【升级版】

原理:

1、从点P出发,任意引一条射线(模拟光线)。

2、该条射线与多边形A的边相交时,若射线从边的左侧贯穿记录leftCount加1,若射线从边的右侧贯穿记录rightCount加1

3、若leftCount-rightCount等于0表示在图形外部,若不等于0表示图形内部

这种模式在另一篇文章中称之为“None Zero Mode”,参考文章https://www.cnblogs.com/guogangj/p/5127527.html

下面也把我对于这种版本的JS代码贴出来,伸手党福利哟~

/**
 * @param  dot {{x,y}} 需要判断的点
 * @param  coordinates {{x,y}[]} 多边形点坐标的数组,为保证图形能够闭合,起点和终点必须相等。
 *        比如三角形需要四个点表示,第一个点和最后一个点必须相同。 
 * @param  
 */
function judge(dot,coordinates,noneZeroMode) {
   // 默认启动none zero mode
   noneZeroMode=noneZeroMode||1;
   var x = dot.x,y=dot.y;
   var crossNum = 0;
   // 点在线段的左侧数目
   var leftCount = 0;
   // 点在线段的右侧数目
   var rightCount = 0;
   for(var i=0;i<coordinates.length-1;i++){
      var start = coordinates[i];
      var end = coordinates[i+1];
      
      // 起点、终点斜率不存在的情况
      if(start.x===end.x) {
         // 因为射线向右水平,此处说明不相交
         if(x>start.x) continue;
         
         // 从左侧贯穿
         if((end.y>start.y&&y>=start.y && y<=end.y)){
            leftCount++;
            crossNum++;
         }
         // 从右侧贯穿
         if((end.y<start.y&&y>=end.y && y<=start.y)) {
            rightCount++;
            crossNum++;
         }
         continue;
      }
      // 斜率存在的情况,计算斜率
      var k=(end.y-start.y)/(end.x-start.x);
      // 交点的x坐标
      var x0 = (y-start.y)/k+start.x;
      // 因为射线向右水平,此处说明不相交
      if(x>x0) continue;
      
      if((end.x>start.x&&x0>=start.x && x0<=end.x)){
         crossNum++;
         if(k>=0) leftCount++;
         else rightCount++;
      }
      if((end.x<start.x&&x0>=end.x && x0<=start.x)) {
         crossNum++;
         if(k>=0) rightCount++;
         else leftCount++;
      }
   }
   
   return noneZeroMode===1?leftCount-rightCount!==0:crossNum%2===1;
}

这段代码对基础版的光线投射法做了升级,加入了noneZeroMode参数,兼容了基础版本的光线投射及升级版的光线投射算法。

三、示例与总结

说了这么多,没有实际的例子,大家也难以理解,下面就上一个我写的示例吧。

两个五角星闪亮登场bling~bling~

五角星示例 https://www.lizhiqianduan.com/products/ycc/examples/polygon-of-star/

多边形相对来说算是比较复杂的图形了,因为计算机里的所有图形都可以使用多边形表示,包括圆、椭圆。

这篇文章只是简单的介绍多边形,很多其他的特性有待大家一起开发与学习。

下期预告

既然我们的Ycc底层库支持多边形了,那么我们就来实践一下。

下篇文章计划使用多边形元素来绘制一个中国地图,敬请期待。

(完)



打赏作者

发表评论

电子邮件地址不会被公开。 必填项已用*标注