Canvas 怎么拾取节点(元素)?

174 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第28天,点击查看活动详情

前言

最近在封装 Canvas 进行绘制节点的时候,做事件处理的时候,需要拾取节点,由于 Canvas 的渲染模式是立即模式,即不保留节点属性,所以我需要处理一下坐标从而拾取应该被拾取的节点从而作出相对应的事件处理,这一点来说,SVG作为保留模式更简单些,但是SVG的拾取节点都是浏览器的行为(操作dom)性能跟浏览器,而Canvas的方案却可以进行优化

方案

总的来说,我们需要缓存我们绘制的节点属性,当通过事件获取到事件句柄的时候,我们可以拿到坐标并处理选择之一或多个:

  • 使用内置API isPointInPath 
  • 使用数学计算包围盒(平面几何)
  • 使用内置API getImageData 

接下来我介绍以下这几个方案的利弊

isPointInPath

这是Canvas提供的一个判断一个点是否在路径内的api,函数签名点击这里,我们只要重绘每个节点,再调用方法判断点是否在节点里,这里可以不调用 stroke 和 fill 方法(但绘制节点的时候需要调用beginPath,否则无法使用isPointInPath函数进行判断

看看代码实现

for (var i = nodes.lenght - 1; i > 0; i--) {
  ctx.save();
  nodes[i].draw();
  if (ctx.isPointInPath(vec2.x, vec2.y)) {
    ctx.restore();
    console.log(nodes[i]);
    break;
  }
  ctx.restore();
}

显然实现起来很简单方便,直接调用api就可以判断,但是缺点是每一次判断都需要走一次渲染,而且不能判断是否在线上,适合节点数量较少的情况下

使用数学计算包围盒(AABB)

我们绘制的点线面节点都可以计算出包围盒,比如下面的小人的虚线框:

通过一些矩阵计算得到一个包围盒,不过我们可以明显看到,小人的周围还是有很多空隙,但我们显然可以利用包围盒判断出拾取的坐标是否被包含,从而第二次检测是否点击中节点

当然包围盒有很多种,比如AABB,OBB,这里暂且不表,几乎适用所有场景,但是如果有很多 Group 的分组,多次transform 的矩阵计算会降低性能,但我们可以通过缓存去优化计算

虽然如此,相比于 isPointInPath 和接下来要说的 getImageData 还是比较复杂的

使用内置API getImageData

这是 Canvas 提供的一个判断一个点是否在路径内的 api,函数签名点击这里

我们通过一个缓存的 Canvas 来重新绘制我们的节点,在网上我是看到了使用索引转换颜色进行判断的方法,

就是将索引转换成颜色进行绘制,然后在显示的canvas中点击,就获取缓存 Cavnas 的该点的颜色,通过颜色值判断节点

但是使用 缓存Canvas 使用  getImageData 来说 比较适用于不会频繁刷新的场景,否则两倍的渲染非常消耗性能

我想可以点击之后反序绘制节点,利用 getImageData 获取的 alpha 值进行判断,如果不为0则是拾取到了

不过到底是否可以点击到透明节点,还得有待商讨

我目前选择的方案

利用包围盒进行判断是否在节点内,再进行一个1*1的缓存的Canvas ,通过切换坐标系,达到点击点绘制在Canvas上,再利用 getImageData 获取的 alpha 值进行判断

但是计算包围盒也仅仅是每一个节点的包围盒,并没有合并包围盒,这点还是可以进行优化的,并且绘制的适合也可以有各个节点的 cache 进行绘制,不用再进行矩阵计算,将会大大提高快速拾取的时间

总结

我所了解的方案就是以上这些,不代表只有这几种方案,也不代表我选择的是最优的,只是目前满足我的需求,选择上肯定需要不同的场景来进行选择