日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
淺談 Canvas 渲染引擎設(shè)計(jì)

用過 Canvas 的都知道它的 API 比較多,使用起來也很麻煩,比如我想繪制一個(gè)圓形就要調(diào)一堆 API,對(duì)開發(fā)算不上友好。

創(chuàng)新互聯(lián)建站服務(wù)項(xiàng)目包括鹽山網(wǎng)站建設(shè)、鹽山網(wǎng)站制作、鹽山網(wǎng)頁制作以及鹽山網(wǎng)絡(luò)營銷策劃等。多年來,我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢(shì)、行業(yè)經(jīng)驗(yàn)、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機(jī)構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,鹽山網(wǎng)站推廣取得了明顯的社會(huì)效益與經(jīng)濟(jì)效益。目前,我們服務(wù)的客戶以成都為中心已經(jīng)輻射到鹽山省份的部分城市,未來相信會(huì)繼續(xù)擴(kuò)大服務(wù)區(qū)域并繼續(xù)獲得客戶的支持與信任!

為了解決這個(gè)痛點(diǎn),誕生了例如 PIXI、ZRender、Fabric 等 Canvas 庫,對(duì) Canvas API 進(jìn)行了一系列的封裝。

今天主要介紹一下社區(qū)幾個(gè)比較有代表性的 Canvas 渲染引擎的設(shè)計(jì)原理。

這篇文中不會(huì)從源碼講起,更像是一篇科普文章,介紹 Canvas 一些有趣的點(diǎn)。

1. 特性

Canvas 渲染引擎一般包括下面幾個(gè)特點(diǎn):

  1. 封裝

將 Canvas API 的調(diào)用封裝成更簡單、清晰的形式,貼近于我們使用 DOM 的方式。

比如想畫一個(gè)圓,直接調(diào)用封裝好的繪制方法就行了,我們不需要關(guān)心是如何繪制的。

  1. 性能

雖然封裝之后的 API 很貼近 HTML 語法,但也意味著開發(fā)者很難去做一些底層的性能優(yōu)化。因此,大部分 Canvas 渲染引擎都會(huì)內(nèi)置了一些性能優(yōu)化手段。

常見的性能優(yōu)化手段有離屏渲染、臟區(qū)渲染、異步渲染等等。

  1. 跨平臺(tái)

一些渲染引擎為了更加通用,在底層做了更多抽象,不僅支持 Canvas Renderer,甚至還支持 WebGL、WebGPU、SVG、CanvasKit、小程序等等,真正實(shí)現(xiàn)了一套代碼多種渲染。

針對(duì)底層的渲染流程和類進(jìn)行抽象化,在不同平臺(tái)具象化去實(shí)現(xiàn)具體的渲染邏輯,從而可以一套代碼,只要切換渲染器就能實(shí)現(xiàn)多平臺(tái)渲染。

2. 封裝

2.1 虛擬節(jié)點(diǎn)

Canvas 是一張畫布,里面的內(nèi)容都是自己調(diào)用 API 繪制的,所以更像是我們拿起畫筆來作畫。

目前主流的 Canvas 渲染引擎都會(huì)將要繪制的圖形封裝成類,以方便開發(fā)者去調(diào)用,復(fù)用性也比較強(qiáng)。調(diào)用方式類似于 DOM,每個(gè)實(shí)例可以當(dāng)做一個(gè)虛擬節(jié)點(diǎn)。

使用 AntV/g 的例子:

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';

// 創(chuàng)建畫布
const canvas = new Canvas({
container: 'container',
width: 500,
height: 500,
renderer: new CanvasRenderer(), // 選擇一個(gè)渲染器
});

// 創(chuàng)建一個(gè)圓
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);

// 監(jiān)聽 `click` 事件
circle.addEventListener('click', function () {
this.style.fill = 'green';
});
});

在此基礎(chǔ)上,可以進(jìn)一步針對(duì) React/Vue 語法進(jìn)行封裝,讓用戶對(duì)底層的實(shí)現(xiàn)無感知。

使用 React-Konva 的例子(通過 react-reconciler 實(shí)現(xiàn)):

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 (
x={20}
y={20}
width={50}
height={50}
fill={this.state.color}
shadowBlur={5}
onClick={this.handleClick}
/>
);
}
}

class App extends Component {
render() {
return (






);
}
}

render(, document.getElementById('root'));

除了內(nèi)置的圖形類,很多渲染引擎還會(huì)提供自定義繪制圖形類的能力。

以 Konva 為例,每個(gè)圖形類都需要實(shí)現(xiàn) sceneFunc 方法,在這個(gè)方法里面去調(diào)用 Canvas API 來進(jìn)行繪制。

如果需要自定義新的圖形,就可以繼承 Shape 來實(shí)現(xiàn) sceneFunc 方法。

Konva 里面圓形繪制類的實(shí)現(xiàn):

export class Circle extends Shape {
_sceneFunc(context) {
context.beginPath();
context.arc(0, 0, this.attrs.radius || 0, 0, Math.PI * 2, false);
context.closePath();
context.fillStrokeShape(this);
}
}

參照 DOM 樹的結(jié)構(gòu),每個(gè) Konva 應(yīng)用包括一個(gè)舞臺(tái) Stage、多個(gè)畫布 Layer、多個(gè)分組 Group,以及若干的葉子節(jié)點(diǎn) Shape,這些虛擬節(jié)點(diǎn)關(guān)聯(lián)起來最終形成了一棵樹。

在 Konva 中,一個(gè) Stage 就是根節(jié)點(diǎn),Layer 對(duì)應(yīng)一個(gè) Canvas 畫布,Group 是指多個(gè) Shape 的集合,它本身不會(huì)進(jìn)行繪制,但同一個(gè) Group 里面的 Shape 可以一起應(yīng)用旋轉(zhuǎn)、縮放等變換。

Shape 則是指具體的繪制節(jié)點(diǎn),比如 Rect、Circle、Text 等等。

2.2 包圍盒

既然有了虛擬節(jié)點(diǎn),那知道每個(gè)虛擬節(jié)點(diǎn)的位置和大小也比較重要,它會(huì)涉及到判斷兩個(gè)圖形是否相交、事件等等。

有時(shí)候元素的形狀不是很規(guī)則,如果直接對(duì)不規(guī)則元素進(jìn)行碰撞檢測(cè)會(huì)比較麻煩,所以就有了一個(gè)近似的算法,就是在物體外側(cè)加上包圍盒,如圖:

目前主流的包圍盒有 AABB 和 OBB 兩種。

AABB 包圍盒:

實(shí)現(xiàn)方式簡單,直接用最大最小的橫縱坐標(biāo)來生成包圍盒,但不會(huì)跟著元素旋轉(zhuǎn),因此空白區(qū)域比較多,也不夠準(zhǔn)確。

也是目前 Konva 和 AntV 使用的方式。(適合表格業(yè)務(wù))

OBB 包圍盒:

實(shí)現(xiàn)方式相對(duì)復(fù)雜,通過構(gòu)建協(xié)方差矩陣來計(jì)算出新的坐標(biāo)軸方向,將其頂點(diǎn)投射到坐標(biāo)軸上面來得到新的包圍盒。

所以 OBB 包圍盒更加準(zhǔn)確一些,也是 cocos2d 使用的方式。

碰撞檢測(cè):

兩個(gè)包圍盒在所有軸(與邊平行)上的投影都發(fā)生重疊,則判定為碰撞;否則,沒有發(fā)生碰撞。

2.3 排版系統(tǒng)

繪制 Canvas 的時(shí)候一般是通過相對(duì)坐標(biāo)來確定當(dāng)前要繪制的位置,所以都是通過各種計(jì)算來拿到 x、y。

即使是 Konva 也是依賴于 x、y 來做相對(duì)定位。

因此,在 AntV 和 SpriteJS 這類 Canvas 渲染引擎里面,都內(nèi)置支持了盒模型的語法糖,底層會(huì)將盒模型屬性進(jìn)行一次計(jì)算轉(zhuǎn)換成 x、y。

以 AntV 為例子,排版能力是基于 Facebook 開源的 Yoga 排版引擎(React Native)來實(shí)現(xiàn)的,支持一套非常完整的盒模型和 Flex 布局語法。

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 里面自己實(shí)現(xiàn)了一套類似 Yoga 的排版引擎,叫做 Titank。

在飛書文檔多維表格里面,排版語法更加接近于 Flutter,實(shí)現(xiàn)了 Padding、Column、Row、Margin、Expanded、Flex、GridView 等 Widget。

下面的示例是 Flutter 的:

Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 對(duì)齊方式
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));

實(shí)現(xiàn)了盒模型和 Flex 布局,可以讓 Canvas 的排版能力更上一層樓。

不僅可以減少代碼中的大量計(jì)算,也可以讓大家從 DOM 開發(fā)無縫銜接進(jìn)來,值得我們參考。

canvas-flexbox - CodeSandbox

3. 事件

Canvas 本身是一塊畫布,所以里面的內(nèi)容都是畫出來的,在 DOM 樹里面也只是一個(gè) Canvas 的節(jié)點(diǎn),所以如何才能知道當(dāng)前點(diǎn)擊的是哪個(gè)圖形呢?

由于 Canvas 渲染引擎都會(huì)封裝虛擬節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)都有自己的包圍盒,所以為實(shí)現(xiàn) Canvas 的事件系統(tǒng)提供了可能性。

主流的 Canvas 渲染引擎都是針對(duì) Canvas 節(jié)點(diǎn)或者上層節(jié)點(diǎn)進(jìn)行事件委托,監(jiān)聽用戶相關(guān)的事件(mouseDown、click、touch等等)之后,匹配到當(dāng)前觸發(fā)的元素,將事件分發(fā)出去,并且擁有一套向上冒泡的機(jī)制。

目前主流的兩種事件實(shí)現(xiàn)方式分別是取色值法和幾何法。

3.1 取色值法

取色值法是 Konva 采用的實(shí)現(xiàn)方式,它的實(shí)現(xiàn)方式非常簡單,匹配精確度很高,適合不規(guī)則圖形的匹配。

取色值法的原理如下:

  1. 在主 Canvas 繪制一個(gè)圖形的時(shí)候,會(huì)為這個(gè)圖形生成一個(gè)隨機(jī)的 colorKey(十六進(jìn)制的顏色),同時(shí)建立類似于 Map 的映射。
getRandomColor() {
var randColor = ((Math.random() * 0xffffff) << 0).toString(16);
while (randColor.length < 6) {
randColor = ZERO + randColor;
}
return HASH + randColor;
},

  1. 繪制的同時(shí)會(huì)在內(nèi)存里的 hitCanvas 同樣位置繪制一個(gè)一模一樣的圖形,填充色是剛才的 colorKey。
  2. 當(dāng)用戶鼠標(biāo)點(diǎn)擊 Canvas 畫布的時(shí)候,可以拿到鼠標(biāo)觸發(fā)的 x、y,將其傳給內(nèi)存里面的 Canvas。
  3. 內(nèi)存里面的 Canvas 通過 getImageData 來獲取到當(dāng)前的顏色,進(jìn)而通過 colorKey 來匹配到對(duì)應(yīng)的圖形。

從上述原理可以看出來,Konva 對(duì)于不規(guī)則圖形的匹配依然很精確,但缺點(diǎn)也很明顯,每次都需要繪制兩份,導(dǎo)致繪制性能變差。

同時(shí),getImageData 耗時(shí)比較高,在頻繁觸發(fā)的場(chǎng)景(onWheel)會(huì)導(dǎo)致幀率下降嚴(yán)重。

3.2 幾何法

幾何法有很多種實(shí)現(xiàn)方式,這里主要講解引射線法,因?yàn)樾枰M(jìn)行一系列幾何計(jì)算,所以這里我稱之為幾何法。

幾何法是 AntV 和飛書文檔采用的實(shí)現(xiàn)方式,實(shí)現(xiàn)方式相對(duì)復(fù)雜一些,針對(duì)不規(guī)則圖形的匹配效率偏低。

幾何法的實(shí)現(xiàn)原理如下:

  1. 基于當(dāng)前虛擬節(jié)點(diǎn)的包圍盒來構(gòu)建一棵 R Tree
  2. 當(dāng)用戶觸發(fā)事件的時(shí)候,利用 R Tree 來進(jìn)行空間索引查找,依據(jù) z-index 找到最頂層的一個(gè)圖形。
  3. 從目標(biāo)點(diǎn)出發(fā)向一側(cè)發(fā)出一條射線,看這條射線和多邊形所有邊的交點(diǎn)數(shù)目。
  4. 如果有奇數(shù)個(gè)交點(diǎn),則說明在內(nèi)部,如果有偶數(shù)個(gè)交點(diǎn),則說明在外部。

為什么奇數(shù)是在內(nèi)部,偶數(shù)是在外部呢?我們假設(shè)射線與這個(gè)圖形的交點(diǎn),進(jìn)入圖形叫做穿入,離開圖形叫做穿出。

在圖形內(nèi)部發(fā)出的射線,一定會(huì)有穿出但沒有穿入的情況。但在外部發(fā)出的射線,穿入和穿出是相對(duì)的。

但是射線剛好穿過頂點(diǎn)的情況比較特殊,因此需要單獨(dú)進(jìn)行判斷。

幾何法的優(yōu)勢(shì)在于不需要在內(nèi)存里面進(jìn)行重復(fù)繪制,但依賴于復(fù)雜的幾何計(jì)算,因此不適合有大量不規(guī)則圖形的情況。

在 AntV 里面支持對(duì)不規(guī)則圖形的匹配,但飛書文檔由于是表格業(yè)務(wù),所以可以將所有圖形都當(dāng)做矩形來處理,反而更簡單一些。

4. 性能

由于 Canvas 渲染引擎都會(huì)進(jìn)行大量的封裝,所以開發(fā)者想針對(duì)底層做性能優(yōu)化是非常難的,需要渲染引擎自身去支持一些優(yōu)化。

4.1 異步批量渲染

在飛書文檔 Bitable 和 Konva 里面都支持異步渲染,將大量繪制進(jìn)行批量處理。

const rect = new Rect({ /... });
// 多次修改屬性,可能會(huì)觸發(fā)多次渲染
rect.x(100);
rect.fill('red');
rect.y(100);

由于每次修改圖形的屬性或者添加、銷毀子節(jié)點(diǎn)都會(huì)觸發(fā)渲染,為了避免同時(shí)修改多個(gè)屬性時(shí)導(dǎo)致的重復(fù)渲染,因此約定每次在下一幀進(jìn)行批量繪制。

batchDraw() {
if (!this._waitingForDraw) {
this._waitingForDraw = true;
Util.requestAnimFrame(() => {
this.draw();
this._waitingForDraw = false;
});
}
return this;
}

這種渲染方式類似于 React 的 setState,避免短時(shí)間內(nèi)多次 setState 導(dǎo)致多次 render。

4.2 離屏渲染

離屏渲染我們應(yīng)該都比較熟悉了,就是兩個(gè) Canvas 來回用 drawImage 繪制可復(fù)用部分,從而減少繪制的耗時(shí)。

這里主要講解 Konva 和飛書 Bitable 里面的離屏渲染。

在 Konva 中的離屏渲染主要是針對(duì) Group 級(jí)別來做的,通過調(diào)用 cache 方法就能實(shí)現(xiàn)離屏渲染。

基于 Group 來做離屏渲染的原理是:

  1. 調(diào)用 cache 方法,創(chuàng)建一個(gè)離屏 Canvas 節(jié)點(diǎn)。
  2. 遍歷 Group 子節(jié)點(diǎn)進(jìn)行繪制,同時(shí)將其繪制到離屏 Canvas 上面。
  3. 下次 batchDraw 的時(shí)候判斷是否有緩存,如果有,那么直接走 drawImage 的形式。

這種離屏渲染的調(diào)用方式比較簡單,Group 的粒度可以由開發(fā)者自己決定,但也有一定的問題。

  1. 比較難應(yīng)用于表格這種形式的業(yè)務(wù)
  2. Konva 沒有臟檢測(cè)能力,即使 Group 里面的 Shape 屬性改變了,依然不會(huì)更新離屏 Canvas。
  3. 由于使用色值法來匹配圖形,導(dǎo)致開啟了離屏渲染,實(shí)際上至少要繪制四份(主canvas、事件 hitCanvas、離屏 cacheCanvas、離屏事件 cacheHitCanvas)。

為什么需要繪制四份呢?因?yàn)殡x屏渲染是 drawImage 的形式,這樣就不會(huì)有 colorKey 和 Shape 對(duì)應(yīng)的情況了,所以離屏 Canvas 也要有一個(gè)自己的 hitCanvas 來做 getImageData,也就是 cacheHitCanvas。

另一種場(chǎng)景的離屏渲染就是飛書 Bitable 里面的實(shí)現(xiàn)。

飛書在底層之上封裝了虛擬列表的 Widget,也就是基于業(yè)務(wù)定制的 Widget,這也是一種有趣的思路。

  1. 創(chuàng)建一個(gè)虛擬列表的 Widget 類,將列表數(shù)據(jù)傳入
  2. 實(shí)現(xiàn)列表每一項(xiàng)的繪制方法,將列表繪制出來
  3. 滾動(dòng)的時(shí)候虛擬列表內(nèi)部進(jìn)行節(jié)點(diǎn)的回收創(chuàng)建,但不會(huì)進(jìn)行異步批量渲染,針對(duì)可復(fù)用的部分進(jìn)行離屏渲染
  4. 更新階段,通過 key 對(duì)比來決定是回收、創(chuàng)建還是復(fù)用。

在多維表格看板視圖里面,每個(gè)分組都是一個(gè)虛擬列表,多個(gè)分組(虛擬列表)又組合成一個(gè)大的虛擬列表。

多選單元格編輯器也可以基于虛擬列表實(shí)現(xiàn)。

虛擬列表 Widget 類適合多維表格這種業(yè)務(wù),多個(gè)視圖都需要有自己的滾動(dòng)容器,不同視圖都需要處理節(jié)點(diǎn)的回收、復(fù)用、新建,通過公用 Widget 可以一步到位去支持,也方便在內(nèi)部去做更多性能優(yōu)化。

4.3 臟區(qū)渲染

對(duì)于 Konva 來說,每次重新渲染都是對(duì)整個(gè) Canvas 做 clearRect 清除,然后重新繪制,性能相對(duì)比較差。

更好的做法是檢測(cè)到當(dāng)前的改動(dòng)影響到的范圍,計(jì)算出重繪范圍后,只清除重繪區(qū)的內(nèi)容重新進(jìn)行繪制。

在 Canvas 中可以通過 rect 和 clip 限制繪制區(qū)域,從而做到只對(duì)部分區(qū)域重繪。

以前 ECharts 底層的 ZRender 為例來講解:

  1. 根據(jù)圖形前后變化,來計(jì)算出重繪區(qū)域,比如上圖的區(qū)域,在飛書文檔中會(huì)將整個(gè)移動(dòng)的路徑當(dāng)做重繪區(qū)域。
  2. 如果有多個(gè)重繪區(qū)域,那么優(yōu)先嘗試將相交(包圍盒)的重繪區(qū)進(jìn)行合并,并且優(yōu)先合并相交面積最大的重繪區(qū)。
  3. 如果合并完成后,當(dāng)前剩余的重繪區(qū)數(shù)量大于5,則進(jìn)一步進(jìn)行合并,直到數(shù)量只剩5。
  4. 依次遍歷這些重繪區(qū)域,先清除掉原有的內(nèi)容,再進(jìn)行繪制。

飛書文檔多維表格沒有做 Canvas 渲染分層,但對(duì)各種交互響應(yīng)速度非???,也是得益于底層渲染引擎對(duì)臟矩形渲染的支持,它的性能也是所有同類產(chǎn)品里面最好的。

除了上述的這些,還有在文檔這邊使用的一些優(yōu)化手段,比如合并相同屬性的圖形繪制(線、矩形、文本等)、Canvas 分層等等,這些就不多做闡述了。

5. 跨平臺(tái)

很多 Canvas 渲染引擎并不滿足于只做 Canvas,一般還會(huì)支持一些其他的渲染模式,比如 SVG 渲染、WebGL 渲染、WebGPU 渲染等等。

在 AntV 里面通過引入對(duì)應(yīng)的 package 來實(shí)現(xiàn)加載渲染器的,在 ZRender 中則是通過 register 來注冊(cè)不同的渲染器。

AntV 中使用 CanvasKit 渲染:

import { Renderer as CanvaskitRenderer } from '@antv/g-canvaskit';
const canvaskitRenderer = new CanvaskitRenderer();

關(guān)于跨平臺(tái)的架構(gòu)這里不做講解,主要是抹平不同平臺(tái)的差異,這里主要講解一下針對(duì)于服務(wù)端渲染的不同處理。

主流的服務(wù)端渲染方式有兩種,一種是用 node-canvas 來輸出一張圖片,在 echarts 等庫中都有使用,缺陷在于文本排版不夠準(zhǔn)確,對(duì)于自適應(yīng)瀏覽器窗口的情況無法處理。因此它不適用于文檔直出的場(chǎng)景。

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('')
})

另一種就是通過 SVG 來模擬 Canvas 的效果,輸出 SVG DOM 字符串。但它的實(shí)現(xiàn)會(huì)比較麻煩,也無法 100% 還原 Canvas 的效果。

但很多 Canvas 渲染引擎本身也支持 SVG 渲染,即使不支持,也可以通過 canvas2svg 這個(gè)庫來進(jìn)行轉(zhuǎn)換。

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();

對(duì)于更加通用的場(chǎng)景來說,在瀏覽器端使用 Canvas 渲染,服務(wù)端使用 SVG 渲染是更合理的形式。

在新版 ECharts 里面,針對(duì) SVG 服務(wù)端渲染的能力,還支持了 Virtual DOM 來代替 JSDOM,最后轉(zhuǎn)換成 DOM 字符串。

在飛書文檔中使用了一種完全獨(dú)立于 node-canvas 和 SVG 的解決方式,非常值得我們借鑒。

由于飛書多維表格底層統(tǒng)一了渲染引擎,所有繪制元素都是 Widget(對(duì)齊 Flutter),可以脫水轉(zhuǎn)換成下面 FVG 格式。

一般來說,文檔業(yè)務(wù)首屏加載是下面這么幾步:

獲取首屏數(shù)據(jù) -> 資源加載 -> 首屏數(shù)據(jù)反序列化 -> 初始化 Model 層 -> 計(jì)算排版數(shù)據(jù) -> Canvas 渲染

在飛書文檔里面直出渲染層 Widget 的數(shù)據(jù)結(jié)構(gòu),這個(gè)數(shù)據(jù)結(jié)構(gòu)是最后提供給 Canvas 渲染的數(shù)據(jù),也就是已經(jīng)經(jīng)過了計(jì)算排版數(shù)據(jù)階段。

當(dāng)渲染層 JS 資源加載完成后,直接省略反序列化、初始化 Model、計(jì)算排版數(shù)據(jù)等階段,將 FVG 轉(zhuǎn)換成 Widget 進(jìn)行 Canvas 渲染,這一步非常接近于 React 的 hydrate,很巧妙。


標(biāo)題名稱:淺談 Canvas 渲染引擎設(shè)計(jì)
文章鏈接:http://www.dlmjj.cn/article/dhodgci.html