闽公网安备 35020302035485号
import { Circle, Canvas, CanvasEvent } from '@antv/g';
import { Renderer as CanvasRenderer } from '@antv/g-canvas';
// or
// import { Renderer as WebGLRenderer } from '@antv/g-webgl';
// import { Renderer as SVGRenderer } from '@antv/g-svg';
// 堆代码 duidaima.com
// 创建画布
const canvas = new Canvas({
container: 'container',
width: 500,
height: 500,
renderer: new CanvasRenderer(), // 选择一个渲染器
});
// 创建一个圆
const circle = new Circle({
style: {
cx: 100,
cy: 100,
r: 50,
fill: 'red',
stroke: 'blue',
lineWidth: 5,
},
});
canvas.addEventListener(CanvasEvent.READY, function () {
// 加入画布
canvas.appendChild(circle);
// 监听 `click` 事件
circle.addEventListener('click', function () {
this.style.fill = 'green';
});
});
在此基础上,可以进一步针对 React/Vue 语法进行封装,让用户对底层的实现无感知。使用 React-Konva 的例子(通过 react-reconciler 实现): import React, { Component } from 'react';
import { render } from 'react-dom';
import { Stage, Layer, Rect, Text } from 'react-konva';
import Konva from 'konva';
class ColoredRect extends React.Component {
state = {
color: 'green',
};
handleClick = () => {
this.setState({
color: Konva.Util.getRandomColor(),
});
};
render() {
return (
<Rect
x={20}
y={20}
width={50}
height={50}
fill={this.state.color}
shadowBlur={5}
onClick={this.handleClick}
/>
);
}
}
class App extends Component {
render() {
return (
<Stage width={window.innerWidth} height={window.innerHeight}>
<Layer>
<Text text="Try click on rect" />
<ColoredRect />
</Layer>
</Stage>
);
}
}
render(<App />, document.getElementById('root'));
除了内置的图形类,很多渲染引擎还会提供自定义绘制图形类的能力。以 Konva 为例,每个图形类都需要实现 sceneFunc 方法,在这个方法里面去调用 Canvas API 来进行绘制。如果需要自定义新的图形,就可以继承 Shape 来实现 sceneFunc 方法。 export class Circle extends Shape<CircleConfig> {
_sceneFunc(context) {
context.beginPath();
context.arc(0, 0, this.attrs.radius || 0, 0, Math.PI * 2, false);
context.closePath();
context.fillStrokeShape(this);
}
}
参照 DOM 树的结构,每个 Konva 应用包括一个舞台 Stage、多个画布 Layer、多个分组 Group,以及若干的叶子节点 Shape,这些虚拟节点关联起来最终形成了一棵树。





const container = new Rect({
style: {
width: 500, // Size
height: 300,
display: 'flex', // Declaring the use of flex layouts
justifyContent: 'center',
alignItems: 'center',
x: 0,
y: 0,
fill: '#C6E5FF',
},
});
在腾讯开源的 Hippy 里面自己实现了一套类似 Yoga 的排版引擎,叫做 Titank。在飞书文档多维表格里面,排版语法更加接近于 Flutter,实现了 Padding、Column、Row、Margin、Expanded、Flex、GridView 等 Widget。 Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 对齐方式
children: [
Icon(Icons.star, color: Colors.green[500]),
Icon(Icons.star, color: Colors.green[500]),
Icon(Icons.star, color: Colors.green[500]),
const Icon(Icons.star, color: Colors.black),
const Icon(Icons.star, color: Colors.black),
],
);
Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: red),
)
Padding(
padding: EdgeInsets.fromLTRB(30, 30, 0, 30),
child: Image.network(
"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1581413255772&di=52021e3e656744094d0339e7016994bb&imgtype=0&src=http%3A%2F%2Fimg8.zol.com.cn%2Fbbs%2Fupload%2F19571%2F19570481.jpg",
fit: BoxFit.cover,
),
)
Widget _buildGrid() => GridView.extent(
maxCrossAxisExtent: 150,
padding: const EdgeInsets.all(4),
mainAxisSpacing: 4,
crossAxisSpacing: 4,
children: _buildGridTileList(30));
实现了盒模型和 Flex 布局,可以让 Canvas 的排版能力更上一层楼。不仅可以减少代码中的大量计算,也可以让大家从 DOM 开发无缝衔接进来,值得我们参考。https://codesandbox.io/s/canvas-flexbox-2e9sp?file=/src/index.ts getRandomColor() {
var randColor = ((Math.random() * 0xffffff) << 0).toString(16);
while (randColor.length < 6) {
randColor = ZERO + randColor;
}
return HASH + randColor;
},
.绘制的同时会在内存里的 hitCanvas 同样位置绘制一个一模一样的图形,填充色是刚才的 colorKey。



const rect = new Rect({ /... });
// 多次修改属性,可能会触发多次渲染
rect.x(100);
rect.fill('red');
rect.y(100);
由于每次修改图形的属性或者添加、销毁子节点都会触发渲染,为了避免同时修改多个属性时导致的重复渲染,因此约定每次在下一帧进行批量绘制。 batchDraw() {
if (!this._waitingForDraw) {
this._waitingForDraw = true;
Util.requestAnimFrame(() => {
this.draw();
this._waitingForDraw = false;
});
}
return this;
}
这种渲染方式类似于 React 的 setState,避免短时间内多次 setState 导致多次 render。

3.下次 batchDraw 的时候判断是否有缓存,如果有,那么直接走 drawImage 的形式。
3.由于使用色值法来匹配图形,导致开启了离屏渲染,实际上至少要绘制四份(主 canvas、事件 hitCanvas、离屏 cacheCanvas、离屏事件 cacheHitCanvas)。



import { Renderer as CanvaskitRenderer } from '@antv/g-canvaskit';
const canvaskitRenderer = new CanvaskitRenderer();
关于跨平台的架构这里不做讲解,主要是抹平不同平台的差异,这里主要讲解一下针对于服务端渲染的不同处理。主流的服务端渲染方式有两种,一种是用 node-canvas 来输出一张图片,在 echarts 等库中都有使用,缺陷在于文本排版不够准确,对于自适应浏览器窗口的情况无法处理。因此它不适用于文档直出的场景。 const { createCanvas, loadImage } = require('canvas')
const canvas = createCanvas(200, 200)
const ctx = canvas.getContext('2d')
// Write "Awesome!"
ctx.font = '30px Impact'
ctx.rotate(0.1)
ctx.fillText('Awesome!', 50, 100)
// Draw line under text
var text = ctx.measureText('Awesome!')
ctx.strokeStyle = 'rgba(0,0,0,0.5)'
ctx.beginPath()
ctx.lineTo(50, 102)
ctx.lineTo(50 + text.width, 102)
ctx.stroke()
// Draw cat with lime helmet
loadImage('examples/images/lime-cat.jpg').then((image) => {
ctx.drawImage(image, 50, 0, 70, 70)
console.log('<img src="' + canvas.toDataURL() + '" />')
})
另一种就是通过 SVG 来模拟 Canvas 的效果,输出 SVG DOM 字符串。但它的实现会比较麻烦,也无法 100% 还原 Canvas 的效果。但很多 Canvas 渲染引擎本身也支持 SVG 渲染,即使不支持,也可以通过 canvas2svg 这个库来进行转换。var ctx = new C2S(500,500); //draw your canvas like you would normally ctx.fillStyle="red"; ctx.fillRect(100,100,100,100); //serialize your SVG var mySerializedSVG = ctx.getSerializedSvg(); //If you really need to you can access the shadow inline SVG created by calling: var svg = ctx.getSvg();对于更加通用的场景来说,在浏览器端使用 Canvas 渲染,服务端使用 SVG 渲染是更合理的形式。在新版 ECharts 里面,针对 SVG 服务端渲染的能力,还支持了 Virtual DOM 来代替 JSDOM,最后转换成 DOM 字符串。在飞书文档中使用了一种完全独立于 node-canvas 和 SVG 的解决方式,非常值得我们借鉴。由于飞书多维表格底层统一了渲染引擎,所有绘制元素都是 Widget(对齐 Flutter),可以脱水转换成下面 FVG 格式。
