浅谈 Canvas 渲染引擎设计( 三 )


目前主流的两种事件实现方式分别是取色值法和几何法 。
3.1 取色值法取色值法是 Konva 采用的实现方式 , 它的实现方式非常简单 , 匹配精确度很高 , 适合不规则图形的匹配 。
取色值法的原理如下:

  1. 在主 Canvas 绘制一个图形的时候 , 会为这个图形生成一个随机的 colorKey(十六进制的颜色) , 同时建立类似于 Map<colorKey, Shape> 的映射 。
getRandomColor() {var randColor = ((Math.random() * 0xffffff) << 0).toString(16);while (randColor.length < 6) {randColor = ZERO + randColor;}return HASH + randColor;}, 
  1. 绘制的同时会在内存里的 hitCanvas 同样位置绘制一个一模一样的图形 , 填充色是刚才的 colorKey 。
  2. 当用户鼠标点击 Canvas 画布的时候 , 可以拿到鼠标触发的 x、y , 将其传给内存里面的 Canvas 。
  3. 内存里面的 Canvas 通过 getImageData 来获取到当前的颜色 , 进而通过 colorKey 来匹配到对应的图形 。
从上述原理可以看出来 , Konva 对于不规则图形的匹配依然很精确 , 但缺点也很明显 , 每次都需要绘制两份 , 导致绘制性能变差 。
同时 , getImageData 耗时比较高 , 在频繁触发的场景(onWheel)会导致帧率下降严重 。
3.2 几何法几何法有很多种实现方式 , 这里主要讲解引射线法 , 因为需要进行一系列几何计算 , 所以这里我称之为几何法 。
几何法是 AntV 和飞书文档采用的实现方式 , 实现方式相对复杂一些 , 针对不规则图形的匹配效率偏低 。
几何法的实现原理如下:
  1. 基于当前虚拟节点的包围盒来构建一棵 R Tree
  2. 当用户触发事件的时候 , 利用 R Tree 来进行空间索引查找 , 依据 z-index 找到最顶层的一个图形 。
  3. 从目标点出发向一侧发出一条射线 , 看这条射线和多边形所有边的交点数目 。
  4. 如果有奇数个交点 , 则说明在内部 , 如果有偶数个交点 , 则说明在外部 。
为什么奇数是在内部 , 偶数是在外部呢?我们假设射线与这个图形的交点 , 进入图形叫做穿入 , 离开图形叫做穿出 。
在图形内部发出的射线 , 一定会有穿出但没有穿入的情况 。但在外部发出的射线 , 穿入和穿出是相对的 。
但是射线刚好穿过顶点的情况比较特殊 , 因此需要单独进行判断 。
几何法的优势在于不需要在内存里面进行重复绘制 , 但依赖于复杂的几何计算 , 因此不适合有大量不规则图形的情况 。
在 AntV 里面支持对不规则图形的匹配 , 但飞书文档由于是表格业务 , 所以可以将所有图形都当做矩形来处理 , 反而更简单一些 。
4. 性能由于 Canvas 渲染引擎都会进行大量的封装 , 所以开发者想针对底层做性能优化是非常难的 , 需要渲染引擎自身去支持一些优化 。
4.1 异步批量渲染在飞书文档 Bitable 和 Konva 里面都支持异步渲染 , 将大量绘制进行批量处理 。
const rect = new Rect({ /... });// 多次修改属性 , 可能会触发多次渲染rect.x(100);rect.fill('red');rect.y(100); 
由于每次修改图形的属性或者添加、销毁子节点都会触发渲染 , 为了避免同时修改多个属性时导致的重复渲染 , 因此约定每次在下一帧进行批量绘制 。
batchDraw() {if (!this._waitingForDraw) {this._waitingForDraw = true;Util.requestAnimFrame(() => {this.draw();this._waitingForDraw = false;});}return this;}这种渲染方式类似于 React 的 setState , 避免短时间内多次 setState 导致多次 render 。
4.2 离屏渲染离屏渲染我们应该都比较熟悉了 , 就是两个 Canvas 来回用 drawImage 绘制可复用部分 , 从而减少绘制的耗时 。
这里主要讲解 Konva 和飞书 Bitable 里面的离屏渲染 。
在 Konva 中的离屏渲染主要是针对 Group 级别来做的 , 通过调用 cache 方法就能实现离屏渲染 。


推荐阅读