多代码,慎读!!!
预览
完整项目预览----预览地址;
属性设计
烟花状态:烟花应有三个状态:
- 升空
- 等待炸裂
- 炸裂后
烟花:发射点(x, y),爆炸点(xEnd, yEnd),升空后等待炸裂时间(wait),炸裂后微粒个数(count),烟花半径(radius)
烟花炸裂后微粒:自身位置(x, y),自身大小(size),自身速度(rate),最大烟花半径(radius)。
config:为全局变量,以及控制参数,包括画布宽高,设定烟花属性等。
设定全局变量
const config = {
width: 360,
height: 600,
canvases: ['bg', 'firework'],
skyColor: '210, 60%, 5%, 0.2)',
fireworkTime:{min:30,max:60},
//烟花参数本身有默认值 传入undefined则使用默认参数
fireworkOpt:{
x: undefined,
y: undefined,
xEnd: undefined,
yEnd: undefined,
count: 300, //炸裂后粒子数
wait: undefined, //消失后 => 炸裂 等待时间
}
}
构建微粒类
class Particle{
//默认值写法
constructor({x, y, size = 1, radius = 1.2} = {}){
this.x = x;
this.y = y;
this.size = size;
this.rate = Math.random(); //每个微粒移动的速度都是随机不同的
this.angle = Math.PI * 2 * Math.random(); //每个微粒的偏移角度
//每次微粒移动速度分解为横纵坐标的移动。
this.vx = radius * Math.cos(this.angle) * this.rate;
this.vy = radius * Math.sin(this.angle) * this.rate;
}
go(){
this.x += this.vx;
this.y += this.vy;
this.vy += 0.02; //重力影响 y越大实际越偏下
//空气阻力
this.vx *= 0.98;
this.vy *= 0.98;
}
//画出微粒的位置
render(ctx){
this.go();
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2, false);
ctx.fill();
}
}
构建烟花类
class Firework{
constructor({x, y = config.height, xEnd, yEnd, count = 300, wait} = {}){
//烟花自身属性
this.x = x || config.width / 8 + Math.random() * config.width * 3 / 4;
this.y = y;
this.xEnd = xEnd || this.x;
this.yEnd = yEnd || config.width / 8 + Math.random() * config.width * 3 / 8;
this.size = 2;
this.velocity = -3;
this.opacity = 0.8;
this.color = `hsla(${360 * Math.random() | 0},80%,60%,1)`;
this.wait = wait || 30 + Math.random() * 30;
//微粒个数等
this.count = count;
this.particles = [];
this.createParticles();
this.status = 1;
}
//创建微粒
createParticles(){
for(let i = 0;i < this.count; ++i){
this.particles.push(new Particle({x:this.xEnd, y:this.yEnd}));
}
}
//升空
rise(){
this.y += this.velocity * 1;
this.velocity += 0.005; //升空时产生的阻力
//烟花升空到目标位置开始渐隐
if(this.y - this.yEnd <= 50){
this.opacity = (this.y - this.yEnd) / 50;
}
//如果到了目标位置 就开始第二个状态
if(this.y <= this.yEnd){
this.status = 2;
}
}
//渲染烟花 烟花所有动作完成之后返回false
render(ctx){
switch(this.status){
case 1: //升空
ctx.save();
ctx.beginPath();
ctx.globalCompositeOperation = 'lighter';
ctx.globalAlpha = this.opacity;
ctx.translate(this.x, this.y);
ctx.scale(0.8, 2.3);
ctx.translate(-this.x, -this.y);
ctx.fillStyle = this.color;
ctx.arc(this.x + Math.sin(Math.PI * 2 * Math.random()) / 1.2, this.y, this.size, 0, Math.PI * 2, false);
ctx.fill();
ctx.restore();
this.rise();
return true;
break;
case 2: //烟花消失阶段,等待炸裂
if(--this.wait <= 0){
this.opacity = 1;
this.status = 3;
}
return true;
break;
case 3: //炸裂之后 渲染烟花微粒
ctx.save();
ctx.globalCompositeOperation = 'lighter';
ctx.globalAlpha = this.opacity;
ctx.fillStyle = this.color;
for(let i = 0;i < this.particles.length;++i){
this.particles[i].render(ctx);
}
ctx.restore();
this.opacity -= 0.01;
return this.opacity > 0;
break;
default:
return false;
}
}
}
放烟花
const canvas = {
init: function(){
//一些属性的设定 可以不用管
this.setProperty();
this.renderBg();
//循环体 **主要
this.loop();
},
setProperty: function(){
this.fireworks = [];
this.width = config.width;
this.height = config.height;
this.fireworkTime = (config.fireworkTime.min + (config.fireworkTime.max - config.fireworkTime.min) * Math.random()) | 0;
this.bgCtx = document.querySelector('#bg').getContext('2d');
this.fireworkCtx = document.querySelector('#firework').getContext('2d');
},
renderBg(){
this.bgCtx.fillStyle = 'hsla(210, 60%, 5%, 0.9)'
this.bgCtx.fillRect(0, 0, this.width, this.height);
},
loop(){
requestAnimationFrame(this.loop.bind(this));
this.fireworkCtx.clearRect(0, 0, this.width, this.height);
//随机创建烟花
if(--this.fireworkTime <= 0){
this.fireworks.push(new Firework(config.fireworkOpt));
//每次到点之后重新设置烟花产生时间 (|0转化为整数)
this.fireworkTime = (config.fireworkTime.min + (config.fireworkTime.max - config.fireworkTime.min) * Math.random()) | 0;
}
for(let i = this.fireworks.length - 1; i >= 0; --i){
//渲染烟花 (若返回值为false则移除烟花)
!this.fireworks[i].render(this.fireworkCtx) && this.fireworks.splice(i,1);
}
}
}
canvas.init();
完善
此时烟花是这样的,感觉少了点小尾巴。
现在我们每一帧都是清除了画布,如果要加上小尾巴其实也很简单,每一帧都不要清除画布,而是覆盖一层新的有透明度的天空上去。
//canvas.loop方法
// this.fireworkCtx.clearRect(0, 0, this.width, this.height);
this.fireworkCtx.fillStyle = config.skyColor;
this.fireworkCtx.fillRect(0,0,this.width,this.height);
这时就变成这样了。
但是,还是缺少了在爆炸瞬间 天空变亮的场景。
那么在画烟花的时候,先会获取一下烟花的颜色以及透明度。
// *****Firework constructor
// this.color = `hsla(${360 * Math.random() | 0},80%,60%,1)`;
this.hue = 360 * Math.random() | 0;
this.color = `hsla(${this.hue},80%,60%,1)`;
// *****Firework 新增实例方法
getSkyColor(){
const skyColor = {
//只有炸裂阶段才返回亮度
lightness: this.status == 3 ? this.opacity : 0 ,
hue: this.hue
};
return skyColor;
}
// *****config 修改config的skyColor
// skyColor: 'hsla(210, 60%, 5%, 0.2)',
skyColor: 'hsla({hue}, 60%, {lightness}%, 0.2)',
// canvas.loop方法
//this.fireworkCtx.fillStyle = config.skyColor;
//每次替换色调与亮度值。
this.fireworkCtx.fillStyle = config.skyColor.replace('{lightness}', 5 + this.skyColor.lightness * 15).replace('{hue}' , this.skyColor.hue);
this.skyColor = { //新增
lightness: 0,
hue: 210
};
for(let i = this.fireworks.length - 1; i >= 0; --i){
//新增 天空颜色为最亮的烟花的颜色
this.skyColor = this.skyColor.lightness >= this.fireworks[i].getSkyColor().lightness ? this.skyColor : this.fireworks[i].getSkyColor();
!this.fireworks[i].render(this.fireworkCtx) && this.fireworks.splice(i,1);
}
到现在就算是大功告成了。
完整项目
github项目地址
如果觉得还不错,请star一个吧。
烟花制作参考链接
参考了codepen.io上的很多作品。
主要参考 --- fireworks seen in the countryside
fireworks seen in the countryside