通过上一片文章,我们知道了一个物体的平抛运动在 HTML5 Canvas 中的实现原理。想一想一朵烟花的“爆炸”,其效果好像多个烟花颗粒的平抛运动的合集。按照这个思路,让我们来尝试实现这个烟花效果。

由于要画出多个烟花颗粒,按常规的画法缺点太多,要管理每个烟花颗粒的运动,最好是将一颗烟花颗粒封装为一个 颗粒对象 ,就像是将一个产品的生产过程模块化,模块化后只需提供生产产品所需的材料就可以了。

首先,我将之前的平抛代码封装起来:

    function Particle() {
        this.x = 100, this.y = 60;
        this.g = 1;
        this.v = 2;
        this.angle = 30;

        this.update = function () {
            // 匀速直线运动
            var vx = Math.cos(this.angle) * this.v;
            this.x += vx;

            // 匀加速直线运动
            var vy = Math.sin(this.angle) * this.v + this.g;
            this.y += vy;
        }

        this.draw = function () {
            ctx.beginPath();
            ctx.arc(this.x, this.y, 2, 0, 2 * Math.PI);
            ctx.fillStyle = 'hsla(0, 100%, 50%, 1)';
            ctx.fill();
        }
    }

其中,烟花颗粒包含了基本的运动参数,还将烟花颗粒的绘制和运动封装为函数,但这些远远不够。

现实生活中的烟花有着不同的颜色,每一颗爆炸的颗粒都会以不同的速度朝着不同的方向四散而去,因为在空气中运动会遭遇空气阻力,越远离爆炸中心,运动的速度越慢,并且,它是有生命周期的,发光的过程中会持续消耗能量,在耗尽最后一丝能量时,它变消失不见。

于是,我丰富了这个烟花颗粒对象:

    // 烟花颗粒对象,参数 hue 是烟花颗粒的颜色色值
    function Particle(hue) {
        // 烟花颗粒爆炸的坐标
        this.x = 100, this.y = 60;
        // 重力加速度
        this.g = 1;
        // 随机烟花颗粒的运动速度,烟花颗粒不会出现运动同样距离的奇怪画面
        this.v = random(2, 4);
        // 随机烟花颗粒的运动角度,烟花颗粒会向 360 度随机运动
        this.angle = random(0, Math.PI * 2);
        // 烟花颗粒的颜色值
        this.hue = hue;
        // 烟花颗粒的亮度
        this.brightness = random(50, 80);
        // 烟花颗粒的透明度
        this.alpha = 1;
        // 随机烟花颗粒透明度衰减幅度,烟花颗粒不会表现出全部烟花颗粒在同一时间消失的奇怪画面
        this.decay = random(0.015, 0.03);

        this.update = function () {
            // 匀速直线运动
            var vx = Math.cos(this.angle) * this.v;
            this.x += vx;

            // 匀加速直线运动
            var vy = Math.sin(this.angle) * this.v + this.g;
            this.y += vy;

            // 烟花爆炸的后,颗粒在空气中的运动速度衰减
            this.v *= 0.95;

            // 逐渐减少烟花颗粒的透明度
            this.alpha -= this.decay;
        }

        this.draw = function () {
            // 画出颗粒
            ctx.beginPath();
            ctx.arc(this.x, this.y, 2, 0, 2 * Math.PI);
            ctx.fillStyle = 'hsla(' + this.hue + ', 100%, ' + this.brightness + '%, ' + this.alpha + ')';
            ctx.fill();
        }
    }

    function random(min, max) {
        return Math.random() * (max - min) + min;
    }

由上面代码描述可见,我们可以将烟花控制的非常仿真了!

这样,我可以很方便的在画布中加入多个烟花颗粒,比如一颗烟花的炸裂可以出现 30 到 50 颗烟花颗粒:

var particlesCount = random(30, 50)
var hue = random(0, 330);
for (var i = 0; i < particlesCount; i++) {
    new Particle(hue);
}

接着,我们定义一个数组来统一管理这些烟花颗粒,并使用定时器让整个动画运行起来:

    var particles = []
    var particlesCount = random(30, 50)

    setInterval(function () {
        ctx.clearRect(0, 0, 200, 200);
        
        // 无烟花颗粒时,点燃新烟花
        if (particles.length == 0) {
            var hue = random(0, 330);
            for (var i = 0; i < particlesCount; i++) {
                particles.push(new Particle(hue));
            }
        }

        // 持续更新每个烟花颗粒的状态
        for (var i = 0; i < particles.length; i++) {
            particles[i].update();
            particles[i].draw();
            
            // 当一颗烟花颗粒消失时,销毁这颗烟花
            if (particles[i].alpha <= particles[i].decay) {
                particles.splice(i, 1);
            }
        }
    }, 1000 / 60)

烟花终于爆炸了,但效果有点不对劲,好像少了一条小尾巴。

代码中,我们在画每一帧之前都清除了画布,如果要加上小尾巴其实也很简单,就是不要清除画布,而是覆盖一层新的有透明度的天空上去。

将代码:
ctx.clearRect(0, 0, 200, 200);

替换成:
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
ctx.fillRect(0, 0, 200, 200);

最终代码:

<html>
<body>
    <canvas id="myCanvas" width="200" height="200" style="background-color: black;">
        你的浏览器不支持canvas,请升级你的浏览器
    </canvas>
</body>
</html>

<script>
    // 准备画布
    var canvas = document.getElementById("myCanvas");
    var ctx = canvas.getContext("2d");

    function random(min, max) {
        return Math.random() * (max - min) + min;
    }

    // 烟花颗粒对象,参数 hue 是烟花颗粒的颜色色值
    function Particle(hue) {
        // 烟花颗粒爆炸的坐标
        this.x = 100, this.y = 60;
        // 重力加速度
        this.g = 1;
        // 随机烟花颗粒的运动速度,烟花颗粒不会出现运动同样距离的奇怪画面
        this.v = random(2, 4);
        // 随机烟花颗粒的运动角度,烟花颗粒会向 360 度随机运动
        this.angle = random(0, Math.PI * 2);
        // 烟花颗粒的颜色值
        this.hue = hue;
        // 烟花颗粒的亮度
        this.brightness = random(50, 80);
        // 烟花颗粒的透明度
        this.alpha = 1;
        // 随机烟花颗粒透明度衰减幅度,烟花颗粒不会表现出全部烟花颗粒在同一时间消失的奇怪画面
        this.decay = random(0.015, 0.03);

        this.update = function () {
            // 匀速直线运动
            var vx = Math.cos(this.angle) * this.v;
            this.x += vx;

            // 匀加速直线运动
            var vy = Math.sin(this.angle) * this.v + this.g;
            this.y += vy;

            // 烟花爆炸的后,颗粒在空气中的运动速度衰减
            this.v *= 0.95;

            // 逐渐减少烟花颗粒的透明度
            this.alpha -= this.decay;
        }

        this.draw = function () {
            // 画出颗粒
            ctx.beginPath();
            ctx.arc(this.x, this.y, 2, 0, 2 * Math.PI);
            ctx.fillStyle = 'hsla(' + this.hue + ', 100%, ' + this.brightness + '%, ' + this.alpha + ')';
            ctx.fill();
        }
    }

    var particles = []
    var particlesCount = random(30, 50)

    setInterval(function () {
        ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
        ctx.fillRect(0, 0, 200, 200);
        
        // 无烟花颗粒时,点燃新烟花
        if (particles.length == 0) {
            var hue = random(0, 330);
            for (var i = 0; i < particlesCount; i++) {
                particles.push(new Particle(hue));
            }
        }

        // 持续更新每个烟花颗粒的状态
        for (var i = 0; i < particles.length; i++) {
            particles[i].update();
            particles[i].draw();
            
            // 当一颗烟花颗粒消失时,销毁这颗烟花
            if (particles[i].alpha <= particles[i].decay) {
                particles.splice(i, 1);
            }
        }
    }, 1000 / 60)


</script>
你的浏览器不支持canvas,请升级你的浏览器