通过上一片文章,我们知道了一个物体的平抛运动在 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>