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 格式。