ZRender学习笔记
如何调试程序
1 | |
注意,HTML 引入 dist 下的文件;如果遇到 TS 报错,设置@ts-ignore 即可。
ZRender 是什么
这个文章描述得很清晰,这里就不再赘述了:
https://www.runoob.com/w3cnote/html5-canvas-zrender.html
ZRender 应用了很多计算机图形学的内容(详见其 core 目录)
因为 ZR 比较早,那时候很多 ES 特性还没有,因此 ZR 都手动实现了
另外为了减少依赖,ZR 也实现了很多算法
所以其源码是绝对值得一读的,可以让我们受益颇多。
为什么叫 ZRender?
因为这是基于层级(z-index)实现的渲染器,因此叫做 ZRender
ECharts 与 ZRender 的关系
这两者就是一个数据驱动的具体体现。
ECharts 只处理数据(storage),相当于只做数据的预处理。
ZRender 就是一个渲染引擎。
数据驱动:我们应该大部分代码都只是操作数据;绘制完全由渲染引擎搞定。
重点学什么
参考二八原则,我只需要先了解 20%最重要的概念,能够应对平时 80%的需求即可。后续的等有具体需求再不断学习。
通过_zr 实例快速了解 ZRender 的数据结构。
Painter 的分治法设计,调度器设计
事件的抽象和封装,比如小程序、canvas、dom 都是不一样的
名词
shape
ZRender 内置了很多图形(圆形、椭圆、圆环、扇形、矩形、多边形、直线、曲线、心形、水滴、路径、文字、图片等),能够满足我们平时的绝大部分需求。
这些 shape 的配置,都是在 option 下的 shape 这个属性上。
这里只列出一些非常规的图形。
| 类型 | 说明 |
|---|---|
| Arc | 圆弧 |
| BezierCurve | 贝塞尔曲线 |
| CompoundPath | 复合路径 |
| Droplet | 水滴 |
| Ellipse | 椭圆 |
| Heart | 心形 |
| Image | 图形 |
| Isogon | 正多边形 |
| Polyline | 填充的多边形 |
| Rose | 玫瑰线 |
| Sector | 扇形 |
| Star | 星型 |
| Trochoid | 内外旋轮曲线 |
| Path.extend(props) | 扩展自定义图形 |
Element
元素,用来修改属性
Eventful
支持事件的元素,其 API 都是和事件处理相关的。
Group
Group 是一个容器,可以插入子节点,Group 的变换也会被应用到子节点上。
这个很适合对包含多个元素的内容进行批量操作,比如一个图形的 legend 有 N 个,那么可以通过 group 对齐进行统一的动画处理和事件处理。
Group 继承自 zrender.Element
Transformable
可以进行变换的对象,变换包括:位移、旋转、缩放。
util
静态工具类
数据结构
图形的抽象类zrender.Displayable一定要弄明白,这是所有图形的属性基础。
这里的配置很多都是和 CSS 相关的,因此需要先熟悉下 CSS。
了解这个的意义,就是你知道哪些视觉样式效果可以实现。
拾取
可视化中的拾取一般有 2 种:像素拾取和包围盒拾取,ZRender 用的是包围盒拾取。但是并不是简单的矩形包围盒拾取,而是在此之上做了更细致的处理,保证不规则图形也能准确拾取。
代码
可以重点看下Handler.ts。
会用上一个元素和当前元素的对比,然后通过 dispatch 的方式触发事件:
1 | |
这个设计很赞!这些事件分发机制是可视化库中很重要的核心机制,也是很好的抽象解耦设计,可以整理整个项目中所有的事件机制,拿出来整体分析讲一下。
(TODO)不规则图形的拾取
可以看下path.ts,针对每个图形,应该有个数据结构来描述和存储其线条信息(数据驱动)。
非零环绕原则
如何判断某个点是否在某个图形内部:非零环绕原则
“非零环绕原则”(Non-Zero Winding Number Rule)是一个在计算机图形学中用于确定点是否在路径内部的规则。这个原则通常用于 Canvas 绘图或图形处理中,尤其是在处理自相交的多边形或路径时。
根据搜索结果,非零环绕原则的工作原理如下:
从区域内随机选取一个点。
从这个点拉一条直线到图形外部。
观察穿过这条直线的路径段,如果是顺时针方向穿过则记为 +1,逆时针方向穿过则记为 -1。
将所有穿过直线的路径段的记数相加,如果总和为非零,则填充该区域;如果总和为零,则不填充。
这个原则适用于复杂图形,包括自相交的多边形,以确定哪些区域应该被填充。
(TODO)性能问题
这部分待确认。
模糊搜索
这个可以用于扩大交互热区,可以重点看下findHover,pointerSize相关的代码。
可惜这是 ECharts5.4 新增的特性,我们目前还不能使用,得升级下基础组件库版本才行。
元素选择
图形元素有 name 属性,这个是很有用的,比如可以给元素设置 name 属性,能很方便的通过 childOfName 寻找元素:
1 | |
事件
事件操作和普通的 DOM 事件操作类似,非常方便:
1 | |
Canvas 就一个画布,那事件是怎么实现的呢?
其原理是给 canvas 绑定相关事件,然后通过记录元素在 canvas 中的
坐标范围,判断事件作用于哪些元素,然后选择这些元素中层级最高的一个,执行该元素的对应事件。
可以看下这个文章,讲得很详细:https://www.cnblogs.com/suyuanli/p/9212994.html
分层
shape 的层级,是根据添加的顺序来定的,先添加的层级较低,后添加的层级较高,会显示在最上面,覆盖之前的图形。
这样刷新也是局部刷新,可以带来性能上的提升。
动画
执行流程

数据结构
一帧动画的定义:
Frame = Clip(Tracks)
Clip:剪辑,确定时间
Track:轨道,确定属性和值

Animator
功能:定义动画(类比 Model)。
动画对象。
Animator 的构造函数的 animatorObj 参数:
Track
Track = Attribute(startValue + endValue)
单属性的关键帧(属性变化的起止值)。每个属性会单独生成一个 Track 对象(比如给 rotate 的变更生成一个 Track),在该对象的 keyframes 数组中存放了该属性的关键帧(一般为起始帧+截止帧,共 2 帧)。
这是个数据概念。
这个类是定义在 Animator 中的,由此可见二者的关系。
这是一个 Track 的数据结构:
1 | |
这是放大效果(enlarge)的 tracks:
1 | |
Clip(动画片段/动画主控制器/剪辑)
这是个操作概念。可以理解为是个代理,代理到Track的step()
功能:剪辑 Animator 的 Tracks(所以 Clip 中没有动画属性,只有时间属性。这也是为什么 onframe 的实现是放在 Animator 中定义的,因为只有 Animator 存放了动画相关的属性):
1 | |
相关动画名词可以参考 Three.js 的动画文档
| 单词 | 翻译 |
|---|---|
| clip | 动画片段(也有翻译为剪辑的,但是感觉没有片段好,ECharts 注释中叫做动画主控制器) |
| tracks | 一个由关键帧轨道(KeyframeTracks)组成的数组。 |
这是最小粒度的动画,每一个属性的变动,都会生成一个 clip,比如这样:
1 | |
总共改变了 circle 的三个属性:半径 r、填充色 fill、位置 position,因此这里会生成 3 个 clip。
Clip 类的成员变量如下:
1 | |
Clip 的step流程:
- 通过 elapsedTime、lifeTime 算出动画百分比 percent
- 通过 easingFunc + percent 算出百分比对应的值 schedule
- 调用 onframe(schedule)更新动画对象的属性(如果没自定义,一般就是调用的
Animator的start()方法中定义的 onframe()方法)
Clip 对象是个链表结构。
由于 Clip 是 ZRender 的底层动画机制,粒度太小了,按理说我们不应该操作它。
GPT 的解释
这个文件定义了一个名为 Clip 的类,它在 ZRender 中扮演着动画剪辑或片段的角色。Clip 类是动画系统的一个基础组件,用于管理和控制单个动画的生命周期和行为。以下是该类的关键点:
动画生命周期管理:Clip 类管理动画的开始、进行、暂停、恢复和结束。
属性定义:
_life:动画的持续时间。
_delay:动画开始前的延迟时间。
_inited:标记动画是否已初始化。
_startTime:动画开始的时间。
_pausedTime 和 _paused:用于控制动画的暂停状态。
回调函数:
onframe:每个动画帧调用的回调函数,用于更新动画状态。
ondestroy:动画结束时调用的回调函数。
onrestart:动画循环重新开始时调用的回调函数。
动画循环控制:
loop:控制动画是否循环播放。
缓动函数:
easing 和 easingFunc:用于指定动画速度变化的缓动函数。
动画控制方法:
step:根据全局时间和时间差来更新动画状态,是动画的主驱动方法。
pause 和 resume:用于控制动画的暂停和恢复。
设置缓动函数:
setEasing:允许动态设置动画的缓动函数。
链表结构:Clip 对象可以通过 next 和 prev 属性连接成链表,这有助于动画管理系统高效地遍历和处理动画队列。
构造函数:接受一个配置对象 opts,用于初始化 Clip 实例的各种属性。
这个类可能是 ZRender 动画系统的一部分,用于处理动画的逻辑。在实际使用中,你可能会创建 Clip 实例,配置其属性,并将其添加到动画控制器中,以实现复杂的动画效果。通过 onframe 回调,你可以定义动画过程中的具体行为,例如更新对象的位置、形状或其他视觉属性。
Animation
这才是动画总控制器(类比 Controller)。
这里的 update()方法是执行动画逻辑的地方。
动画的RAF 帧循环也是在这里实现的(有做优化,仅会在动画中运行):
1 | |
我们可以通过修改 animation 的属性,然后调用 animation.update(),跳转到任何一个指定的动画关键帧。只需要设置一个 point 比例尺,反向根据数值计算动画的百分比即可。
折线图的动画,要确认下是否拆分为了多个 clip,因为是有多段的;也可能是一个动画,通过裁剪实现的?
常用方法
常用的有 animateTo()和 animate()这 2 个方法:
1 | |
建议使用 animate(),这个更完善,能实现的功能更多。
顺序播放动画的实现
- 可以通过设置不同的 delay 来实现,适合多个元素不同动画同时执行的情况
- 可以通过.done()执行动画的链式调用来实现,适合一个元素多个动画顺序执行
这两种方式有不同的应用场景,可以结合起来用。
如何跳到某一帧?
手动计算
- 通过动画的起止值,设置一个属性比例尺 attrScale
- 根据动画的播放时长,设置一个时间比例尺 timeScale
- 利用 timeScale,根据传入的目标时间参数(now),得到归一化后的时间值 t
- 根据该归一化的时间值 t,通过属性比例尺 attrScale 得到该时间点的属性值 attrObject
- 调用图形的 attr(attrObject)方法绘制该帧数的图形
1 | |
clip.onframe(percent)
1 | |
比如 Animator.ts 的 stop()方法就是用这个实现跳转到动画最后一帧的:
1 | |
缓动类型
可以查看这个页面:
https://echarts.apache.org/examples/zh/editor.html?c=line-easing
禁用动画
禁用入场动画
禁用播放动画
ZRender.Element.stopAnimation()
1 | |
渲染器
ZRender 从 4.0 版本就实现了 Canvas、SVG、VML 三种渲染器,通过配置项的 renderer 即可配置。
不过据说 SVG 渲染器在处理渐变色上有 BUG,官方尚未修正(似乎是尚未实现)。
注册渲染器
如果想要引入 SVG 渲染器,需要注册下。类似这样:
1 | |
在这个 src/svg/svg.ts 中,进行了渲染器的注册:
1 | |
这样你就可以使用 SVG 渲染器了。
如果你项目中是通过 npm 包的形式引入 ZRender 的,那么在 node_modules/zrender/zrender.all.js 中,也可以看到渲染器的引入:
1 | |
SVG 渲染器的实现原理
SVGPathRebuilder 这个类,用 SVG 实现了 Canvas 的各种 API,因此 ZRender 就可以像操控 Canvas 一样操控 SVG 了。

(待确认)具体是调用 Canvas 还是 SVG,是在 src/canvas/Painter.ts 和 src/svg/Painter.ts 的 paintOne()方法中进行差异化的:
1 | |
绘图流程
数据驱动
数据驱动的理念,在 ZRender 中和 D3 中,都是一样的,核心都是将数据分为三个类别:enter、update、exit,针对不同类型的数据,实现渲染/动画函数,并将数据处理和渲染分离开。
为什么要有数据驱动,而不是删除重建:动画和图形的连续性。
下面以一个柱状图动画为例进行讲解。
扩展
分而治之的图形定义策略允许你扩展自己独有的图形元素,你既可以完整实现三个接口方法(brush、drift、isCover), 也可以通过 base 派生后仅实现你所关心的图形细节。
常见功能的实现原理
坐标轴
ZRender 画坐标轴,是通过画一条长线(Line)+N个刻度线(Line)+N个坐标轴数值文本(Text)来实现的,那么在页面缩放的时候,这些数值是如何自动调整的呢?
1 | |
渐变
渐变是通过将 shape 的 style.fill 属性设置为一个渐变元素来实现的:
1 | |
扩展新的图形
分而治之的图形定义策略允许你扩展自己独有的图形元素,你既可以完整实现三个接口方法(brush、drift、isCover), 也可以通过 base 派生后仅实现你所关心的图形细节。
可以参考 graphic/helper/roundRect.ts 这个类,对于圆角矩形的实现方案。
参考这个案例:https://github.com/ecomfe/zrender/blob/master/test/pin.html
通过 Path 构建自定义图形
业务需求中经常会出现不规则图形,这种情况,使用 ZRender 默认提供的几何图形可能无法实现,就需要我们自己去扩展了,此时用 Path 就很合适。
ZRender 支持不同类型的渲染器(Painter),如果想要用 Path 绘图,那么我们需要获取到渲染器对象 SVGPathRebuilder 的引用,才能调用偏底层的绘图函数,比如 arc、moveTo、lineTo 这种。
但是 ZRender 并没有对外提供获取 SVGPathRebuilder 的方法,那我们怎么才能获取 SVGPathRebuilder 的引用呢?
答案就是通过 zrender.Path.extend()扩展新的 Path 类图形,这样在渲染该图形的时候,扩展类的 buildPath(ctx, shape)方法中,ZRender 会默认将 SVGPathRebuilder 引用作为第一个参数传入进去,我们就可以用这个引用绘图了。
绘制自定义图形的过程
svg/Painter.ts 中:
1 | |
svg/graphic.ts 中:
1 | |
一个示例
比如我要扩展一个带圆角的矩形,就可以这样编写代码:
1 | |
变形动画(morphPath)
可以通过这个功能,实现很炫酷的形变动画效果!
对应的 API 是 zrender.morphPath:
1 | |
可以参考 zrender 代码库下的 test/morphPath.html 这个示例,源码在 zrender/src/tool/morphPath.ts 中。
从源码来看,使用了贝赛尔曲线,会通过新老图形的 path 信息,寻找一个合适的点,将老图形往这个点集合,然后再从这个点扩展到新图形。
ZRender4 VS. ZRender5
| 内容 | ZRender4 | ZRender5 | 备注 |
|---|---|---|---|
| 语言 | JavaScript | TypeScript | |
| 继承方式 | 原型链 | class | |
几个代码层面需要注意的地方:
1、zrender4 的 Canvas 模式不支持 path.beginPath(),如果加上这一行,会导致第一次图形绘制不出来。
注意事项
初始页面元素必须设置高宽,否则图形会因为 height=0 无法显示,因为 zrender.init(domElement)是根据传入的 DOM 元素的属性来设置高宽的。
怎么在网页中调试 ZRender 呢?比如我想知道页面上的图形,具体是由哪些元素(shape)构成的、每个元素的层级是什么?
元素初始化之后,需要通过 zrender.Element.attr 修改属性
官方文档明确说明了这一点。初始化之后,直接修改 style 是不生效的。
感触
因为之前看过 D3.js,感觉 ZRender 就是 Canvas 版本的 D3.js 的作图部分,学起来非常快,因为很多概念都是想通的。
可以通过 ZRender+D3 结合的方式来作图,D3 负责做数据计算,ZRender 负责做渲染。