1. 程式人生 > >看你骨骼驚奇,這裡有一套canvas粒子動畫方案瞭解一下?

看你骨骼驚奇,這裡有一套canvas粒子動畫方案瞭解一下?

導語:在日常的開發過程中,我們會常常會用到canvas來製作一些動畫特效,其中有一個動畫種類,需要我們生成一定數量,形狀類似且行為基本一致的粒子,通過這些粒子的運動,來展現動畫效果,比如:下雨閃爍的星空。。。此類效果統一可稱為粒子系統動畫

簡單地說,粒子系統是一些粒子的集合,通過指定發射源 (即每個粒子的起始位置) 發射粒子流 (即粒子的動畫效果)

本文具體示例及完整程式碼見 :

canvas粒子動畫系統解決方案

本文目錄:

1. 粒子系統的共性

首先我們觀察一個簡單的粒子動畫效果,如下圖:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge"
>
<title>Document</title> </head> <body> <canvas id="example"></canvas> </body> <script> var cvs = document.getElementById('example'); var ctx = cvs.getContext('2d'); var width = 400; var height = 400; cvs.width = 400; cvs.height = 400
; var particle = []; var lineAnimation; function createItem(amount) { for (let i = 0; i < amount; i++) { particle.push({ posX: Math.round(Math.random() * width), posY: Math.round(Math.random() * height), r: 4, color: Math.random() < 0.5 ? '#d63e3e' : '#23409b' }); } draw(); }; function draw() { ctx.clearRect(0, 0, width, height); particle.map((item, index) => { ctx.beginPath(); ctx.arc(item.posX, item.posY, item.r, 0, 2 * Math.PI); ctx.fillStyle = item.color; ctx.fill(); //畫實心圓 ctx.closePath(); item.posY = item.posY + 2; if (item.posY > height) { item.posX = Math.round(Math.random() * width); item.posY = Math.round(Math.random() * height); }; }) lineAnimation = requestAnimationFrame(draw); } function stop() { cancelAnimationFrame(lineAnimation); } createItem(100);
</script> </html> 複製程式碼

分析下上述程式碼,我們可以總結出粒子系統的一些特性:

1. 建立 canvas 畫布。

2. 初始化粒子(建立粒子形狀,確定粒子的起始位置)。

3. 繪製粒子到畫布。

4. 定義粒子的運動方式(即粒子的運動動畫)。

5. 控制動畫的播放與暫停。

6. 清除畫布。

既然粒子系統有這麼多的通用性, 為什麼我們不能把其中通用的地方抽離出來,建立一個粒子系統呢?

現在正式進入文章的第二部分, 開始搭建一個粒子系統

開始搭建一個粒子系統(基於es6)

根據上一部分總結出的共性,我們可以寫出一個粒子系統的大概組成程式碼:


const STATUS_RUN = 'run';
const STATUS_STOP = 'stop';

//粒子系統基類
class Particle {
    //1. 建立 `canvas` 畫布
    constructor(idName, width, height, options) {
        this.canvas = document.getElementById(`${idName}`);
        this.ctx = this.canvas.getContext('2d'); //canvas執行上下文
        this.timer = null; //動畫執行定時器,採用requestAnimationFrame
        this.status = STATUS_STOP; //動畫執行狀態 預設為stop
        this.options = options || {}; //配置(粒子數量,速度等)
        this.canvas.width = width;
        this.canvas.height = height;
        this.width = width;
        this.height = height;
        this.init();
    };
    //2. 初始化粒子
    init() {

    };
    //3. 繪製粒子到畫布
    draw() {
        let self = this;
        let { ctx, width, height } = this;
        ctx.clearRect(0, 0, width, height);
        this.moveFunc(ctx, width, height);
        this.timer = requestAnimationFrame(() => {
            self.draw();
        });
    };
    //4. 定義粒子的運動方式
    moveFunc() {

    };
    //5. 控制動畫的播放與暫停。
    run() {
        if (this.status !== STATUS_RUN) {
            this.status = STATUS_RUN;
            this.draw();
        }
    };
    stop() {
        this.status = STATUS_STOP;
        cancelAnimationFrame(this.timer);
    };
    //6. 清除畫布
    clear() {
        this.stop();
        this.ctx.clearRect(0, 0, this.width, this.height);
    };

};

export {
    Particle
}
複製程式碼

我們通過這個方法改寫下最開始的例子:

import { Particle } from "../lib/particleI.js";

class exampleMove extends Particle {
    //2. 初始化粒子
    init() {
        this.particle = [];
        let amount = this.options.amount;
        let { width, height } = this;
        for (let i = 0; i < amount; i++) {
            this.particle.push({
                posX: Math.round(Math.random() * width),
                posY: Math.round(Math.random() * height),
                r: 4,
                color: Math.random() < 0.5 ? '#d63e3e' : '#23409b'
            });
        }
    };
    //4. 定義粒子的運動方式
    moveFunc(ctx, width, height) {
        this.particle.map(item => {
            item.posY = item.posY + 2;
            if (item.posY > height) {
                item.posX = Math.round(Math.random() * width);
                item.posY = Math.round(Math.random() * height);
            };
            this.createParticle(ctx, item.posX, item.posY, item.r, item.color);
        });
    };
    //粒子形狀
    createParticle(ctx, x, y, r, color) {
        ctx.fillStyle = color;
        ctx.beginPath();
        ctx.arc(x, y, r, 0, 2 * Math.PI);
        ctx.closePath();
        ctx.fill();
    };
    //4. 定義粒子的運動方式
    moveFunc(ctx, width, height) {
        this.particle.map(item => {
            item.posY = item.posY + 2;
            if (item.posY > height) {
                item.posX = Math.round(Math.random() * width);
                item.posY = Math.round(Math.random() * height);
            };
            this.createParticle(ctx, item.posX, item.posY, item.r, item.color);
        });
    };
}

複製程式碼

新建例項,讓粒子系統運動:

var example = new exampleMove('example', 400, 400, { speed: 3, amount: 8 });
example.run();
複製程式碼

寫到這裡, 一個小小的粒子系統就搭建完成了,我們看下總結看下:

關於粒子系統的這些共性:

1. 建立 canvas 畫布。 (基類完成)

2. 初始化粒子(建立粒子形狀,確定粒子的起始位置)。

3. 繪製粒子到畫布 (基類完成)

4. 定義粒子的運動方式(即粒子的運動動畫)。

5. 控制動畫的播放與暫停(基類完成)

6. 清除畫布(基類完成)

由於每個人的粒子動畫的展現方式有所不同,所以2、4兩點需要,自己繼承進行修改。

文章到此你以為就完了嘛?

我們把剛才搭建的粒子系統的數量提高到 6000 個看一下:

幀率在30左右非常低!!!一般幀率應該要保持在60,否則動畫會出現卡頓感!!!

ps:關於效能分析,可以看我之前的一篇總結:兄dei,聽說你動畫很卡?

那我們改咋辦呢?老鐵?

現在就讓我們進入第三部分 加入離屏渲染優化你的粒子系統

加入離屏渲染優化你的粒子系統

在開始之前,我們是不是要分析一下,為什麼我們的粒子動畫到達一定數量以後會卡!!

根據chrome 效能分析工具,觀察下圖:

不難看出每一幀大部分的時間消耗都在canvas Api的呼叫中。

如何解決這個問題?

看似一個 ctx.fillStyle = '#f00' 整的跟 var a = '#f00' 差不多似的,實際的消耗是遠遠大約簡單的變數賦值的,如下程式碼:

var cvs = document.getElementById('example');
    var ctx = cvs.getContext('2d');

    var timeStart = (new Date()).getTime();
    var count;
    for (var i = 0; i < Math.pow(10, 7); i++) {
        // ctx.fillStyle = '#f00';
        count = '#f00';
    };
    var timeEnd = (new Date()).getTime();
    console.log('during:::', timeEnd - timeStart);
複製程式碼

所以我們解決問題的關鍵就是要儘可能減少呼叫渲染相關 API 的次數。

這時就需要用到我們的離屏渲染機制啦!!!

所謂離屏渲染,其實就是為了避開每一幀頻繁的呼叫渲染相關 API 的次數,那麼該如何避開呢?

離屏渲染原理

我們為每個粒子單獨建立一個canvas畫布,把粒子先在畫布中畫出。

如下程式碼(完整程式碼 canvas粒子系統):

// 離屏粒子類(這裡的畫布大小盡量和粒子大小保持一致,畫布太大也會消耗效能);
class offScreenItem {
    constructor(width, height, create) {
        this.canvas = document.createElement('canvas');
        this.width = this.canvas.width = width * 2;
        this.height = this.canvas.height = height * 2;
        this.ctx = this.canvas.getContext('2d');
        //在畫布上繪製粒子
        create(this.ctx, this.width, this.height);
    };
    
    // 移動粒子(使用 drawImage 方法,通過改變粒子canvas畫布的位置,達到運動的效果)
    move(ctx, x, y) {
        if (this.canvas.width && this.canvas.height) {
            ctx.drawImage(this.canvas, x, y);
        }
    }
}
複製程式碼

我們來看下,開啟離屏渲染後的效能如何?

同樣是6000個粒子,但是幀率已經幾乎回到了60, 開森!!!。

注意:

在建立離屏粒子例項時,一定要按種類建立,比如,上圖中,實際上我只有紅藍兩種圓,所以只要例項化兩次就好,千萬不要每一個粒子都例項化一次,會十分消耗記憶體,還不如沒開啟離屏渲染的時候。

關於粒子系統原始碼使用說明:

import { Particle, offScreenItem } from "../lib/particle.js";

class exampleMove extends Particle {
    //粒子形狀繪製
    createParticle(ctx, x, y, r, color) {
        //todo...
    };
    //粒子如何運動
    moveFunc(ctx, width, height) {
        //todo...
    };
    //離屏粒子初始化位置
    createOffScreenInstance(width, height, amount) {
        //todo...
    };

    //正常粒子初始化位置
    createNormalInstance(width, height, amount) {
        //todo...
    }
}
/**
 * @param {[String]} id [canvas畫布的id]
 * @param {[Number]} width [canvas畫布的寬]
 * @param {[Number]} height [canvas畫布的高]
 * @param {[Object]} option [粒子系統的配置{speed: 3, amount: 800}]
 * @param {[Boolean]} offScreen [是否採用離屏渲染]
 * */ 
var example = new exampleMove(id, width, height, option, offScreen);

//運動
example.run();
//停止
example.stop();
//清理畫布
example.clear();

複製程式碼

總結:

通過本文,應該清楚瞭如下內容:

1. 什麼是粒子系統?

2. 為什麼我們需要寫一個粒子系統?

3. 當粒子數量到達一定的瓶頸,我們應該如何優化?

scanvas 本身有很多可以優化的點,效能問題,不是能夠單單的靠一兩個通用的解決方案就全部解決的,本文只是其中一個方向,希望可以給大家帶來一些啟發和思考。