阅读 3198

看你骨骼惊奇,这里有一套 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 本身有很多可以优化的点,性能问题,不是能够单单的靠一两个通用的解决方案就全部解决的,本文只是其中一个方向,希望可以给大家带来一些启发和思考。

关注下面的标签,发现更多相似文章
评论