在 Canvas 实现的应用中,会有这样的一类场景,当点击画布中的某个几何图形时,要触发一些交互操作。但是,Canvas 并不支持为画布内的图形元素添加事件,这需要借助数学知识来解决这个问题。
虽然,Canvas 不支持为画布内的图形元素添加事件,但是我们可以监听 Canvas 元素自身的点击事件,来计算出点击位置在画布上的坐标。
canvas.addEventListener('click', (event) => {
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const point = {
x,
y,
}
console.log(point); // 打印点击位置坐标
})
如果用户想点击某个图像,那么他的点击位置一定会落在图形的内部。为了简化问题,这里将只讨论多边形的场景。判断一个点是否在多边形内,有交叉数 (Crossing Number) 和环绕数 (Winding Number) 两种方法。
交叉数法:以某一点做射线,如果该射线与多边形的边相交的次数为奇数时,则该点在多边形内部,否则在多边形外部。如下图所示:
通常,我们会选择水平向右方向的射线(x 轴正方向),下面是代码的实现:
function getCrossingNumber(point, lines) {
let count = 0;
for (let i = 0; i < lines.length; i += 1) {
// o, d 是多边形某条边的起点和终点
const { o, d } = lines[i];
// 起点和终点位于水平射线的两侧才会有交点
if ((o.y > point.y) ^ (d.y > point.y)) {
// x = (y - y0) / k + x0
const x = (point.y - o.y) * (d.x - o.x) / (d.y - o.y) + o.x;
if (x > point.x) {
count += 1;
}
}
}
return count;
}
Fabric.js 这个库就是使用交叉数法来判断一个点是否在多边形内,具体可以查看代码 https://github.com/fabricjs/fabric.js/blob/8ecbdb10f797ce07fb4dccca348fe63ff1558b62/dist/fabric.js#L16499
交叉数法在某些场景下,得出的结果会出现错误,相对来说,环绕数法会更准确一些。
环绕数法:以某一点做水平向右的射线,如果多边形的某条边的从下往上穿过该射线,则环绕数加一;如果多边形的某条边的从上往下穿过该射线,则环绕数减一;最终的环绕数如果不为 0 则该点在多边形内部,否则在多边形的外部。如下图所示:
对于人来说,判断从上往下(或从下往上)穿过射线相对来说比较容易,在代码实现上会有一些复杂,因此需要将这种上下关系转换为左右关系。如下图所示,如果一条边向上穿过射线,那么 P 点在边 AB 的左侧;而对于一条向下的边,P 点在边 AB 的右侧。
使用右手定则,当 P 点在 AB 的左侧时, AB 和 AP 的法向量向外;当 P 点在 AB 的右侧时, AB 和 AP 的法向量向里。AB 和 AP 的法向量可以通过向量的外积(叉乘)来计算:
向量 AB = (x1, y1, 0)
向量 AP = (x2, y2, 0)
AB × AP = (x1y2 - x2y1)k
其中,k 是 z 轴单位向量,x1y2 - x2y1 的正负代表方向
进而得到以下结果:
通过上面的两个算法,都可以判断出 Canvas 画布上点击的位置是否在某个多边形上,然后触发相关交互即可。其中,环绕数法理解起来需要一些数学知识,代码实现并不复杂。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8