OffscreenCanvas-離屏canvas使用說明


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對象上。

最終的繪制效果如下:
image.png

把繪制放到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。 最終顯示的效果如下圖:
perf.gif

可以看到兩個canvas都在繪制動畫。區別在於,單擊的時候,都會調用比較重的changeColor函數,頁面端的canvas會阻塞主線程,而離屏的canvas不會阻塞主線程,演示如下:
perf2.gif

除了不阻塞主線程之外,離屏的OffscreenCanvas對象也不會被主線程的重任務阻塞,比如我們在頁面添加一個button,調用一個耗時的任務:

<button id='heavyTask' style="position: absolute;display:inline;left: 100px;"  onclick="heavyTask()">heavyTask</button>

其實耗時的任務還是用了fibonacci函數來模擬:

function heavyTask() {
   fibonacci(41);
}

當點擊按鈕的時候,頁面的canvas會停止動畫,而離屏的canvas不會停止動畫:
perf3.gif

如果讀者不清楚canvas相關知識點,建議學習相關知識,也推薦有興趣讀者,訂閱專欄(本文內容就摘取自專欄):
Canvas高級進階 https://xiaozhuanlan.com/canvas,相關知識會在專欄中介紹。

歡迎關注公眾號“ITman彪叔”。彪叔,擁有10多年開發經驗,現任公司系統架構師、技術總監、技術培訓師、職業規划師。熟悉Java、JavaScript、Python語言,熟悉數據庫。熟悉java、nodejs應用系統架構,大數據高並發、高可用、分布式架構。在計算機圖形學、WebGL、前端可視化方面有深入研究。對程序員思維能力訓練和培訓、程序員職業規划有濃厚興趣。
ITman彪叔公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM