OffscreenCanvas-離屏canvas使用說明

OffscreenCanvas 是一個實驗中的新特性,主要用於提高 Canvas 2D/3D 繪圖的渲染性能和使用體驗。OffscreenCanvas 的 API 很簡單,可是要真正掌握好如何使用。前端

OffscreenCanvas和canvas都是渲染圖形的對象。 不一樣的是canvas只能在window環境下使用,而OffscreenCanvas便可以在window環境下使用,也能夠在web worker中使用,這讓不影響瀏覽器主線程的離屏渲染成爲可能。程序員

與之關聯的還有ImageBitmap對象和ImageBitmapRenderingContext。web

ImageBitmap

ImageBitmap對象表示可以被繪製到 canvas上的位圖圖像,具備低延遲的特性。 ImageBitmap提供了一種異步且高資源利用率的方式來爲WebGL的渲染準備基礎結構。ImageBitmap能夠經過createImageBitmap函數來建立,它能夠從多種圖像源生成。 還能夠經過OffscreenCanvas.transferToImageBitmap函數生成。canvas

屬性

ImageBitmap.height 只讀無符號長整型數值,表示ImageData的CSS像素單位的高度。ImageBitmap.width 只讀無符號長整型數值, 表示ImageData的CSS像素單位的寬度。windows

函數

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) { ... });複製代碼

更多相關的內容,能夠參考:developer.mozilla.org/zh-CN/docs/…異步

建立OffscreenCanvas

有兩種方式能夠建立OffscreenCanvas,一種是經過OffscreenCanvas的構造函數直接建立。好比下面的示例代碼:函數

var offscreen = new OffscreenCanvas(width, height); // width 、height表示寬高。複製代碼

另一種方式,是使用canvas的transferControlToOffscreen函數獲取一個OffscreenCanvas對象,繪製該OffscreenCanvas對象,同時會繪製canvas對象。好比以下代碼:post

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高級進階 xiaozhuanlan.com/canvas,相關知識會在專欄中介紹。

歡迎關注公衆號「ITman彪叔」。彪叔,擁有10多年開發經驗,現任公司系統架構師、技術總監、技術培訓師、職業規劃師。在計算機圖形學、WebGL、前端可視化方面有深刻研究。對程序員思惟能力訓練和培訓、程序員職業規劃有濃厚興趣。ITman彪叔公衆號

相關文章
相關標籤/搜索