/ 前言 /
早期的浏览器并没有提供 2d 绘图功能,许多用到绘图的地方往往使用 flash 完成。随着 html5 标准的普及,svg/canvas 在浏览器中基本不存在兼容性问题了,在 node,小程序环境也能使用 canvas 绘图。
各种基于 2d 绘图的应用也开始涌现出来, 比如像 echarts,g2 这样的可视化库, processon 的流程图, 甚至连谷歌文档也改用 canvas 实现了。本文介绍 2d 绘图的基础知识以及一些性能优化手段。
/ 基础技术 /
目前常见的 web 2d 绘图使用的底层技术主要是 canvas 和 svg, 大多数 2d 渲染引擎都是基于这两种技术,如 zrender、paperjs、raphael.js。webgl 主要用于 3d 渲染, 2d 是 3d 的一个特例,所以 webgl 也可以用来绘制 2d 图形,如 pixijs。
这几种技术各有优缺点,一般来说对于偏静态的场景如图表库、图可视化,使用 canvas/svg 差点不大。在数据量较大或动画较多的场景,使用 canvas 在性能上会更有优势。webgl 除了性能优势外,还能做出很多 canvas/svg 没有的特效。
svg
svg 是基于 xml 的 2d 矢量绘图技术,具有分辨率无关的优点,缩放时不会模糊。除了提供基本的绘图外,svg 还内置了动画和丰富的滤镜元素。由于 svg 是基于 dom 的,也可以很好地与 react, vue 等框架结合。
<svg width= 300 height= 300 >
<circle cx= 50 cy= 50 r= 10 fill= #000 stroke= red stroke-width= 1 />
</svg>
canvas
canvas 画布是 html5 中新增的元素, 用于提供绘图接口。和 svg 相比, canvas 对像素的掌握更灵活,而且没有 dom,对于大量数据的渲染,性能相比 svg 更好一些。
// 用canvas画一个圆
const ctx = canvas.getContext('2d')
ctx.beginPath()
ctx.arc(50, 50, 10, 0, Math.PI * 2)
ctx.fillStyle = '#000'
ctx.strokeStyle = 'red'
ctx.fill()
ctx.stroke()
webgl
使用 webgl 绘制 2d 图形通常做法是将 2d 图形使用三角剖分,再使用 webgl 绘制三角形。简单几何形状如直线、圆、矩形的三角剖分比较容易, 对于复杂形状尤其是带孔洞的三角剖分,则要麻烦的多。
还有一种基于 SDF(有向距离场)的方案来绘制一些常见几何形状。这种方案定义点在区域边界外部为正,边界上为 0,边界内为负。通过 sdf 函数,可以判断像素点是否在图形内。
圆的 sdf 函数
float myCircleSDF( in vec2 p, in float r )
{
return length(p)-r;
}
/ 图形绘制 /
在 svg 中除了直接提供常见的图形元素如 rect, circle, polygon 等形状外,还提供了一个万能的path元素,通过 path 指令完成任何形状的绘制。canvas 也提供了类似 path 的 api, 可以和 svg path 实现互相转换,进而实现 svg/canvas 双引擎的支持。
canvas | svg path |
---|---|
moveTo | m, M |
lineTo | L, l |
beizerCurveTo | c, C, S, s |
quadraticCurveTo | Q q T t |
arc & ellipse | A, a |
closePath | z, Z |
rect | - |
svg path 绘制一个三角形
<path d= M 0,0L100,100L0,100Z />
使用 canvas 实现
ctx.beginPath()
ctx.moveTo(0,0)
ctx.lineTo(100, 100)
ctx.lineTo(0, 100)
ctx.closePath()
此外现代浏览器支持使用Path2D对象生成绘制路径
const path = new Path2D('M 0,0L100,100L0,100Z');
ctx.fill(path);
/ 事件交互 /
如果使用的是 svg 绘图技术,可以像普通 dom 元素一样监听各种事件。而 canvas 只提供了一个画布,无法直接获取事件坐标所在的图形,需要用户自己判断拾取,常见的拾取方式有几何拾取和像素拾取。此外,canvas 2d 提供了isPointInPath api 用于判断点是否在路径内。
几何拾取
几何拾取是直接基于数学方法判断点的坐标是否在图形内。比如点是否在圆内的,可以计算点到圆心的距离是否小于圆的半径。
function isPointInCirlce(cx, cy, r, x, y) {
return (x -cx ) ** 2 + (y - cy) ** 2 < r ** 2
}
对于大部分简单的形状,都可以使用数学方法计算出来,性能也是各种方法里最好的。对于复杂的形状,尤其是包含曲线的形状,数学计算的难度也很大。
像素拾取
像素拾取的实现原理比较简单,将所有图形在离屏 canvas 上重新绘制一遍,绘制时使用图形的编号生成索引颜色。然后使用 canvas 提供的getImageData获取(x, y)处的颜色,即可获取该点对应的图形。像素拾取由于重新将图形绘制了一遍,当图形较多时,绘制开销较大,性能不如纯几何拾取的效果好。
OKee 在实践中综合使用了以上两种拾取方式。对于大部分简单的形状,使用数学计算。无法使用数学计算的复杂形状,再使用像素拾取。整体拾取性能可以支持数十万图元的交互。
/ 动画 /
动画是根据人眼的视觉停留效果,通过更改图形的位置、颜色等属性,形成动画效果。浏览器提供了requestAnimationFrame接口,通过使用该接口绘制下一帧。
根据动效的不同,将动画分成以下两类
属性动画,位置、旋转、颜色等属性进行插值
路径动画,沿着一条路径运动
变形动画,形状随着时间变化
动画插值使用的缓动函数,可以参考 Tween.js 提供的,如二次函数、三次曲线。
/ 性能优化 /
渲染性能优化
尽可能减少 canvas 上下文调用,如相同样式的图形只设置一次上下文
// 绘制1000个相同样式的矩形, 只设置一次上下文
ctx.save()
ctx.strokeStyle = color
ctx.lineWidth = 1
for (let i = 0; i < 1000; i++) {
ctx.strokeRect(x, y, width, height)
}
ctx.restore
视口之外的图形不参与绘制
有许多场景只有部分元素在可视范围内, 视口外不可见的元素就可以跳过绘制
只绘制变化的部分(脏矩形技术)
当屏幕上的元素发生变化只局限于一个区域时, 可以擦除这个区域的像素, 并重绘与这个区域重叠的图形, 而不用全屏重绘.
对于不变和变动的图形分层渲染或使用cacheAsBitMap缓存技术
大批量的数据分成多帧渲染
比如图表中大量的散点, 如果同时渲染需要等待数秒才能绘制完毕. 可以每帧渲染一部分, 直至将所有图形绘制完.
使用多线程和离屏 canvas 分块瓦片渲染
将 canvas 分成多个瓦片, 在多个 work 线程中使用 OffscreenCanvas 渲染完后再渲染到对应的瓦片区域, 这种优化手段在地图应用中比较常见, 地图数据本身就是瓦片式存储的.
拾取性能优化
使用包围盒加快判断
先判断点是否在其包围盒内, 如果不在包围盒内, 则直接排除. 如果在, 则使用更精确的几何或像素拾取判断.
缓存图形的变换矩阵、逆矩阵
在拾取过程中, 需要反复计算图形的矩阵和逆矩阵. 缓存后可以减少计算量.
使用 R-tree 等空间索引加速拾取过程
使用空间换取时间,通过建立 R-tree 索引, 加快拾取过程.
/ 关于我们 /
OKee Design 创立于2019 年,面向场景繁多、诉求复杂的企业级产品设计,旨在打造一套逻辑清晰、扩展性强大的设计系统。通过足够全面、通用、美观、灵活的内容,为各类产品设计提供强有力的解决方案及指导规则。
github 组织: https://github.com/oceanengine
/ 参考 /
zrender: https://github.com/ecomfe/zrender
pixijs: https://github.com/pixijs/pixijs
raphaeljs: https://github.com/DmitryBaranovskiy/raphael
Path2D: https://developer.mozilla.org/zh-CN/docs/Web/API/Path2D
mdn canvas 2d 上下文 ( https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D )
canvas 拾取方案选择 ( https://www.yuque.com/antv/ou292n/ktmgek )
贝塞尔曲线相关算法 ( https://pomax.github.io/bezierinfo/ )
射线法 ( http://blog.sina.com.cn/s/blog_86186c970102ybwn.html )
glMatrix ( http://glmatrix.net/docs/ )
tween.js ( https://github.com/tweenjs/tween.js/ )
显示列表与脏矩形渲染 ( https://idom.me/articles/841.html )
2d 基本图形SDF(有向距离场) ( https://blog.csdn.net/qw8704149/article/details/121412729 )