引言
继续巩固我的可视化学习,向量运算是计算机图形学的基础,本例依旧是向量的一种应用,利用向量判断多边形边界,但是多边形的边界判断稍微有点复杂,所以除了应用向量之外,还需要借助三角剖分的相关工具。这个例子中可视化的展示采用Canvas2D来实现。
问题
假设Canvas画布上存在一个如下多边形:
我们移动鼠标的时候,想要实现一个效果,就是当鼠标移动到多边形内部的时候,将多边形内部的填充颜色更新成其他颜色;所以此时我们需要判断鼠标是否在多边形内部,这就涉及到多边形边界的判断。
思路
首先我们先将这个多边形绘制到Canvas画布上。
<canvas width="512" height="512"></canvas>
canvas {
width: 512px;
height: 512px;
border: 1px solid #eee;
}
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.scale(1, -1);
const vertices = [
[ -179.2, 128 ],
[ -102.4, 76.8 ],
[ -64, 181.76 ],
[ -25.6, 143.36 ],
[ -25.6, 33.28 ],
[ 102.4, 53.76 ],
[ 0, -153.6 ],
[ -76.8, -76.8 ],
[ -153.6, -76.8 ],
[ -115.2, 0 ]
];
drawPolygon(vertices);
function drawPolygon(vertices, fillStyle = "red") {
ctx.beginPath();
ctx.moveTo(...vertices[0]);
for (let i = 1; i < vertices.length; i ++) {
ctx.lineTo(...vertices[i]);
}
ctx.closePath();
ctx.fillStyle = fillStyle;
ctx.fill();
}
1. 调用API
对于Canvas2D而言,有一个API自带的方法,就是CanvasRenderingContext2D的isPointInPath方法。
这个方法使用起来非常简单,我们在这个时候直接增加一个鼠标移动事件的监听就可以。
const {left, top} = canvas.getBoundingClientRect();
canvas.addEventListener('mousemove', e => {
const {x: pageX, y: pageY} = e;
// 坐标转化
const offsetX = x - left;
const offsetY = y - top;
// 清除画布
ctx.clearRect(-256, -256, 512, 512);
if (ctx.isPointInPath(offsetX, offsetY)) {
drawPolygon(vetices, "green");
} else {
drawPolygon(vetices);
}
});
但是这个API的使用存在很大的局限性,就是它只能针对当前绘制的图形生效。
就比如说,如果在完成这个多边形的绘制之后,又绘制了一个小三角形。
const triangle = [
[100, 100],
[100, 200],
[150, 200]
];
drawPolygon(triangle, "blue");
为了保持这个小三角形,我们还需要修改鼠标监听事件,以达到更新画布时,三角形依旧被绘制。
canvas.addEventListener('mousemove', e => {
const {pageX: x, pageY: y} = e;
// 坐标转化
const offsetX = x - left;
const offsetY = y - top;
// 清除画布
ctx.clearRect(-256, -256, 512, 512);
if (ctx.isPointInPath(offsetX, offsetY)) {
drawPolygon(vertices, "green");
drawPolygon(triangle, "blue");
} else {
drawPolygon(vertices);
drawPolygon(triangle, "blue");
}
});
此时我们再移动鼠标,就会发现,在鼠标移动到多边形内部时,多边形的填充颜色并不会变,但是当鼠标移动到小三角形内部时,多边形的填充色发生了变化;这就是Canvas2D Context的isPointInPath方法所存在的局限性。
2. 自定义isPointInPath
为了突破Canvas2D API中自带方法的局限性,最简单的方法就是,我们手动自定义一个自己的isPointInPath方法。
具体实现如下:
function isPointInPath(x, y) {
// 根据ctx重新clone一个新的Canvas对象
const cloned = ctx.canvas.cloneNode().getContext('2d');
cloned.translate(canvas.width / 2, canvas.height / 2);
cloned.scale(1, -1);
let ret = false;
// 绘制多边形,判断点是否在图形内部
drawPolygon(cloned, vertices, "red");
ret |= cloned.isPointInPath(x, y);
if (!ret) {
// 如果不在,继续绘制小三角形,判断点是否在图形内部
drawPolygon(cloned, triangle, "blue");
ret |= cloned.isPointInPath(x, y);
}
return ret;
}
- 首先,根据原画布的Context创建一个新的Canvas对象并获取它的上下文
- 然后绘制多边形,并判断鼠标是否在多边形内部
- 如果不在多边形内部,继续判断是否在三角形内部
- 最后将结果返回
可以看到,在这个自定义的方法内部,我们依然是调用了Canvas2D Context的isPointInPath方法。
接着我们还需要修改鼠标的监听事件,把判断方法改为我们自定义的isPointInPath。
此时移动鼠标,可以看到,当鼠标移动到多边形或者三角形内部,都可以使多边形的填充色发生变化;这就是因为我们在自定义的isPointInPath中做的两次判断。
但是我们也能发现,虽然这种方式解决了我们在第一种方式中所碰到的问题,却也存在其他问题,第一,是增加了很多无谓的Canvas绘图操作;第二,是通用性差,如果图形有修改,那么isPointInPath方法就要跟着修改,并且这个方法依赖于Canvas2D的API,如果哪天修改了绘图方式,比如改为使用WebGL,就不能使用了。