本文来自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 的方法
使用几何算法的拾取方案几乎适合于所有的场景,但是需要配合各种缓存机制,并注意矩阵乘法带来的开销
上面的几种方法可以混合使用,拾取的优化无止境,但是满足需求即可。
-----------------------------------------
欢迎关注“玄说-前端”
扫描下方二维码,加”助理“好友,回复”加群“,进入“玄说-前端” 微信群,一起讨论前端技术。
-------------------
下篇文章公布获奖名单
精彩不断,点击“在看”