OffscreenCanvas 是一個實驗中的新特性,主要用於提升 Canvas 2D/3D 繪圖的渲染性能和使用體驗。OffscreenCanvas 的 API 很簡單,但是要真正掌握好如何使用。
OffscreenCanvas和canvas都是渲染圖形的對象。 不同的是canvas只能在window環境下使用,而OffscreenCanvas即可以在window環境下使用,也可以在web worker中使用,這讓不影響瀏覽器主線程的離屏渲染成為可能。
與之關聯的還有ImageBitmap對象和ImageBitmapRenderingContext。
ImageBitmap
ImageBitmap對象表示能夠被繪制到 canvas上的位圖圖像,具有低延遲的特性。 ImageBitmap提供了一種異步且高資源利用率的方式來為WebGL的渲染准備基礎結構。
ImageBitmap可以通過createImageBitmap函數來創建,它可以從多種圖像源生成。 還可以通過OffscreenCanvas.transferToImageBitmap函數生成。
屬性
ImageBitmap.height 只讀
無符號長整型數值,表示ImageData的CSS像素單位的高度。
ImageBitmap.width 只讀
無符號長整型數值, 表示ImageData的CSS像素單位的寬度。
函數
ImageBitmap.close()
釋放ImageBitmap所相關聯的所有圖形資源。
createImageBitmap
createImageBitmap 用於創建ImageBitmap對象。該函數存在 windows 和 workers 中。
它接受各種不同的圖像來源, 並返回一個Promise, resolve為ImageBitmap。
createImageBitmap(image[, options]).then(function(response) { ... });
createImageBitmap(image, sx, sy, sw, sh[, options]).then(function(response) { ... });
更多相關的內容,可以參考:
https://developer.mozilla.org/zh-CN/docs/Web/API/WindowOrWorkerGlobalScope/createImageBitmap
創建OffscreenCanvas
有兩種方式可以創建OffscreenCanvas,一種是通過OffscreenCanvas的構造函數直接創建。比如下面的示例代碼:
var offscreen = new OffscreenCanvas(width, height); // width 、height表示寬高。
另外一種方式,是使用canvas的transferControlToOffscreen函數獲取一個OffscreenCanvas對象,繪制該OffscreenCanvas對象,同時會繪制canvas對象。比如如下代碼:
var canvas = document.getElementById('canvas');
//var ctx = canvas.getContext('2d');
var offscreen = canvas.transferControlToOffscreen();
// canvas.getContext('2d'); // 會報錯
上面的代碼代碼首先獲取網頁元素canvas對象,然后調用canvas對象的transferControlToOffscreen函數創建一個OffscreenCanvas對象offscreen,並把控制權交給offscreen。
需要注意的是,canvas對象調用了函數transferControlToOffscreen移交控制權之后,不能再獲取繪制上下文,調用canvas.getContext('2d')會報錯; 同樣的原理,如果canvas已經獲取的繪制上下文,調用transferControlToOffscreen會報錯。
OffscreenCanvas.transferToImageBitmap函數
通過transferToImageBitmap函數可以從OffscreenCanvas對象的繪制內容創建一個ImageBitmap對象。該對象可以用於到其他canvas的繪制。
比如一個常見的使用是,把一個比較耗費時間的繪制放到web worker下的OffscreenCanvas對象上進行,繪制完成后,創建一個ImageBitmap對象,並把該對象傳遞給頁面端,在頁面端繪制ImageBitmap對象。
下面是示例代碼,主線程中:
var worker2 = null,canvasBitmap, ctxBitmap;
function init() {
canvasBitmap = document.getElementById('canvas-bitmap');
ctxBitmap = canvasBitmap.getContext('2d');
worker2 = new Worker('./bitmap_worker.js');
worker2.postMessage({msg:'init'});
worker2.onmessage = function (e) {
ctxBitmap.drawImage(e.data.imageBitmap,0,0);
}
}
function redraw() {
ctxBitmap.clearRect(0, 0, canvasBitmap.width, canvasBitmap.height)
worker2.postMessage({msg:'draw'});
}
worker線程中:
var offscreen,ctx;
onmessage = function (e) {
if(e.data.msg == 'init'){
init();
draw();
}else if(e.data.msg == 'draw'){
draw();
}
}
function init() {
offscreen = new OffscreenCanvas(512, 512);
ctx = offscreen.getContext("2d");
}
function draw() {
ctx.clearRect(0,0,offscreen.width,offscreen.height);
for(var i = 0;i < 10000;i ++){
for(var j = 0;j < 1000;j ++){
ctx.fillRect(i*3,j*3,2,2);
}
}
var imageBitmap = offscreen.transferToImageBitmap();
postMessage({imageBitmap:imageBitmap},[imageBitmap]);
}
- 在主線程中,獲取canvas對象,然后生成worker對象,並把繪制命令傳遞給worker。
- 在worker線程中,創建一個OffscreenCanvas,然后執行繪制命令,繪制完成后,通過transferToImageBitmap函數創建imageBitmap對象,並通過postMessage把imageBitmap對象傳遞給主線中。
- 主線程接收到imageBitmap對象之后,把imageBitmap繪制到canvas對象上。
最終的繪制效果如下:
把繪制放到web worker中的好處是,繪制的過程不阻塞主線程的運行。 讀者可以自行運行代碼查看,在繪制過程過程中,界面可以交互, 比如可以選擇下拉框。
ImageBitmapRenderingContext
ImageBitmapRenderingContext接口是 canvas 的渲染上下文,它只提供使用給定 ImageBitmap 替換 canvas 的功能。它的上下文 ID (HTMLCanvasElement.getContext() 或 OffscreenCanvas.getContext() 的第一個參數) 是 "bitmaprenderer"。
這個接口可用於 window context 和 worker context.
方法
ImageBitmapRenderingContext.transferFromImageBitmap函數用於
在與此“渲染上下文”對應的 canvas 中顯示給定的 ImageBitmap對象。 ImageBitmap 的所有權被轉移到畫布上。
在前面的例子中,可以做如下修改:
function init() {
...
ctxBitmap = canvasBitmap.getContext('bitmaprenderer');
...
worker2.onmessage = function (e) {
ctxBitmap.transferFromImageBitmap(e.data.imageBitmap);
}
}
首先,把獲取渲染上下文的id改成“bitmaprenderer”,返回額ctxBitmap是一個ImageBitmapRenderingContext對象。
然后,在渲染ImageBitmap對象的時候,把drawImage函數改為transferFromImageBitmap函數。
最終渲染效果和上圖顯示一樣。
transferControlToOffscreen函數
transferControlToOffscreen函數可以通過頁面的canvas對象來創建一個OffscreenCanvas。 既然可以通過構造函數創建OffscreenCanvas對象,為啥還需要這樣操作。 原因是這樣的:
我們看前面一個示例,我們在worker線程中創建OffscreenCanvas對象並繪制然后獲取ImageBitmap對象,通過web worker通信把ImageBitmap傳遞給頁面。
而如果通過canvas.transferControlToOffscreen生成的OffscreenCanvas對象,不需要再通過web worker通信來傳遞繪制的效果,生成了OffscreenCanvas對象之后,OffscreenCanvas對象的繪制會自動在canvas元素上面顯示出來。這相對於web worker通信有着不言而喻的優勢。
通過transferControlToOffscreen函數創建的OffscreenCanvas對象有兩大功能:
- 避免繪制中大量的計算阻塞主線程
- 避免主線程的重任務阻塞繪制
下面我們將會通過示例來說明以上結論。
首先,我們寫一個Circle類,這個類的作用主要是用於繪制一個圓,並且可以啟動動畫,不斷的改變圓的半徑大小:
class Circle {
constructor(ctx){
this.ctx = ctx;
this.r = 0;
this.rMax = 50;
this.color = 'black';
this.bindAnimate = this.animate.bind(this);
}
draw(){
this.ctx.fillStyle = this.color;
this.ctx.beginPath();
this.ctx.arc(this.ctx.canvas.width/2,this.ctx.canvas.height/2,this.r,0,Math.PI*2);
this.ctx.fill();
}
animate(){
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
this.r = this.r + 1;
if(this.r > this.rMax){
this.r = 0;
}
this.draw();
requestAnimationFrame(this.bindAnimate);
}
changeColor(){
fibonacci(41);
if(this.color == 'black'){
this.color = 'blue';
}else{
this.color = 'black';
}
this.r = 0;
}
}
- draw 函數用於繪制一個填充的圓形
- animate 用於動畫,其不斷改變圓形的半徑
另外還有一個函數changeColor,表示改變繪制的顏色,其會在黑色和藍色之間不斷變化,本示例中,為了模擬比較耗時的操作,在changeColor函數中,調用了下fibonacci函數,fibonacci函數用於計算斐波那契數列,當傳入值是41的時候,計算量較大,主線程會把阻塞一段時間。下面是fibonacci的定義:
function fibonacci(num) {
return (num <= 1) ? 1 : fibonacci(num - 1) + fibonacci(num - 2);
}
然后,我們定義兩個canvas,一個用於普通的canvas應用,一個用於呈現離屏繪制的內容:
<canvas id="canvas-window" width="300" height="400" style="background: white;left: 10px;top: 20px;position: relative;"></canvas>
<canvas id="canvas-worker" width="300" height="400" style="background: white;left: 10px;top: 20px;position: relative;"></canvas>
對於第一個canvas,我們直接在其上不斷繪制半徑變化的圓形:
var canvasInWindow = document.getElementById('canvas-window');
var ctx = canvasInWindow.getContext('2d');
var circle = new Circle(ctx);
circle.animate();
canvasInWindow.addEventListener('click', function () {
circle.changeColor();
});
並在該canvas上添加‘click’事件,當點擊時,調用Circle類的changeColor函數。
對於第二個canvas,我們使用webworker,首先使用transferControlToOffscreen函數創建OffscreenCanvas對象offscreen,然后創建worker對象,並把offscreen發送給worker線程:
var canvasInWorker = document.getElementById('canvas-worker');
// var ctxInWorkder = canvasInWorker.getContext('2d');
var offscreen = canvasInWorker.transferControlToOffscreen();
var worker = new Worker('./worker.js');
worker.postMessage({ msg: 'start', canvas: offscreen }, [offscreen]);
canvasInWorker.addEventListener('click', function () {
worker.postMessage({msg:'changeColor'});
});
// canvasInWorker.getContext('2d'); // 會報錯
該canvas上同樣添加‘click’事件,當點擊時,發送changeColor的命令給worker線程。
然后,我們看下worker.js線程的內容:
var offscreen = null,ctx,circle;
onmessage = function (e) {
var data = e.data;
if(data.msg == 'start'){
offscreen = data.canvas;
ctx = offscreen.getContext('2d');
circle = new Circle(ctx);
circle.animate();
} else if (data.msg == 'changeColor' && circle) {
circle.changeColor();
}
}
function fibonacci(num) {
return (num <= 1) ? 1 : fibonacci(num - 1) + fibonacci(num - 2);
}
class Circle {
constructor(ctx) {
this.ctx = ctx;
this.r = 0;
this.rMax = 50;
this.color = 'black';
this.bindAnimate = this.animate.bind(this);
}
draw() {
this.ctx.fillStyle = this.color;
this.ctx.beginPath();
this.ctx.arc(this.ctx.canvas.width / 2, this.ctx.canvas.height / 2, this.r, 0, Math.PI * 2);
this.ctx.fill();
}
animate() {
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
this.r = this.r + 1;
if (this.r > this.rMax) {
this.r = 0;
}
this.draw();
requestAnimationFrame(this.bindAnimate);
}
changeColor() {
fibonacci(41);
if (this.color == 'black') {
this.color = 'blue';
} else {
this.color = 'black';
}
this.r = 0;
}
}
在worker.js中,定義了一個同樣的Circle類和fibonacci函數。 在onmessage函數中,接受頁面端傳遞來的信息,當接受到start命令時,在接收到的OffscreenCanvas對象offscreen上繪制圓形的動畫。當接受到changeColor命令時,調用Circle類的changeColor函數。
讀者可以看出,在worker線程中繪制了圖形之后,並沒有傳遞給頁面端,其內容會自動顯示給頁面的斷的canvas。 最終顯示的效果如下圖:
可以看到兩個canvas都在繪制動畫。區別在於,單擊的時候,都會調用比較重的changeColor函數,頁面端的canvas會阻塞主線程,而離屏的canvas不會阻塞主線程,演示如下:
除了不阻塞主線程之外,離屏的OffscreenCanvas對象也不會被主線程的重任務阻塞,比如我們在頁面添加一個button,調用一個耗時的任務:
<button id='heavyTask' style="position: absolute;display:inline;left: 100px;" onclick="heavyTask()">heavyTask</button>
其實耗時的任務還是用了fibonacci函數來模擬:
function heavyTask() {
fibonacci(41);
}
當點擊按鈕的時候,頁面的canvas會停止動畫,而離屏的canvas不會停止動畫:
如果讀者不清楚canvas相關知識點,建議學習相關知識,也推薦有興趣讀者,訂閱專欄(本文內容就摘取自專欄):
Canvas高級進階 https://xiaozhuanlan.com/canvas,相關知識會在專欄中介紹。
歡迎關注公眾號“ITman彪叔”。彪叔,擁有10多年開發經驗,現任公司系統架構師、技術總監、技術培訓師、職業規划師。熟悉Java、JavaScript、Python語言,熟悉數據庫。熟悉java、nodejs應用系統架構,大數據高並發、高可用、分布式架構。在計算機圖形學、WebGL、前端可視化方面有深入研究。對程序員思維能力訓練和培訓、程序員職業規划有濃厚興趣。