AntV Canvas 局部渲染总结

Canvas 局部渲染优化总结

简介

G2(图表引擎) 4.0 和 G6(图分析引擎) 3.4版本已经替换了 G(2D 渲染引擎)4.0,这个版本最大的改进是支持了局部渲染,在一些场景下例如节点的状态改变、图形的个体动画等方面性能提高巨大。G 4.0 从开始重构到如今稳定经历了半年的不断完善,遇到了各类各样的问题,本文将对 Canvas 的局部渲染作一个总结,给后来者一些帮助。 javascript

问题分析

因为 Canvas 的绘制方式是画笔式的,在 Canvas 上绘图时每调用一次 API 就会在画布上进行绘制一次,一旦绘制图形就成为画布的一部分。绘制图形时并无对象保存下来,一旦图形须要更新,须要清除整个画布从新绘制。
java

image.png
image.png

为何要把整个 Canvas 画布都清除,而后总体重绘?咱们以上面的两张图为例,以左图为例,1, 2 没有同其余的图形重合,能够清除掉从新绘制,可是 3,4 就没法单独清理掉重绘;右图仅仅在左图的基础上增长一条折线,这时候咱们就没法刷新单个图形了。
而在咱们要实现局部渲染时,须要考虑的两个因素是:

  • 单次刷新时影响的范围最小
  • 刷新的图形不会影响其余图形的正确绘制

仅仅缩小刷新时的范围从而提高性能并不够,以右图为例,若是咱们要刷新图形 2 将图形变成红色。这时候若是仅仅清理掉图形 2 ,从新绘制则:
git

image.png

折线就会部分消失,这与咱们的预期不一致。局部刷新不但要保证刷新的范围足够小,还要保证图形绘制的正确性。

方案

咱们来思考 Canvas 局部渲染方案时,须要看 Canvas 的 API 给咱们提供了什么样的接口,这里主要用到两个方法:github

经过这两个 API 咱们能够获得 Canvas 局部刷新的方案:canvas

  1. 清除指定区域的颜色,并设置 clip
  2. 全部同这个区域相交的图形从新绘制

image.png
image.png

image.png
image.png


以上图为例,若是咱们想刷新图形 3,使得图形的颜色变成红色

  1. 首先肯定图形的矩形包围盒
  2. 清除这个包围盒内的颜色,设置这个区域为 clip 区域
  3. 从新绘制全部跟这个区域相交的图形
  4. 重绘图形 3
  5. 重绘图形 4

遇到的问题

真实的在 G 4.0 中实现局部渲染时遇到的问题比上面的案例复杂的多:浏览器

  • G 不只仅支持图形渲染,也支持分组 Group,一旦分组发生变化也会触发局部刷新
  • 除了图形的属性变化外,图形的顺序调整、添加、移除图形以及显示隐藏等也会致使刷新
  • 图形和分组上会增长各类矩阵,图形的包围盒计算频繁而又复杂

这些问题在 1-2 周内都解决了,可是在接入 G2 和 G6的过程当中遇到了一些彻底没想过的问题持续了半年的时间,主要体如今两个方面:安全

  • 包围盒计算不精确,致使的残影问题
  • 局部刷新致使的性能降低

残影的问题

首先咱们来看画布上的两条线,一样都是 1 像素颜色 #333 的线,有什么差异?
markdown

image.png

很明显上面的一条,两像素宽,同时颜色变淡,两条线的坐标为:

  • 线段1(粗): (10, 100) - (200, 100)
  • 线段2(细):(10, 149.5) - (200, 149.5)

因为屏幕的分辨率只能在整数的点上绘制颜色,线段 1 一半绘制在 (10, 99)-(200, 99) 一半绘制在 (10, 101 )-(200 101)上,因此浏览器会自动的把落到半个像素上的点扩展成一个点,颜色变淡,就变成了下图的示例(示例中画布进行放大,每一个单元格表明一像素)。
app

image.png
image.png

因此在 Canvas 画布上绘制图形时,任意的点若是部分落到一个像素上,都会占满整个像素,这个问题在平时的总体刷新时不明显,一旦咱们来实现局部刷新就会出现问题,下面的多个问题都与此相关。

浮点数计算的问题

咱们在绘制图形时不少图形属性是自动计算出来的,例如:oop

  • 直线 (10.2, 44.3) -  (20.1, 10.5)
  • 圆,圆心(10.5, 8.8) 半径 3.4

这时候图形绘制的区域同数学计算出来的并不一致,这就会致使局部刷新时清空的区域不足,会留下一些残影。

image.png

上图中对圆进行几何计算的包围盒和实际的包围盒有了差异,这时候局部渲染就出问题了。解决方案:

  • 将包围盒的 minX, minY 向下取整 (10.2, 10.5) -> (10, 10)
  • 将包围盒的 maxX,maxY 向上取整 (20.1, 44.3) -> (21, 45)

折线夹角的问题

因为 Canvas 在实现折线时,在线段的交接处作了处理,会附加额外的像素,使得折线更美观,咱们来看下单独绘制两条线段,和一条折线的差异:

image.png
image.png

红框为咱们经过数学计算出来的包围盒,能够看出折线的拐角处明显超出一部分,这时候折线改变时的刷新就不许确。解决这个问题有两个解决方案:

  • canvas 在绘制时提供了一个参数: lineJoin,能够设置 context.lineJoin="bevel|round|miter";详情参看 canvas lineJoin 能够改为 bevel 就再也不有尖角,可是同咱们的预期不一致。
  • 计算折线包围盒时增长拐角的计算,若是折线线段的夹角小于 90 度,则计算超出的像素数。若是折线的线段比较多,能够仅计算落到折线上下左右四个边上拐点超出的像素数(有必定风险)

shadow 的问题

在 Canvas 上绘制图形时能够指定阴影,有四个参数关系到阴影的设置:

shadowColor 设置或返回用于阴影的颜色
shadowBlur 设置或返回用于阴影的模糊级别
shadowOffsetX 设置或返回阴影距形状的水平距离
shadowOffsetY 设置或返回阴影距形状的垂直距离

下面两个圆,若是不考虑阴影进行局部刷新时会出现下面的状况:

image.png
image.png

因此在局部渲染时,经过判断是否有 shadowColor 来附加额外的包围盒,计算出阴影影响的范围,同原始的包围盒相并便可:

// 若是存在 shadow 则计算 shadow
if (attrs.shadowColor) {
  const { shadowBlur = 0, shadowOffsetX = 0, shadowOffsetY = 0 } = attrs;
  const shadowLeft = minX - shadowBlur + shadowOffsetX;
  const shadowRight = maxX + shadowBlur + shadowOffsetX;
  const shadowTop = minY - shadowBlur + shadowOffsetY;
  const shadowBottom = maxY + shadowBlur + shadowOffsetY;
  minX = Math.min(minX, shadowLeft);
  maxX = Math.max(maxX, shadowRight);
  minY = Math.min(minY, shadowTop);
  maxY = Math.max(maxY, shadowBottom);
}
复制代码

箭头的问题

在线上增长箭头是个常见需求,可是因为箭头是附加在线上的,计算包围盒未计算在其中,这就致使刷新时箭头未被清除,同时箭头又有多种状况,还要考虑箭头的自定义:

image.png
image.png

所以 G 4.0 将箭头实现成了一个新的 shape,线包围盒计算时同时附加箭头的包围盒,进行相并处理。

文本渲染的问题

你能看清楚下面的文本发生了什么吗?若是仔细观察会发现文本左侧被裁剪掉了一像素,这种状况在多个场景下都存在

image.png

经过一番痛苦的排查发现,在这个 demo 的页面上有一个属性,在不一样的字体下致使文字的宽度不一致:
     
image.png

分析一下缘由发现,当前文本的宽度计算是经过离屏 Canvas ,也就是建立一个 canvas 标签,可是没有放入 document.body 下,致使离屏 Canvas (1*1 的画布) 上的 font 相关的属性同当前 Canvas 不一致致使的。有两个方案能够解决这个问题:

  • 将离屏的 cavas 添加到当前页面文件流中
  • 在图表中设置全部的 font 属性,覆盖掉 body 上的属性

浏览器缩放的问题

G2 4.0 和 G6 3.4 发布后,有用户反馈在页面上进行操做时,出现一些线的划痕

image.png

一开始是怀疑线宽度计算时的浮点数问题,咱们在本地和虚拟机上进行了测试,始终没有定位到问题。用户反馈他们对浏览器进行了缩放,经过检测他们浏览器页面上的 window.devicePixelRatio 是非整数。这个参数是浏览器和屏幕的像素比,通常状况下是 1,高精屏下多是 2 或者 3,为了让图形的绘制更加清晰咱们在 G 上进行处理,屏蔽了这个参数,可是在用户对浏览器进行了缩放后,这个参数会变成 1.二、1.3 、1.5 等非整数值。
在开始讨论遇到的局部渲染问题时,咱们介绍了直线在屏幕上的绘制,绘制发生在部分像素时浏览器会将整个像素设置颜色,而且变浅,这就出现了线的浅色的划痕。
解决方案:当 window.devicePixelRatio 是非整数时,给包围盒四个方向各附加 0.5 像素,而后取整便可。

// 附加 0.5 像素,会解决1px 变成 2px 的问题,不管 pixelRatio 的值是多少
// 真实测试的环境下,发如今 1-2 之间时会出现 >2 和 <1 的状况下未出现,可是为了安全,统一附加 0.5
const appendPixel = 0.5;
if (region) {
  region.minX = Math.floor(region.minX - appendPixel);
  region.minY = Math.floor(region.minY - appendPixel);
  region.maxX = Math.ceil(region.maxX + appendPixel);
  region.maxY = Math.ceil(region.maxY + appendPixel);
}
复制代码

性能问题

大量分散的图形刷新

若是同时有多个图形进行刷新,为了减小包围盒的计算,咱们会把全部刷新的图形的包围盒进行合并,可是会出现一些特殊状况,致使局部渲染的性能降低,例如:

image.png

视窗外的图形刷新

image.png

这些局部渲染的性能问题的解决方案牵扯的方面比较多,不在这里展开,能够参考 渲染裁剪优化

总结

经历了半年的改造,G 4.0 的局部渲染方案已经经历了 G2 和 G6 的考验,在局部刷新的场景下在交互和性能等方面提高了 7-10 倍,可是依然存在一些特殊场景上的性能问题,还在持续优化中。渲染的一分提高,上层都会有十分的收获。


AntV 官网:antv.vision/
2D 绘图引擎 G:github.com/antvis/g

相关文章
相关标签/搜索