canvas離屏渲染優化

最近在作canvas粒子動畫效果的研究,發現當粒子數量達到必定等級的時候,動畫效果會變慢變卡。搜索了一下解決辦法,發現離屏渲染是推薦最多的解決辦法,那本文就利用離屏渲染實現一個動畫效果來對比性能的提高。javascript

概念

查閱了一下資料,概述一下離屏渲染的概念,至關於在屏幕渲染的時候開闢一個緩衝區,將當前須要加載的動畫事先在緩衝區渲染完成以後,再顯示到屏幕上。html

非離屏渲染

非離屏渲染就是不創建緩衝區,直接在屏幕上逐個進行繪製,須要重複利用canvas的api。當粒子數量到達必定等級時,性能上會受到較大影響。前端

實現

先建立一個雪花粒子的類,構造相關的屬性,定義一個名爲snowArray的數組,將每一個粒子都存入該數組中。count爲雪花的數量。java

class DrawSnow {
  constructor(count) {
    this.canvas = document.getElementById('canvas');
    this.content = this.canvas.getContext('2d')
    this.width = this.canvas.width = 1200;
    this.height = this.canvas.height = 1000;
    this.r = 2.5;

    this.timer = null;
    this.snowArray= [];
    this.count = count;

    this.useOffCanvas = false; // 是否使用離屏渲染
    this.init()
  }
}
複製代碼

init()函數初始化雪花粒子,根據粒子的數量,重複渲染生成隨機的位置,並存入數組中。初始化完成以後,開始繪製粒子。並執行動畫函數animate()chrome

init() {
    let OffScreen = '';

    if (this.useOffCanvas) {
      OffScreen = new OffScreen();
    }

    for (let i = 0; i < this.count; i++) {
      let x = this.width * Math.random();
      let y = this.height * Math.random();

      this.snowArray.push({
        x: x,
        y: y
      });
    this.draw(x, y);
    }
    this.animate();
  }
複製代碼

animate()函數實現了動畫的循環,在一次動畫執行完成以後,經過window.requestAnimationFrame來實現重複效果。根據存儲在snowArray[]中的粒子信息,反覆進行繪製。canvas

animate() {
    this.content.clearRect(0, 0, this.width, this.height);

    for (let i in this.snowArray) {
      let snow = this.snowArray[i];

      snow.y += 2;
      if (snow.y >= this.height + 10) {
        snow.y = Math.random() * 50;
      }

      this.draw(snow.x, snow.y);
    }
    this.timer = requestAnimationFrame(() => {
      this.animate();
    });
  }
複製代碼

效果

完成以上的步驟以後,來看一下在瀏覽器中的效果api

小雪數組

非離屏小雪

中雪瀏覽器

非離屏中雪

大雪dom

非離屏大雪

性能分析

上述動圖中,右上角爲chrome自帶的性能分析工具,點擊開發者工具performance面板,按快捷鍵cmd + shift + p 而後輸入show rendering (打開實時查看幀率的面板),能夠看到實時的幀率變化。

performance面板的使用在以前有介紹,指路:十分鐘上手chrome性能分析面板

小雪、中雪、大雪須要繪製的粒子分別是80、200、7000個粒子,當粒子數量較少時,動畫效果比較順暢,維持在60FPS左右,當數量增長到7000個時,動畫開始卡頓,幀數快速降低。由於錄屏工具對實際幀數會產生影響,上述動圖可做爲參考,實際幀數參考下圖:

非離屏大雪截圖

離屏渲染

實現

原理

建立緩衝區,須要額外建立一個canvas畫布,將緩衝的畫面如今該canvas上繪製好,在經過drawImage()的方式將該畫布渲染到屏幕顯示的畫布上。

代碼

首先實現離屏渲染的粒子構造方法,構造完成以後進行繪製,move()將繪製好的畫布經過drawImage方法在屏幕上展現。

// 粒子類
class OffScreen {
  constructor() {
    this.canvas = document.createElement('canvas');
    this.r = 2.5;
    this.width = this.canvas.width = 5;
    this.height = this.canvas.height = 5;
    this.ctx = this.canvas.getContext('2d');
    this.x = this.width * Math.random();
    this.y = this.height * Math.random();

    this.create();
  }
  
  // 建立粒子
  create() {
    this.ctx.save();
    this.ctx.fillStyle = 'rgba(255,255,255)';
    this.ctx.beginPath();
    this.ctx.arc(this.x, this.y, 2.5, 0, 2 * Math.PI, false);
    this.ctx.closePath();
    this.ctx.fill();
    this.ctx.restore();
  }
  
  // 繪製粒子
  move(ctx, x, y) {
    ctx.drawImage(this.canvas, x, y);
  }
}
複製代碼

初始化粒子時,判斷是不是離屏渲染模式,離屏模式下構造一個離屏粒子,先在畫布中畫出,當遍歷粒子數組時,經過animate()中執行OffScreen類中的move()方法,將粒子展現出來,相似複製黏貼的操做。

class DrawSnow {
  constructor(count,useOffCanvas) {
    ......

    this.useOffCanvas = useOffCanvas; // 是否使用離屏渲染
    this.init();
  }
    
  init() {
    let offScreen = '';

    if (this.useOffCanvas) {
      offScreen = new OffScreen();
    }

    for (let i = 0; i < this.count; i++) {
      let x = this.width * Math.random();
      let y = this.height * Math.random();

      if (this.useOffCanvas) {

        this.snowArray.push({
          instance: offScreen,
          x: x,
          y: y
        });
      } else {
        this.snowArray.push({
          x: x,
          y: y
        });
        this.draw(x, y);
      }
    }
    this.animate();
  }
  
  animate() {
    this.content.clearRect(0, 0, this.width, this.height);

    for (let i in this.snowArray) {
      let snow = this.snowArray[i];

      snow.y += 2;
      if (snow.y >= this.height + 10) {
        snow.y = Math.random() * 50;
      }

      if (this.useOffCanvas) {
        snow.instance.move(this.content, snow.x, snow.y);
      } else {
        this.draw(snow.x, snow.y);
      }
    }
    this.timer = requestAnimationFrame(() => {
      this.animate();
    });
  }
}
複製代碼

效果

小雪

離屏小雪

中雪

離屏中雪

大雪

離屏大雪

性能分析

和非離屏渲染進行對比,發現當粒子數量很少時,差距並不明顯,當粒子數量達到7000時,有了明顯差距。

在上述動圖中,離屏渲染下,大雪動畫的幀率達到平均23FPS,錄屏工具會對性能產生影響,實際的性能以下圖:

離屏大雪截圖

相比非離屏模式下幀率提高了一倍。

如何選擇使用離屏渲染

上述例子中,使用離屏渲染確實提高了動畫運行的幀率,但不是任什麼時候候都適用離屏渲染。

基於上面這個例子,衍生實現另外一個效果,即改變雪花粒子的樣式,隨機選擇粒子的大小位置和透明度,使畫面更有層次感。

案例效果

先觀察一下兩種模式下的實現效果以及幀率,離屏渲染的幀率反而更低,與以前的結果徹底相反。

複雜非離屏大雪截圖

複雜離屏大雪截圖

緣由

新的粒子是隨機生成大小位置和透明度,若是經過以前的方式去構建離屏粒子,那麼每一個粒子的屬性都將相同,沒法實現隨機效果。在本例中,須要經過循環,將不一樣的參數傳遞給構造函數,至關於屢次調用了構造函數的canvas api。與非離屏渲染模式相比,還增長了建立緩衝區,從緩衝區繪製到屏幕上的性能消耗,因此幀率相比非離屏模式,反而更低。

而在以前的例子中,粒子的大小、顏色、透明度都相同,不須要重複構造,因此只調用了一次構造函數,也只調用了一次繪製的canvas api。

相關代碼

觀察下方代碼,結合上文中在離屏模式下的構造方式,能夠發現,本例中循環構造了新的粒子,也就不斷調用了api,並無下降性能的消耗。

init() {
    let offScreen = '';

    for (let i = 0; i < this.count; i++) {
      let x = this.width*Math.random();
      let y = this.height*Math.random();
      let alpha = (Math.floor(Math.random() * 10) + 1) / 10 / 2;
      let color = "rgba(255,255,255," + alpha + ")";
      let r = Math.random() * 2 + 1;

      if (this.useOffCanvas) {
      
        // 循環構造新的粒子
        offScreen = new OffScreen();

        this.snowArray.push({
          instance: offScreen,
          x: x,
          y: y,
          color: color,
          r:r
        });
      } else {
        this.snowArray.push({
          x: x,
          y: y,
          color: color,
          r:r
        });
        this.draw(x,y,color,r);
      }
    }
    this.animate();
  }
複製代碼

FPS

FPS是圖像領域中的定義,是指畫面每秒傳輸幀數,通俗來說就是指動畫或視頻的畫面數。

理論上說,FPS 越高,動畫會越流暢,目前大多數設備的屏幕刷新率爲 60 次/秒,因此一般來說 FPS 爲 60 frame/s 時動畫效果最好。

當不一樣的幀率下,動畫的視覺效果如何呢?

  • 幀率可以達到 50 ~ 60 FPS 的動畫將會至關流暢,讓人倍感溫馨;
  • 幀率在 30 ~ 50 FPS 之間的動畫,因各人敏感程度不一樣,溫馨度因人而異;
  • 幀率在 30 FPS 如下的動畫,讓人感受到明顯的卡頓和不適感; 幀率波動很大的動畫,亦會令人感受到卡頓。

因此流暢的動畫,幀率須要達到30fps往上。

具體分析能夠參考: 【前端性能】Web 動畫幀率(FPS)計算

總結

離屏渲染在動畫優化上很是多人推薦,但也不是任何狀況下均可以利用,離屏渲染首先須要構造一個緩衝區,再將緩衝區中的畫面展現到顯示屏上,這兩個過程也須要消耗性能。

例如上文中第二個例子,並無減小對api的調用,反而離屏的過程增長了性能的消耗,這種狀況就不合適採用這種方式。離屏渲染也須要選擇合理的使用場景。

相關文章
相關標籤/搜索