阅读 171

Canvas 中判断点是否在图形上

在 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) 两种方法。

交叉数法

交叉数法:以某一点做射线,如果该射线与多边形的边相交的次数为奇数时,则该点在多边形内部,否则在多边形外部。如下图所示:

代码实现如下所示:

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 这个库就是使用交叉数法来判断一个点是否在多边形内,具体可以查看代码 github.com/fabricjs/fa…

交叉数法在某些场景下,比如几何图形内部有重叠的时候,得出的结果会出现错误。相对来说,环绕数法会更准确一些。

环绕数法

环绕数法:以某一点做水平向右的射线,如果多边形的某条边的从下往上穿过该射线,则环绕数加一;如果多边形的某条边的从上往下穿过该射线,则环绕数减一;最终的环绕数如果不为 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 的正负代表方向 复制代码

进而得到以下结果:

  • 当 x1y2 - x2y1 > 0 时,P 在 AB 左侧,环绕数 +1

  • 当 x1y2 - x2y1 = 0 时,P 在 AB 上,环绕数不变

  • 当 x1y2 - x2y1 < 0 时,P 在 AB 右侧,环绕数 -1

通过上面的两个算法,都可以判断出 Canvas 画布上点击的位置是否在某个多边形上,然后触发相关交互即可。其中,环绕数法理解起来需要一些数学知识,但是代码实现并不复杂。上面提到了用 交叉数法环绕数 来判断某个点是否在几何图形内。除了这两种方法外,下面将介绍另外一种基于像素的颜色值的检测方案。

像素检测

当用户点击 Canvas 画布时,通过点击位置的像素可判断是否点击到某个图形上。例如,在下图中矩形区域的颜色是橘黄色,而矩形区域外面的颜色是透明的;如果计算出某点 P 的颜色是橘黄色,我们就可以推断出 P 点是落在了矩形上。

上面介绍的是理想情况,实际情况要比这复杂得多。比如下图中,当 Canvas 渲染的图形颜色都一样时,这就没有办法做判断了。

在大多数的 Canvas 库中(比如 Fabric.js、Create.js)通常会将图形的属性、状态等数据存储于内存中,在下次渲染时直接取出来即可使用。同理,有了这些数据,我们也可以在一个新的 Canvas 中完成图形绘制,并且通过额外的一些处理来检测某点是否落在了图形上。我们在这里介绍两种不同的检测方法,其中一种是基于透明度做检测,另外一种是使用随机颜色做检测。

基于透明度的检测

这种检测方案是将每个图形依次在新的 Canvas 渲染,由于新的 Canvas 的背景是完全透明的,只需要对检测位置的透明度做出判断即可,如果被检测的点透明度不为 0 则表示该点命中了这个图形。下图中左侧的 Canvas 是用于正常展示给用户的画布,右侧的 Canvas 是在检测时创建的画布。

Create.js 就是使用的这种透明度对比的方法,来判断某个点是否在某个图形上。这里把 Create.js 的一些代码展示出来,以方便大家的理解。

/**  * Tests whether the display object intersects the specified point in local coordinates (ie. draws a pixel with alpha > 0 at  * the specified position). This ignores the alpha, shadow, hitArea, mask, and compositeOperation of the display object.  *   * Please note that shape-to-shape collision is not currently supported by EaselJS.  * @method hitTest  * @param {Number} x The x position to check in the display object's local coordinates.  * @param {Number} y The y position to check in the display object's local coordinates.  * @return {Boolean} A Boolean indicating whether a visible portion of the DisplayObject intersect the specified  * local Point. */ p.hitTest = function(x, y) {   // DisplayObject._hitTestContext 是一个新的 Context   var ctx = DisplayObject._hitTestContext;   // 做矩阵变换,将检测的点移到 (0, 0) 的位置   ctx.setTransform(1, 0, 0, 1, -x, -y);         // 在新的 ctx 上绘制出图形   this.draw(ctx, !(this.bitmapCache && !(this.bitmapCache._cacheCanvas instanceof WebGLTexture) ));   // 获取 (0, 0) 位置的透明度,如果透明度大于 1 则被检测的点 (x, y) 在图形上   var hit = ctx.getImageData(0, 0, 1, 1).data[3] > 1;   ctx.setTransform(1, 0, 0, 1, 0, 0);   ctx.clearRect(0, 0, 2, 2);   return hit; }; 复制代码

但是值得注意的是,当图形的填充颜色完全透明时,这种检测方法就会失效,需要做一些特殊的处理。Create.js 的解决方案是给这个透明图形绑定一个大小一致的不透明的图形,用这个图形来做检测。上面的代码在这里,感兴趣可以自行查看。

基于随机颜色的检测

在这种实现方案中,会为画布中每个图形元素产生一个随机的颜色值 colorKey。除了渲染一个为用户展示的 sceneCanvas 之外,还会在内存中绘制一个用于检测点击的 hitCanvas。所有的图形都会在这个 hitCanvas 重新绘制一遍,并且各个图形的大小和位置属性也保持一致,但是图形填充的颜色使用的是对应的 colorKey 值。在用户点击之后,通过获取 hitCanvas 上点击位置的颜色值(即 colorKey),就可以找到是哪个图形元素被点击了。

如上图所示,左侧的是正常渲染的 sceneCanvas,右侧的是 hitCanvas。hitCanvas 里面的每个图形所对应的颜色都是唯一的,通过颜色值与图形的对应关系,可以很容易定位出哪个元素被点击了。随机的 hex 颜色最多会会有 256 * 256 * 256 = 16777216 种,也就是说在 Konva.js 最多支持 16777216 个图形做点击检测,这个数量也足够我们开发使用了。上面提到的透明度检测方案,是将每个图形分别在不同的 Canvas 中绘制;而随机颜色的方案,是将所有的图形在同一个 Canvas 中绘制,以颜色为 ID 做区分。

Konva.js 采用的是这种方式来判定图形是否点击的。下面的代码是生成随机颜色的代码,点击链接可查看源码, Konva.js 生成随机颜色:

constructor(config) {   super(config);   // set colorKey   let key;   while (true) {     // 生成随机颜色     key = Util.getRandomColor();     if (key && !(key in shapes)) {       break;     }   }   this.colorKey = key;   // window.Konva.shapes   shapes[key] = this; } 复制代码

下面的代码用于判断某点是否在图形内部,点击链接可查看源码,Konva.js 判断点是否在图形内部。

_getIntersection(pos) {   const ratio = this.hitCanvas.pixelRatio;   const p = this.hitCanvas.context.getImageData(Math.round(pos.x * ratio), Math.round(pos.y * ratio), 1, 1).data;   const p3 = p[3];   // fully opaque pixel   if (p3 === 255) {       const colorKey = Util._rgbToHex(p[0], p[1], p[2]);       const shape = shapes[HASH + colorKey];       // 找到对应的图形       if (shape) {           return {               shape: shape,           };       }       return {           antialiased: true,       };   }   else if (p3 > 0) {       // antialiased pixel       return {           antialiased: true,       };   }   // empty pixel   return {}; } 复制代码

当点击多个图形的重叠区域时,上面的方法只能判断出最上层的图形被点击了。在这种情况下 Konva.js 提供了 getAllIntersections(pos) 方法可获取到所有被点击的图形,这个方法的实现原理与 Create.js 的实现方案是一样的,在内存中将每个图形逐一渲染,判断对应点的透明度是否大于 0 即可。在 Konva.js 文档中这个方法是不推荐使用的,因为它会带来性能的损耗:

getAllIntersections(pos): get all shapes that intersect a point. Note: because this method must clear a temporary canvas and redraw every shape inside the container, it should only be used for special situations because it performs very poorly. Please use the Konva.Stage#getIntersection method if at all possible because it performs much better


作者:KooFE
链接:https://juejin.cn/post/7036019244215042078

 伪原创工具 SEO网站优化  https://www.237it.com/ 


文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐