如何在 Canvas 上实现图形拾取?

大家好,我是前端西瓜哥,今天来和大家说说 canvas 怎么做图形拾取。

图形拾取,指的是用户通过鼠标或手指在图形界面上能选中图形的能力。图形拾取技术是之后的高亮图形、拖拽图形、点击触发事件的基础。

canvas 作为一个过于朴实无华的绘制工具,我们想知道如何让 canvas 能像 HTML 一样,知道鼠标点中了哪个 “div”。

维护节点树

canvas 只提供 API 在画布上绘制形状,并不知道它之前画过的图形是什么,不会保存它们的坐标、宽高等信息。

所以如果你想让 canvas 支持将其中的图形进行编辑,比如拖拽和放大,那就必须自己去维护一棵节点树。

类似这样:

const tree = {
  type: 'stage',
  children: [
    {
      type: 'rect',
      x: 10, y: 10, w: 100, h: 100,
      fill: 'red',
    },
    {
      type: 'circle',
      x: 0, y: 0, radius: 80,
      stroke: 'yellow',
    }
  ],
};

然后 canvas 基于此去按层级绘制这些图形。

下面我们看看元素拾取的几种方案。

方案 1:isPathInPoint

isPointInPath 是 canvas 原生提供的一个检测某个点是否在指定路径内的方法。

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

ctx.beginPath(); // 表示路径的开始
ctx.rect(30, 30, 100, 50);
ctx.stroke(); // 如果只是计算,可以不绘制出来

ctx.isPointInPath(40, 40); // true,在路径内
ctx.isPointInPath(10, 10); // false,不在路径内

线上 demo:

https:/ /codesandbox.io/s/h7pxsm

优点:

  1. 原生 API 支持,方便;

缺点:

  1. 判断光标点中哪个元素,需要遍历元素,去调用方法,直到返回 true 为止,性能可能会差一点(可以用四叉树碰撞检测,减少需要遍历的元素数量,但极端情况可能还是会有很多元素,另外可通过包围盒减少计算量);
  2. 检测点是否在一条 strokeWidth 较大的线上可能会有错误,因为路径是没有宽度的;

方案 2:缓存 Canvas

根据真正的 canvas 元素,额外创建一个大小相同离屏的缓存 canvas 元素。

每次我们在主 canvas 上绘制形状时,也在缓存 canvas 上绘制同样形状的纯色块,并用哈希表记录颜色和对应的图形对象,比如红色表示矩形 A,绿色表示矩形 B。

如何在 Canvas 上实现图形拾取?_第1张图片

然后当我们在真实 canvas 上点击时,我们在 canvas 绑定事件,就可以拿到坐标位置 (x, y),再通过 offScreenCtx.getImageData(x, y, 1, 1) 方法得到缓存 canvas 的对应像素点的颜色值,然后找到它对应的图形对象,执行其注册的事件。

Konva 库使用了该方案。

写了个简单的线上 demo,你可以尝试点击上面那个 canvas 下的图形,看看控制台输出:

https:/ /codesandbox.io/s/veivt3

优点:

  1. 能够快速确定点所在的图形;
  2. 能够修改碰撞范围,比如给一条细的线条进行区域的外扩,让用户更好选中这条线条;
  3. 适合图形量大、重绘较少的场景。

缺点:

  1. 渲染开销加倍。每个图形需要调用两次 API(页面上的 canvas 和缓存 canvas 各绘制一次);
  2. 如果图形频繁变化,性能会更低。

方案 3:图形学算法

可以用计算机图形学的算法,去判断一个点是否在某个形状内。

比如:

(1)点是否在矩形内。

function isPointInRect(point, rect) {
  return (
    point.x >= rect.x &&
    point.y >= rect.y &&
    point.x <= rect.x + rect.width &&
    point.y <= rect.y + rect.height
  );
}

(2)点是否在圆形内。

export function isPointInCircle(point, circle) {
  const dx = point.x - circle.x;
  const dy = point.y - circle.y;
  const dSquare = dx * dx + dy * dy;
  return dSquare <= circle.radius * circle.radius;
}

还有其他的:通过 “射线法” 判断点是否在多边形等。

优点:

  1. 某种意义上是 isPointInPath 的底层实现,能做到平台无关;

缺点:

  1. 和 isPointInPath 方案一样,需要遍历图形检测;
  2. 实现复杂,简单图形还算简单,但如果涉及到贝塞尔曲线等复杂形状,实现就会很复杂且性能堪忧(可以考虑用 isPointInPath);
  3. 如果使用了 transform,因为要进行矩阵乘法,性能会有所下降。

结尾

总结一下,canvas 的图形拾取有三种方案:

  1. isPointInPath:canvas 原生提供的 API,能够知道点是否在路径内;
  2. 缓存 Canvas:额外使用一个 canvas,每次绘制图形都在这个 canvas 上绘制纯色图形,记录映射关系。交互时通过 getImageData 得到颜色值,然后根据映射关系找到对应图形;
  3. 计算机图形学算法:自己写点是否在特定形状下的算法,本质是 isPointInPath 的底层实现。但复杂图形碰撞检测实现起来困难。

我是前端西瓜哥,欢迎关注我,学习更多知识。

你可能感兴趣的:(前端,javascript,开发语言)