入门zrender
zrender是Echarts底层的2D绘图引擎,在搞懂其原理之前,我们先学会如何使用zrender,我们从绘制一个简单圆形入门。这里也给出官网入门教程
初始化
zrender.init(dom)
初始化zrender实例,入参是DOM容器
var zr = zrender.init(document.getElementById('container'));
创建Circle元素
通过new zrender.Circle(opts)
创建Circle元素,入参用来配置circle元素的属性
var circle = new zrender.Circle({
shape: {
cx: 150, // 圆心x坐标
cy: 50, // 圆心y坐标
r: 40 // 圆半径
},
style: {
fill: 'none', // 是否填充
stroke: '#000' // 线条颜色
}
});
将元素添加到zrender实例中
zr.add(circle);
将元素添加到zrender实例中,圆形便在页面中呈现。
zrender的使用方法很简单,最终效果:
绘制原理
我们可以在这里获取到zrender项目源码,在了解具体的绘制原理前,我们先了解zrender的整体结构:
zrender采用MVC结构,M(Model)数据层,用来管理图形数据的增删改查;V(View)视图层,主要负责图形渲染;C(Controller)控制层,主要实现事件交互。
以下是zrender的主要文件:
.
├── Element.ts // 各种图形的基类
├── Handler.ts // Controller,控制层
├── PainterBase.ts //
├── Storage.ts // Model数据层
├── all.ts
├── animation // 动画相关
├── canvas // View视图层相关,主要负责绘制渲染图形
│ ├── Layer.ts
│ ├── Painter.ts
│ ├── canvas.ts
│ ├── dashStyle.ts
│ ├── graphic.ts
│ └── helper.ts
├── config.ts // 配置文件
├── contain // 包含判断
│ ├── arc.ts
│ ├── cubic.ts
│ ├── line.ts
│ ├── path.ts
│ ├── polygon.ts
│ ├── quadratic.ts
│ ├── text.ts
│ ├── util.ts
│ └── windingLine.ts
├── core // 核心与工具代码
│ ├── BoundingRect.ts
│ ├── Eventful.ts
│ ├── GestureMgr.ts
│ ├── LRU.ts
│ ├── OrientedBoundingRect.ts
│ ├── PathProxy.ts // 绘制相关
│ ├── Point.ts
│ ├── Transformable.ts
│ ├── WeakMap.ts
│ ├── arrayDiff.ts
│ ├── bbox.ts
│ ├── curve.ts
│ ├── dom.ts
│ ├── env.ts
│ ├── event.ts // 事件
│ ├── fourPointsTransform.ts
│ ├── matrix.ts
│ ├── platform.ts
│ ├── timsort.ts
│ ├── types.ts
│ ├── util.ts // utils.guid() 生成唯一ID
│ └── vector.ts
├── debug
│ └── showDebugDirtyRect.ts
├── dom
│ └── HandlerProxy.ts // 与dom事件有关
├── export.ts
├── global.d.ts
├── graphic // 图形相关
│ ├── CompoundPath.ts
│ ├── Displayable.ts // 图形的基类
│ ├── Gradient.ts
│ ├── Group.ts
│ ├── Image.ts
│ ├── IncrementalDisplayable.ts
│ ├── LinearGradient.ts
│ ├── Path.ts
│ ├── Pattern.ts
│ ├── RadialGradient.ts
│ ├── TSpan.ts
│ ├── Text.ts
│ ├── constants.ts
│ ├── helper
│ └── shape // 定义了各种图形
│ ├── Arc.ts
│ ├── BezierCurve.ts
│ ├── Circle.ts
│ ├── Droplet.ts
│ ├── Ellipse.ts
│ ├── Heart.ts
│ ├── Isogon.ts
│ ├── Line.ts
│ ├── Polygon.ts
│ ├── Polyline.ts
│ ├── Rect.ts
│ ├── Ring.ts
│ ├── Rose.ts
│ ├── Sector.ts
│ ├── Star.ts
│ └── Trochoid.ts
├── mixin
├── svg
├── svg-legacy
├── tool
└── zrender.ts // 入口
接下来我们研究zrender初始化时做了什么事情:
/**
* Initializing a zrender instance
*
* @param dom Not necessary if using SSR painter like svg-ssr
*/
export function init(dom?: HTMLElement | null, opts?: ZRenderInitOpt) {
// 调用构造函数(id, dom, opts)
const zr = new ZRender(zrUtil.guid(), dom, opts);
// instances数组用来存放zrender实例
instances[zr.id] = zr;
return zr;
}
通过源码可见init函数接收dom和opt参数,通过ZRender构造方法创建zrender实例,然后将实例存储在数组中。初始化参数opt有如下选项(都是可选的)
名称 | 类型 | 默认值 | 描述 |
---|---|---|---|
renderer | string | 'canvas' | 渲染方式,支持:'canavs' 、'svg' 、'vml' |
devicePixelRatio | number | 2 | 画布大小与容器大小之比,仅当 renderer 为 'canvas' 时有效。 |
width | number|string | 'auto' | 画布宽度,设为 'auto' 则根据 devicePixelRatio 与容器宽度自动计算。 |
height | number|string | 'auto' | 画布高度,设为 'auto' 则根据 devicePixelRatio 与容器高度自动计算。 |
useCoarsePointer | 'auto'|boolean | 'auto' | (5.4.0 版本起支持)是否扩大可点击元素的响应范围。'auto' 表示对移动设备开启;true 表示总是开启;false 表示总是不开启。 |
pointerSize | number | 44 | 扩大元素响应范围的像素大小,配合 opts.useCoarsePointer 使用。 |
useDirtyRec | boolean | 'false' | |
ssr | boolean | 'false' | 是否支持ssr模式 |
zrUtil.guid()
用来创建实例的唯一ID:
let idStart = 0x0907;
export function guid(): number {
return idStart++;
}
ZRender的构造函数如下:
constructor(id: number, dom?: HTMLElement, opts?: ZRenderInitOpt) {
opts = opts || {};
/**
* @type {HTMLDomElement}
*/
this.dom = dom;
this.id = id;
// 初始化zrender数据层
const storage = new Storage();
// 渲染方式,默认canvas
let rendererType = opts.renderer || 'canvas';
// painterCtors 用来存储渲染方式 见all.ts
if (!painterCtors[rendererType]) {
// Use the first registered renderer.
rendererType = zrUtil.keys(painterCtors)[0];
}
if (process.env.NODE_ENV !== 'production') {
if (!painterCtors[rendererType]) {
throw new Error(`Renderer '${rendererType}' is not imported. Please import it first.`);
}
}
// default: false
opts.useDirtyRect = opts.useDirtyRect == null
? false
: opts.useDirtyRect;
// 初始化painter 初始化View层
const painter = new painterCtors[rendererType](dom, storage, opts, id);
const ssrMode = opts.ssr || painter.ssrOnly;
this.storage = storage;
this.painter = painter;
// 初始化Controller相关
const handerProxy = (!env.node && !env.worker && !ssrMode)
? new HandlerProxy(painter.getViewportRoot(), painter.root)
: null;
// default: false
const useCoarsePointer = opts.useCoarsePointer;
const usePointerSize = (useCoarsePointer == null || useCoarsePointer === 'auto')
? env.touchEventsSupported
: !!useCoarsePointer;
const defaultPointerSize = 44;
// 点击元素影响范围大小
let pointerSize;
if (usePointerSize) {
pointerSize = zrUtil.retrieve2(opts.pointerSize, defaultPointerSize);
}
// 初始化Controller相关
this.handler = new Handler(storage, painter, handerProxy, painter.root, pointerSize);
// 动画 非ssr模式下绑定animation.stage = this._flush(true), 与刷新渲染相关
this.animation = new Animation({
stage: {
update: ssrMode ? null : () => this._flush(true)
}
});
// 非ssr模式下开始动画
if (!ssrMode) {
this.animation.start();
}
}
我们由this.animation.start()
定位到animation._startLoop()
方法:
_startLoop() {
const self = this;
this._running = true;
function step() {
if (self._running) {
requestAnimationFrame(step);
!self._paused && self.update();
}
}
requestAnimationFrame(step);
}
可见,_startLoop()
方法中,只要_running
和_pause
为true
(也就是动画开始且未暂停),就不断通过Animation.update()
方法更新渲染页面。更多有关Animation更新方式,我们之后再详细了解。
我们将思绪拉回圆形创建过程,初始化zrender实例后,我们通过new zrender.Circle(opts)
创建Circle元素。Circle的实现在src/graphic/shape
目录下,您可以在该目录下找到更多图形的实现。
图形元素的继承关系如下,其中Element是画布中元素的最基本单位
Circle的构造函数调用其父类Element类的构造函数,将传入的参数opts映射到circle实例的属性上,同时Circle类中有buildPath方法如下,该方法定义了如何通过原生canvas画出我们创建的Circle元素。
最后,我们将Circle元素添加到zrender实例中,zrender.add()源码如下:
/**
* 添加元素
*/
add(el: Element) {
if (!el) {
return;
}
this.storage.addRoot(el);
el.addSelfToZr(this);
this.refresh();
}
可以看出add()方法做了3件事:
- 保存新元素:通过
this.storage.addRoot(el)
将元素保存在storage._root数组中 - 将元素绑定到zrender实例:el._zr属性绑定到当前zrender实例,如果元素有动画,将动画添加到zrender动画中;将元素自身属性也绑定到zrender实例。
- zrender.refresh,调用animation.start()更新渲染
细说如何更新渲染
// Animation.update
update(notTriggerFrameAndStageUpdate?: boolean) {
const time = getTime() - this._pausedTime;
const delta = time - this._time;
let clip = this._head;
while (clip) {
// Save the nextClip before step.
// So the loop will not been affected if the clip is removed in the callback
const nextClip = clip.next;
let finished = clip.step(time, delta);
if (finished) {
clip.ondestroy();
this.removeClip(clip);
clip = nextClip;
}
else {
clip = nextClip;
}
}
this._time = time;
if (!notTriggerFrameAndStageUpdate) {
// 'frame' should be triggered before stage, because upper application
// depends on the sequence (e.g., echarts-stream and finish
// event judge)
this.trigger('frame', delta);
this.stage.update && this.stage.update();
}
}
暂不考虑有动画的复杂场景,最简单情况,zrender触发‘frame’事件,然后this.stage.update()
方法中会调用el.buildPath()
方法绘制图形:
// Circle.buildPath
buildPath(ctx: CanvasRenderingContext2D, shape: CircleShape) {
// Use moveTo to start a new sub path.
// Or it will be connected to other subpaths when in CompoundPath
ctx.moveTo(shape.cx + shape.r, shape.cy);
ctx.arc(shape.cx, shape.cy, shape.r, 0, Math.PI * 2);
}
如此圆形绘制成功。
我们也可以用原生实现同样效果:
const canvas = document.createElement("canvas");
const el = document.getElementById('container')
el.appendChild(canvas);
const ctx = canvas.getContext("2d");
let shape = {
cx: 150,
cy: 50,
r: 40
}
ctx.strokeStyle = "#000";
ctx.beginPath()
ctx.moveTo(shape.cx + shape.r, shape.cy);
ctx.arc(shape.cx, shape.cy, shape.r, 0, Math.PI * 2);
ctx.stroke()
ctx.closePath()
参考文章: