Canvas 的拾取方案选择

本文来自G 4.0 官方文档(https://www.yuque.com/antv/ou292n/ktmgek),

github地址:https://github.com/antvis/g

简介

当前流行的图表库的底层渲染层都是使用 Canvas 或者 SVG 。从功能上来说 SVG 的功能更全面一些,提供的接口更丰富,使用更简单,但是所有的渲染和拾取(点击获取图形)都是浏览器内置的行为,其性能跟浏览器相关;而 Canvas 是使用一种直接绘制的方式,其渲染性能和拾取性能跟用户的实现的方式密切相关,性能的优化空间非常大。本文关注 Canvas 的拾取的方案以及各种方案的优缺点,提供给用户在合适的场景下选择恰当的拾取方式。

拾取方案

由于 Canvas 不会保存绘制图形的信息,一旦绘制完成用户在浏览器中得到的是一张图片,用户在图片上点击时时不能获取对应的图形信息,所以需要缓存图形的信息,根据用户点击的位置进行判断击中了那些图形。常见的拾取方案有以下几种:

  • 使用缓存 Canvas 通过颜色拾取图形

  • 使用 Canvas 内置的 API 拾取图形

  • 使用几何运算拾取图形

  • 混杂上面的几种方式来拾取图形

上面的各种拾取方案各有利弊,下面来详细的介绍各种方案的实现方式和一些问题,最后对比一下性能。

使用缓存 Canvas

使用缓存的 Canvas 来进行图形的拾取步骤如下:

  • 在显示的 Canvas 上绘制图形

  • 在缓存(隐藏)的 Canvas 上重新绘制一下所有的图形,使用图形的索引值作为图形的颜色来绘制图形

  • 在显示的 Canvas 进行点击,获取缓存 Canvas 上对应位置的像素点,将像素的颜色转换成数字,这个数字就是图形的索引值

左图是在显示的 Canvas 上绘制图形,右图是在缓存的 Canvas 上绘制图形

伪代码
// 绘制显示的图形
drawShapes(shapesArray);
// 绘制隐藏的图形
drawCacheShapes(shapesArray);
// 获取缓冲的画布的数据
var cacheImageData = cacheContext.getImageData(0, 0, width, height);
canvas.onClick = function(ev) {
  var point = getPoint(ev.clientX, ev.ClientY);
  var color = getCacheColor(point);
  var index = colorToNumber(color);
  var shape = shapesArray[index];
  // 拾取完毕
}

优缺点分析:

  • 优点

    • 实现简单,只需要将图形绘制两遍即可

    • 拾取性能好,核心的拾取算法复杂度 O(1)

  • 缺点

    • 渲染开销加倍

    • 画布过大时获取缓存数据 getImageData() 方法开销很大,会降低快速拾取的收益

适合的场景和不适宜的场景

  • 适合的场景

    • 图形的数量比较大、重绘不频繁的场景

    • 支持局部刷新的场景效果更好

  • 不适合的场景

    • 频繁动画的场景,两倍的渲染开销和获取缓存数据方法的开销过大,性能反而降低

    • 图形的数据量很小的情况下优势不明显

10000 个矩形的绘制时性能点:

  • 绘制显示的10000个图形 6ms

  • 在缓存的图形 14ms ,增加了将数字转换成颜色的开销

  • 获取缓存的图片数据 getImageData() 的开销 14ms

  • 图形拾取的开销 0.1ms

使用内置的 API

Canvas 标签提供了一个接口 isPointInPath() 可以通过这个接口获取对应的点是否在绘制的图形内部,所以这种拾取方案的步骤如下:

  • 绘制所有图形

  • 进行拾取时,重绘每个图形,不调用 stroke 和 fill 方法(不真正的绘制出图形)调用 isPointInPath() 方法判断点是否在图形中,如果在则终止。

伪代码实现

function drawShapes(shapesArr) {
  for(var i = 0; i < shapesArr.length; i++) {
    ctx.save();
    drawShape(shapesArr[i])
    ctx.fill(); // 或者 ctx.stroke()
    ctx.restore();
  }
}
function hitShape(shapesArr, point) {
  var rst = null;
  for(var i = shapesArr.lenght - 1; i > 0; i--) {
    ctx.save();
    drawShape(shapesArr[i])
    if (ctx.isPointInPath(point.x, pont.y)) {
      ctx.restore();
      rst = shapesArr[i];
      break;
    }
  }
  return rst;
}

优缺点:

  • 优点

    • 实现简单,仅使用 Canvas 原生的接口

    • 不会拖慢首次渲染的时间

  • 缺点

    • 性能差,每次检测都得走一遍图形的绘制

    • 仅能检测是否被包围,不能检测是否在线上

适合的场景

  • 图形的量非常小 < 100 个时

  • 可以配合包围盒检测、四分树检测一起使用

性能检测

  • 拾取 10000 个图形的时间 2000ms

纯几何运算拾取图形

Canvas 上绘制的图形都是标准的几何图形,点、线、面的检测在几何算法中比较成熟,当拾取图形时可以直接使用数据运算检测是否击中图形。

检测过程如下:

  • 反序检测所有的图形

  • 判断点是否在图形的包围盒内,如果不在,则返回 false

  • 如果图形绘制线,则判断是否在线上

  • 如果图形被填充,则判断是否被包围

drawShapes(shapeArray);
function hitShape(shapeArray, point) {
   const rst = null;
   for(var i = shapesArr.lenght - 1; i > 0; i--) {
     const shape = shapesArr[i]
     if (isInShape(shape, point)) {
       rst = shape;
       break;
     }
   }
   return rst;
}
// 检测是否在图形内
function isInShape(shape, point) {
  const bbox = getBBox(shape);
  const isHit = false;
  // 检测是否在包围盒内
  if (inBBox(bbox, point)) {
    // 如果绘制线,则检测是否在线上
    if (shape.stroke) {
        isHit = isInStroke(shape, point);
    }
    // 检测是否包围在图形内
    if(!isHit && shape.fill) {
        isHit = isInPath(shape, point);
    }
  }
  return isHit;
}

优缺点:

  • 优点

    • 图形检测算法比较成熟

    • 思路比较清晰,优化潜力大,可以通过各种缓存机制优化检测性能

    • 不会影响图形的渲染性能

  • 缺点

    • 实现复杂,特别是一些贝塞尔曲线和非闭合曲线的检测性能比较差

    • 在存在大量分层的场景下,每个分层上有 transform 的存在,矩阵运算大大降低运算的性能

适合的场景

  • 使用范围广

性能检测:

  • 10000 个点的检测性能 5 - 20ms

混杂拾取

在各种各样的基于 Canvas 的场景下上面的几种拾取方式并非是非此即彼的,经常混合在一起使用,下面是几种典型的混杂拾取:

  • 几何计算 + 缓存 Canvas:使用缓存 Canvas 时需要缓存的 Canvas 的大小跟原始 Canvas 的大小保持一致,但是可以仅仅创建 1*1 的缓存 Canvas, 先通过计算是否在图形的包围盒内,将所有包含拾取点的图形在这个一像素的画布上进行绘制(需要进行 translate 将画布中心定位到拾取的点上),然后对这一像素进行颜色的检测。

注意:这种混杂模式对于简单图形”圆“、”矩形“ 的拾取并不比单纯的几何算法更快。

  • 几何计算 + isPointInPath: 简单的图形使用几何算法,复杂的很多填充的图形可以使用包围盒检测和 Canvas 内置的 isPointInPath 来检测。

总结

在 Canvas 上拾取图形时的方案选择与用户的场景密切相关,不同的场景适用的方案也不同:

  • 在图形数量少,不需要精确拾取的场景下(移动端)可以直接使用 isPointInPath 方法

  • 在画布不频繁刷新、图形量大的场景下适合使用缓存的 Canvas 的方法

  • 使用几何算法的拾取方案几乎适合于所有的场景,但是需要配合各种缓存机制,并注意矩阵乘法带来的开销

  • 上面的几种方法可以混合使用,拾取的优化无止境,但是满足需求即可。

-----------------------------------------

欢迎关注“玄说-前端”

扫描下方二维码,加”助理“好友,回复”加群“,进入“玄说-前端” 微信群,一起讨论前端技术。

-------------------

下篇文章公布获奖名单

精彩不断,点击“在看”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值