0%

ECharts源码解析之SVGRender

版本:V5.3.2

背景

ECharts底层渲染引擎为zrenderzrender的默认渲染器为canvas。但同时支持svg渲染。本文通过一个简单的饼图示例来简单探究下echartszrender是如何实现同时支持canvassvg渲染的。

image-20220526100211246

源码

Path: /src/svg/Painter.ts,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 这里将 待渲染的元素做了渲染
_paintList(list: Displayable[], scope: BrushScope, out?: SVGVNode[]) {
const listLen = list.length;

const clipPathsGroupsStack: SVGVNode[] = [];
let clipPathsGroupsStackDepth = 0;
let currentClipPathGroup;
let prevClipPaths: Path[];
let clipGroupNodeIdx = 0;
for (let i = 0; i < listLen; i++) {
const displayable = list[i];
if (!displayable.invisible) {
const clipPaths = displayable.__clipPaths;
const len = clipPaths && clipPaths.length || 0;
const prevLen = prevClipPaths && prevClipPaths.length || 0;
let lca;
// Find the lowest common ancestor
for (lca = Math.max(len - 1, prevLen - 1); lca >= 0; lca--) {
if (clipPaths && prevClipPaths
&& clipPaths[lca] === prevClipPaths[lca]
) {
break;
}
}
// pop the stack
for (let i = prevLen - 1; i > lca; i--) {
clipPathsGroupsStackDepth--;
// svgEls.push(closeGroup);
currentClipPathGroup = clipPathsGroupsStack[clipPathsGroupsStackDepth - 1];
}
// Pop clip path group for clipPaths not match the previous.
for (let i = lca + 1; i < len; i++) {
const groupAttrs: SVGVNodeAttrs = {};
setClipPath(
clipPaths[i],
groupAttrs,
scope
);
const g = createVNode(
'g',
'clip-g-' + clipGroupNodeIdx++,
groupAttrs,
[]
);
(currentClipPathGroup ? currentClipPathGroup.children : out).push(g);
clipPathsGroupsStack[clipPathsGroupsStackDepth++] = g;
currentClipPathGroup = g;
}
prevClipPaths = clipPaths;
// 这里在渲染待渲染的元素
const ret = brush(displayable, scope);
if (ret) {
(currentClipPathGroup ? currentClipPathGroup.children : out).push(ret);
}
}
}
}

Path:/src/svg/graphic.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 这里将待渲染的元素分为了三类 path image 和 tspan
// 由于echarts本身是基于canvas在渲染 所以 这里将大多数元素都作为path来渲染了
// 将moveTo lineTo等命令解析成 svg 的 path属性相对容易
export function brush(el: Displayable, scope: BrushScope): SVGVNode {
if (el instanceof Path) {
return brushSVGPath(el, scope);
}
else if (el instanceof ZRImage) {
return brushSVGImage(el, scope);
}
else if (el instanceof TSpan) {
return brushSVGTSpan(el, scope);
}
}

Path:/src/svg/graphic.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 这里在渲染SVG的Path
export function brushSVGPath(el: Path, scope: BrushScope) {
const style = el.style;
const shape = el.shape;
const builtinShpDef = buitinShapesDef[el.type];
const attrs: SVGVNodeAttrs = {};
const needsAnimate = scope.animation;
let svgElType = 'path';
const strokePercent = el.style.strokePercent;
const precision = (scope.compress && getPathPrecision(el)) || 4;
// Using SVG builtin shapes if possible
if (builtinShpDef
// Force to use path if it will update later.
// To avoid some animation(like morph) fail
&& !scope.willUpdate
&& !(builtinShpDef[1] && !builtinShpDef[1](shape))
// use `path` to simplify the animate element creation logic.
&& !(needsAnimate && hasShapeAnimation(el))
&& !(strokePercent < 1)
) {
svgElType = el.type;
const mul = Math.pow(10, precision);
builtinShpDef[0](shape, attrs, mul);
}
else {
if (!el.path) {
el.createPathProxy();
}
const path = el.path;

if (el.shapeChanged()) {
// 这里在创建了Path canvas 指令数组
path.beginPath();
el.buildPath(path, el.shape);
el.pathUpdated();
}
const pathVersion = path.getVersion();
const elExt = el as PathWithSVGBuildPath;

let svgPathBuilder = elExt.__svgPathBuilder;
if (elExt.__svgPathVersion !== pathVersion
|| !svgPathBuilder
|| strokePercent !== elExt.__svgPathStrokePercent
) {
if (!svgPathBuilder) {
svgPathBuilder = elExt.__svgPathBuilder = new SVGPathRebuilder();
}
svgPathBuilder.reset(precision);
// 这里将 canvas 的指令数组 转译成了 svg Path的d属性
path.rebuildPath(svgPathBuilder, strokePercent);
svgPathBuilder.generateStr();
elExt.__svgPathVersion = pathVersion;
elExt.__svgPathStrokePercent = strokePercent;
}

attrs.d = svgPathBuilder.getStr();
}

setTransform(attrs, el.transform);
setStyleAttrs(attrs, style, el, scope);

scope.animation && createCSSAnimation(el, attrs, scope);

return createVNode(svgElType, el.id + '', attrs);
}

Path: /src/graphic/shape/Rect.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 这里在创建 Path
buildPath(ctx: CanvasRenderingContext2D, shape: RectShape) {
let x: number;
let y: number;
let width: number;
let height: number;

if (this.subPixelOptimize) {
const optimizedShape = subPixelOptimizeRect(subPixelOptimizeOutputShape, shape, this.style);
x = optimizedShape.x;
y = optimizedShape.y;
width = optimizedShape.width;
height = optimizedShape.height;
optimizedShape.r = shape.r;
shape = optimizedShape;
}
else {
x = shape.x;
y = shape.y;
width = shape.width;
height = shape.height;
}

if (!shape.r) {
ctx.rect(x, y, width, height);
}
else {
// 这里在创建 圆角矩形的Path
roundRectHelper.buildPath(ctx, shape);
}
}

Path:/src/graphic/helper/roundRect.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// 这里在创建圆角矩形的path
export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: {
x: number
y: number
width: number
height: number
r?: number | number[]
}) {
let x = shape.x;
let y = shape.y;
let width = shape.width;
let height = shape.height;
let r = shape.r;
let r1;
let r2;
let r3;
let r4;

// Convert width and height to positive for better borderRadius
if (width < 0) {
x = x + width;
width = -width;
}
if (height < 0) {
y = y + height;
height = -height;
}

if (typeof r === 'number') {
r1 = r2 = r3 = r4 = r;
}
else if (r instanceof Array) {
if (r.length === 1) {
r1 = r2 = r3 = r4 = r[0];
}
else if (r.length === 2) {
r1 = r3 = r[0];
r2 = r4 = r[1];
}
else if (r.length === 3) {
r1 = r[0];
r2 = r4 = r[1];
r3 = r[2];
}
else {
r1 = r[0];
r2 = r[1];
r3 = r[2];
r4 = r[3];
}
}
else {
r1 = r2 = r3 = r4 = 0;
}

let total;
if (r1 + r2 > width) {
total = r1 + r2;
r1 *= width / total;
r2 *= width / total;
}
if (r3 + r4 > width) {
total = r3 + r4;
r3 *= width / total;
r4 *= width / total;
}
if (r2 + r3 > height) {
total = r2 + r3;
r2 *= height / total;
r3 *= height / total;
}
if (r1 + r4 > height) {
total = r1 + r4;
r1 *= height / total;
r4 *= height / total;
}
// 这里在执行 构建圆角矩形的指令
ctx.moveTo(x + r1, y);
ctx.lineTo(x + width - r2, y);
r2 !== 0 && ctx.arc(x + width - r2, y + r2, r2, -Math.PI / 2, 0);
ctx.lineTo(x + width, y + height - r3);
r3 !== 0 && ctx.arc(x + width - r3, y + height - r3, r3, 0, Math.PI / 2);
ctx.lineTo(x + r4, y + height);
r4 !== 0 && ctx.arc(x + r4, y + height - r4, r4, Math.PI / 2, Math.PI);
ctx.lineTo(x, y + r1);
r1 !== 0 && ctx.arc(x + r1, y + r1, r1, Math.PI, Math.PI * 1.5);
}

Path: /src/core/PathProxy.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 由于 PathProxy 的指令较多 这里只拿moveTo来举例
moveTo(x: number, y: number) {
// Add pending point for previous path.
this._drawPendingPt();

this.addData(CMD.M, x, y);
this._ctx && this._ctx.moveTo(x, y);

// x0, y0, xi, yi 是记录在 _dashedXXXXTo 方法中使用
// xi, yi 记录当前点, x0, y0 在 closePath 的时候回到起始点。
// 有可能在 beginPath 之后直接调用 lineTo,这时候 x0, y0 需要
// 在 lineTo 方法中记录,这里先不考虑这种情况,dashed line 也只在 IE10- 中不支持
this._x0 = x;
this._y0 = y;

this._xi = x;
this._yi = y;

return this;
}
/**
* 填充 Path 数据。
* 尽量复用而不申明新的数组。大部分图形重绘的指令数据长度都是不变的。
*/
addData(
cmd: number,
a?: number,
b?: number,
c?: number,
d?: number,
e?: number,
f?: number,
g?: number,
h?: number
) {
if (!this._saveData) {
return;
}

let data = this.data;
if (this._len + arguments.length > data.length) {
// 因为之前的数组已经转换成静态的 Float32Array
// 所以不够用时需要扩展一个新的动态数组
this._expandData();
data = this.data;
}
for (let i = 0; i < arguments.length; i++) {
// 这里复写了 data 中的指令行
data[this._len++] = arguments[i];
}
}
// 指令集
// const CMD = { M: 1, L: 2, C: 3, Q: 4, A: 5, Z: 6, R: 7};

// 构建 canvas path 指令集 的结果举例:
// [ 1, 16, -16, 2, 37.40794372558594, -16, 5, 37.40794372558594, 0, 16, 16, 4.71238898038469, 1.5707963267948966, 0, 1, 2, 53.40794372558594, 0, 5, 37.40794372558594, 0, 16, 16, 0, 1.5707963267948966, 0, 1, 2, 16, 16, 5, 16, 0, 16, 16, 1.5707963267948966, 1.5707963267948966, 0, 1, 2, 0, 0, 5, 16, 0, 16, 16, 3.141592653589793, 1.5707963267948966, 0, 1 ]

// 翻译为 svg path 的 d 属性为
// [ "M16 -16", "L37.4079 -16", "A16 16 0 0 1 53.4079 0", "L53.4079 0", "A16 16 0 0 1 37.4079 16", "L16 16", "A16 16 0 0 1 0 0", "L0 0", "A16 16 0 0 1 16 -16"]

Path: /src/core/PathProxy.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
// 这里的 ctx 已经不是类 canvas 的 ctx 而是类Svg的ctx
rebuildPath(ctx: PathRebuilder, percent: number) {
const d = this.data;
const ux = this._ux;
const uy = this._uy;
const len = this._len;
let x0;
let y0;
let xi;
let yi;
let x;
let y;

const drawPart = percent < 1;
let pathSegLen;
let pathTotalLen;
let accumLength = 0;
let segCount = 0;
let displayedLength;

let pendingPtDist = 0;
let pendingPtX: number;
let pendingPtY: number;


if (drawPart) {
if (!this._pathSegLen) {
this._calculateLength();
}
pathSegLen = this._pathSegLen;
pathTotalLen = this._pathLen;
displayedLength = percent * pathTotalLen;

if (!displayedLength) {
return;
}
}
// 这里在遍历指令集
lo: for (let i = 0; i < len;) {
const cmd = d[i++];
const isFirst = i === 1;

if (isFirst) {
// 如果第一个命令是 L, C, Q
// 则 previous point 同绘制命令的第一个 point
// 第一个命令为 Arc 的情况下会在后面特殊处理
xi = d[i];
yi = d[i + 1];

x0 = xi;
y0 = yi;
}
// Only lineTo support ignoring small segments.
// Otherwise if the pending point should always been flushed.
if (cmd !== CMD.L && pendingPtDist > 0) {
ctx.lineTo(pendingPtX, pendingPtY);
pendingPtDist = 0;
}
switch (cmd) {
case CMD.M:
x0 = xi = d[i++];
y0 = yi = d[i++];
// 这里调用了svg的ctx的moveTo
ctx.moveTo(xi, yi);
break;
case CMD.L: {
x = d[i++];
y = d[i++];
const dx = mathAbs(x - xi);
const dy = mathAbs(y - yi);
// Not draw too small seg between
if (dx > ux || dy > uy) {
if (drawPart) {
const l = pathSegLen[segCount++];
if (accumLength + l > displayedLength) {
const t = (displayedLength - accumLength) / l;
ctx.lineTo(xi * (1 - t) + x * t, yi * (1 - t) + y * t);
break lo;
}
accumLength += l;
}

ctx.lineTo(x, y);
xi = x;
yi = y;
pendingPtDist = 0;
}
else {
const d2 = dx * dx + dy * dy;
// Only use the farthest pending point.
if (d2 > pendingPtDist) {
pendingPtX = x;
pendingPtY = y;
pendingPtDist = d2;
}
}
break;
}
case CMD.C: {
const x1 = d[i++];
const y1 = d[i++];
const x2 = d[i++];
const y2 = d[i++];
const x3 = d[i++];
const y3 = d[i++];
if (drawPart) {
const l = pathSegLen[segCount++];
if (accumLength + l > displayedLength) {
const t = (displayedLength - accumLength) / l;
cubicSubdivide(xi, x1, x2, x3, t, tmpOutX);
cubicSubdivide(yi, y1, y2, y3, t, tmpOutY);
ctx.bezierCurveTo(tmpOutX[1], tmpOutY[1], tmpOutX[2], tmpOutY[2], tmpOutX[3], tmpOutY[3]);
break lo;
}
accumLength += l;
}

ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3);
xi = x3;
yi = y3;
break;
}
case CMD.Q: {
const x1 = d[i++];
const y1 = d[i++];
const x2 = d[i++];
const y2 = d[i++];

if (drawPart) {
const l = pathSegLen[segCount++];
if (accumLength + l > displayedLength) {
const t = (displayedLength - accumLength) / l;
quadraticSubdivide(xi, x1, x2, t, tmpOutX);
quadraticSubdivide(yi, y1, y2, t, tmpOutY);
ctx.quadraticCurveTo(tmpOutX[1], tmpOutY[1], tmpOutX[2], tmpOutY[2]);
break lo;
}
accumLength += l;
}

ctx.quadraticCurveTo(x1, y1, x2, y2);
xi = x2;
yi = y2;
break;
}
case CMD.A:
const cx = d[i++];
const cy = d[i++];
const rx = d[i++];
const ry = d[i++];
let startAngle = d[i++];
let delta = d[i++];
const psi = d[i++];
const anticlockwise = !d[i++];
const r = (rx > ry) ? rx : ry;
// const scaleX = (rx > ry) ? 1 : rx / ry;
// const scaleY = (rx > ry) ? ry / rx : 1;
const isEllipse = mathAbs(rx - ry) > 1e-3;
let endAngle = startAngle + delta;
let breakBuild = false;

if (drawPart) {
const l = pathSegLen[segCount++];
if (accumLength + l > displayedLength) {
endAngle = startAngle + delta * (displayedLength - accumLength) / l;
breakBuild = true;
}
accumLength += l;
}
if (isEllipse && ctx.ellipse) {
ctx.ellipse(cx, cy, rx, ry, psi, startAngle, endAngle, anticlockwise);
}
else {
ctx.arc(cx, cy, r, startAngle, endAngle, anticlockwise);
}

if (breakBuild) {
break lo;
}

if (isFirst) {
// 直接使用 arc 命令
// 第一个命令起点还未定义
x0 = mathCos(startAngle) * rx + cx;
y0 = mathSin(startAngle) * ry + cy;
}
xi = mathCos(endAngle) * rx + cx;
yi = mathSin(endAngle) * ry + cy;
break;
case CMD.R:
x0 = xi = d[i];
y0 = yi = d[i + 1];

x = d[i++];
y = d[i++];
const width = d[i++];
const height = d[i++];

if (drawPart) {
const l = pathSegLen[segCount++];
if (accumLength + l > displayedLength) {
let d = displayedLength - accumLength;
ctx.moveTo(x, y);
ctx.lineTo(x + mathMin(d, width), y);
d -= width;
if (d > 0) {
ctx.lineTo(x + width, y + mathMin(d, height));
}
d -= height;
if (d > 0) {
ctx.lineTo(x + mathMax(width - d, 0), y + height);
}
d -= width;
if (d > 0) {
ctx.lineTo(x, y + mathMax(height - d, 0));
}
break lo;
}
accumLength += l;
}
ctx.rect(x, y, width, height);
break;
case CMD.Z:
if (drawPart) {
const l = pathSegLen[segCount++];
if (accumLength + l > displayedLength) {
const t = (displayedLength - accumLength) / l;
ctx.lineTo(xi * (1 - t) + x0 * t, yi * (1 - t) + y0 * t);
break lo;
}
accumLength += l;
}

ctx.closePath();
xi = x0;
yi = y0;
}
}
}

Path: /src/svg/SVGPathRebuilder.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 由于 SVGPathRebuilder 的指令较多 这里只拿moveTo来举例
moveTo(x: number, y: number) {
this._add('M', x, y);
}
_add(cmd: string, a?: number, b?: number, c?: number, d?: number, e?: number, f?: number, g?: number, h?: number) {
const vals = [];
const p = this._p;
for (let i = 1; i < arguments.length; i++) {
const val = arguments[i];
if (isNaN(val)) {
this._invalid = true;
return;
}
// 这里svg指令集中新增了新的指令
vals.push(Math.round(val * p) / p);
}
this._d.push(cmd + vals.join(' '));
this._start = cmd === 'Z';
}

Path: /src/svg/core.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 这里渲染了 svg 标签
export function createVNode(
tag: string,
key: string,
attrs?: SVGVNodeAttrs,
children?: SVGVNode[],
text?: string
): SVGVNode {
return {
tag,
attrs: attrs || {},
children,
text,
key
};
}

小结

在使用svg进行渲染时首先对元素的类型进行了区分,以使用不同的svg标签,这里为了使指令转译层尽量单薄仅抽象了三个svg标签即path,tspan,image。绝大多数元素都使用path来渲染。

zrendercanvas的API之上抽象了一层指令层例如将moveTo(x, y),lineTo(x1, y1)等抽象为了[1,x,y,2,x1,y1]

在使用canvas渲染时直接将指令层解析成指令进行渲染。

在使用svg渲染时首先svg渲染器实现了一道canvas like的api。渲染之前会首先调用这个api时会构建对应的svg标签属性。之后会基于svg的标签和属性进行正常的svg元素渲染。

参考 & 引用

SVG - Text with background color and rounded borders - Stack Overflow

css - Background color of text in SVG - Stack Overflow

Examples - Apache ECharts