在说多边形网格化之前,我们先画个复杂点的多边形出来。
准备一组用于逆时针绘图的顶点:
const points=[ new Vector2(0,0), new Vector2(0,600), new Vector2(600,600), new Vector2(600,200), new Vector2(200,200), new Vector2(200,400), new Vector2(300,400), new Vector2(300,300), new Vector2(500,300), new Vector2(500,500), new Vector2(100,500), new Vector2(100,100), new Vector2(600,100), new Vector2(600,0)];
基于这组顶点绘制一个多边形:
const poly=new Poly({ position:new Vector2(50,50), stroke:true, close:true, vertices:[...points]});poly.draw(ctx);
思路解析
接下来,我们就说一下网格化思路:
1.先判断points 中的顶点数量,若等于3,那就直接将这三个点存储起来。否则,下一步。
2.把多边形里的第一个点作为起点
3.从起点连接下下个点,构成一个三角形,如▲012
4.对上面的三角形做两个判断:
- 三角形是否包含了其它顶点,如上图▲012
- 三角形是否为凹三角形,如下图▲678
若出现上面任何一种情况,就把起点的下一个点做为起点,执行第1步。
若上面的情况都不存在,如下图▲456,那就把构成这个三角形的顶点存储起来,把下一个点从points 中删除,把下下个点做为起点,执行第1步。
到这里,网格化的思路就说完了,这是一个递归方法,最终效果如下:
接下来,咱们就说一下这整个逻辑中涉及的两个知识点:
1.三角形是否包含了其它顶点。
这个方法我们上一章说过,遍历其它顶点,逐一判断即可,在此便不再赘述。
2.判断三角形的凹凸。
首先我们在定点的时候要遵守一个规则,这里是逆时针定点。
这样我们在用叉乘的方法求三角形的面积的时候,面积为正,那就是凹三角形;面积为负,那就是凸三角形。
求三角形面积的方法:
function triangleArea(p0,p1,p2){ const [bx,by,cx,cy]=[ p1.x-p0.x, p1.y-p0.y, p2.x-p0.x, p2.y-p0.y, ]; return (bx*cy-cx*by)/2;}
这个求面积方法用到的是一个叉乘算法。利用这个算法还可以计算任意边数的多边形面积,详情我写过的另一篇文章:多边形面积
整体代码的实现步骤
1.绘制多边形
const points=[ new Vector2(0,0), new Vector2(0,600), new Vector2(600,600), new Vector2(600,200), new Vector2(200,200), new Vector2(200,400), new Vector2(300,400), new Vector2(300,300), new Vector2(500,300), new Vector2(500,500), new Vector2(100,500), new Vector2(100,100), new Vector2(600,100), new Vector2(600,0)];const poly=new Poly({ position:new Vector2(50,50), stroke:true, close:true, lineWidth:2, vertices:[...points]});poly.draw(ctx);
2.声明用于存储三角形的数组triangles[] 和起点
const triangles=[];let start=0;
3.建立网格
crtMesh(start);function crtMesh(start){ const len=points.length; const [i0,i1,i2]=[ start%len, (start+1)%len, (start+2)%len ]; const [p0,p1,p2]=[ points[i0], points[i1], points[i2], ]; if(len===3){ triangles.push([p0,p1,p2]); }else{ const area=triangleArea(p0,p1,p2); if(area>=0||hasOtherPointInTriangle(p0,p1,p2)){ //凹三角 crtMesh(i1); }else{ //凸三角 triangles.push([p0,p1,p2]); points.splice(i1,1); crtMesh(i1); } }}
len 是points 顶点长度
i0,i1,i2 是通过取余的方法获取三角形顶点的索引,因为网格化可能会对points 循环遍历。
len===3 是如果points 长度为三,就直接将三角形顶点放到triangles 集合里,这也是个终止递归的条件,后面points 会不断删除三角形的第2个顶点,从而不断变小。
area>=0||hasOtherPointInTriangle(p0,p1,p2) 便是思路4中的两个情况:
- 若满足其中之一,就把下一个顶点作为参数,递归执行crtMesh(i1) 方法。
- 若都不满足,就存储三角形,冲points 中删除三角形的第二个顶点,将三角形的第二个顶点作为起点参数,执行crtMesh(i1) 方法。
好了,整个网格化的实现过程就是这样。
要选择多边形的话,那就遍历triangles里的三角形即可,鼠标点只要在一个三角形中,那也就在多边形中了。
最后我把上面的过程进行了封装,建立了一个Mesh 类:
/*Mesh 网格对象* points 所有的顶点* triangles 三角形集合* tmpPoints 顶点的暂存区,用于做递归删除* update() 更新方法,建立网格* crtMesh(start) 建立网格的方法* hasOtherPointInTriangle(p0,p1,p2) 判断▲p0p1p2是否包含了其它顶点* triangleArea(p0,p1,p2) ▲p0p1p2的面积* inMesh(v) 点v是否在多边形中* crtPolys() 根据三角形集合建立多边形集合* draw(ctx) 绘图* */export default class Mesh{ constructor(points=[]) { this.points=points; this.triangles=[]; this.tmpPoints=[]; this.update(); } update(){ this.tmpPoints=[...this.points]; this.crtMesh(0); } crtMesh(start){ const points=this.tmpPoints; const len=points.length; const [i0,i1,i2]=[ start%len, (start+1)%len, (start+2)%len ]; const [p0,p1,p2]=[ points[i0], points[i1], points[i2], ]; if(points.length===3){ this.triangles.push([p0,p1,p2]); }else{ const area=this.triangleArea(p0,p1,p2); if(area>=0||this.hasOtherPointInTriangle(p0,p1,p2)){ //凹三角 this.crtMesh(i1); }else{ //凸三角 this.triangles.push([p0,p1,p2]); points.splice(i1,1); this.crtMesh(i1); } } } hasOtherPointInTriangle(p0,p1,p2){ for (let ele of this.tmpPoints){ if(ele!==p0&&ele!==p1&&ele!==p2){ if (ele.inTriangle(p0,p1,p2)){ return true; } } } return false; } triangleArea(p0,p1,p2){ const [bx,by,cx,cy]=[ p1.x-p0.x, p1.y-p0.y, p2.x-p0.x, p2.y-p0.y, ]; return (bx*cy-cx*by)/2; } inMesh(v){ const len=this.triangles.length; for(let ind=0;ind<len;ind++){ const triangle=this.triangles[ind]; if(v.inTriangle(...triangle)){ return {ind,triangle}; } } return null; } crtPolys(option){ this.triangles.forEach((ele,ind)=>{ option.vertices=ele; const poly=new Poly(option); this.polys.push(poly); }); } draw(ctx){ this.polys.forEach((poly,ind)=>{ poly.draw(ctx); }) } }
网格实例化和绘制方法:
const mesh=new Mesh(points);mesh.crtPolys({ position:new Vector2(50,50), stroke:true, close:true,});mesh.draw(ctx);
图形选择方法:
canvas.addEventListener('mousemove',mousemoveFn);let hover=false;function mousemoveFn(event){ const mousePos=getMousePos(event,poly); const obj=mesh.inMesh(mousePos); const bool=!!obj; if(hover!==bool){ poly.fill=bool; ctx.clearRect(0,0,canvas.width,canvas.height); poly.draw(ctx); mesh.draw(ctx); hover=bool; }}
效果:
[源码地址](https://github.com/buglas/interview-01)