详细讲解Canvas实现烟花效果
一、问题分析
问题分析的好坏直接决定着我们后面的实现效果,采用哪种平台或者语言无关紧要,希望这一篇文章能够讲述清楚烟花燃放的整个分析思路。
首先,我们可以想想一个烟花从地面发射到空中爆炸的整个过程。烟花从地面以一定的速度发出,并在火药的推力下加速运动,运动一定时间后爆炸,爆炸产生小火花,向四周散开,并逐渐变暗直至消失。
从上述的描述中我们总结以下几点:
- 一束烟花从地面发射
- 烟花以一定的加速度向某个方向飞去
- 飞行一定时间后爆炸
- 爆炸产生一定数量的小火花
- 小火花向四周散开,并逐渐变暗
二、设计
烟花(Firework)
属性
1.定义一个烟花对象
function Firework (){}
2.烟花从地面发射,飞行一定时间后爆炸,为了产生高低不一、四处发散的效果,我们随机产生一个目标位置,当烟花到达此位置时即爆炸。此外,我们还想提供一个方法,当鼠标点击某个位置时,这个位置将作为烟花爆炸的地方,也就说通过鼠标给定烟花的爆炸位置。因此,我们在构造烟花对象时传入两个位置,一个位置是起始位置,一个位置是结束位置,这个位置可以由鼠标点击给定,也可以随机产生。这样一来,也解决了烟花发射方向的问题。
function Firework (conf) {
// 烟花的开始位置
this.sx = conf['startX'];
this.sy = conf['startY'];
// 烟花的结束位置
this.ex = conf['endX'];
this.ey = conf['endY'];
// 烟花的当前位置
this.x = this.sx;
this.y = this.sy;
// 烟花发射的角度,计算烟花速度水平和垂直分量时有用
this.angle = Math.atan2( this.ey-this.sy, this.ex-this.sx );
}
3.我们实现一个随机算法,给定一个范围,随机产生一个数字:
function random ( min, max ) {
return Math.random() * ( max - min ) + min;
}
4.创建烟花时,x的范围取决于绘图区域的宽度,y的范围取决于绘图区域的高度,因此我们可以通过下面的方式随机创建一个烟花:
// cw为绘图区域宽度
var cw = window.innerWidth,
// ch为绘图区域高度
ch = window.innerHeight;
conf['startX'] = cw/2;
conf['startY'] = ch;
conf['endX'] = random(0, cw);
conf['endY'] = random(0, ch/2);
new Firework(conf);
5.烟花的起点和终点确定之后,那么烟花的移动距离也就确定了,采用勾股定理,因此我们实现一个计算距离的函数:
// calculate the distance between two points
function calculateDistance( p1x, p1y, p2x, p2y ) {
var xDistance = p1x - p2x,
yDistance = p1y - p2y;
return Math.sqrt( Math.pow( xDistance, 2 ) + Math.pow( yDistance, 2 ) );
}
var Firework = function (conf) {
this.distanceToTarget = calculateDistance(conf['startX'], conf['startY'], conf['endX'], conf['endY']);
}
6.烟花以一定的加速度向空中射出,那么我们就需要定义烟花的初始速度和加速度:
var Firework = function (conf) {
this.speed = conf['speed'] || 15;
this.acceleration = conf['acceleration '] || 1.05;
}
7.因为人的视觉暂留现象,所以烟花在移动过程中还要有一个尾巴。尾巴的实现主要是从前面经过的某个位置向当前位置画一条直线,因为我们需要记录移动过程一些位置的坐标。尾巴的长短主要取决于记录的早晚,我们采取队列实现,在每帧更新时记录下当时的位置存入队尾,并从队首移除最早的记录,因此队列的大小实质上就决定了尾巴的长短。
var Firework = function (conf) {
this.coordinates = [];
this.coordinatesCount = conf['coordinatesCount'] || 3;
while (this.coordinatesCount --) {
this.coordinates.push( [this.sx, this.sy] );
}
}
8.最终要的一点不要忘了,那就是烟花的颜色,我们采用HSL色彩模式。
var Firework = function ( conf ) {
// 色相
this.hue = conf['hue'] || 120;
// 明亮度
this.lightness = conf['lightness'] || random( 50, 70)
}
方法
1.在每次帧更新时需要画出烟花的轨迹,之前我们提到了烟花的尾巴。
// canvas 为绘图画布
// ctx 为二维绘图对象
var ctx = canvas.getContext( '2d' );
Firework.prototype.draw = function () {
ctx.beginPath();
// 移动到最早记录的那个位置
ctx.moveTo( this.coordinates[this.coordinates.length - 1][0], this.coordinates[this.coordinates.length - 1][1], );
ctx.lineTo( this.x, this.y );
ctx.strokeStyle = 'hsl(' + this.hue + ', 100%, ' + this.lightness + '%)';
ctx.stroke();
}
2.此外,在每帧要更新烟花的位置,当烟花到达结束位置时,产生爆炸效果。
Firework.prototype.update = function () {
// 从用于实现烟花尾巴的队列中移除最早的位置
this.coordinates.pop();
// 记录当前位置
this.coordinates.unshift( [this.x, this.y] );
// 计算当前速度
this.speed *= this.acceleration;
// 计算烟花此时速度在水平和垂直方向上的分量,其实就是从上一帧到这一帧之间烟花移动的水平和垂直方向上的距离
var vx = Math.cos( this.angle ) * this.speed,
vy = vy = Math.sin( this.angle ) * this.speed;
// 计算烟花已经移动的距离,当距离超出最大距离时,产生爆炸
this.distanceTraveled = calculateDistance( this.sx, this.sy, this.x + vx, this.y + vy );
if( this.distanceTraveled >= this.distanceToTarget ) {
// 产生爆炸
// 销毁当前烟花
} else {// 更新当前位置
this.x += vx;
this.y += vy;
}
}
爆炸效果
爆炸效果其实和烟花一样,我们在这里实现一个简易的粒子系统。
属性
1.定义一个粒子对象
function Particle( x, y ) {
// 粒子的初始位置
this.x = x;
this.y = y;
}
2.和烟花一样,粒子在移动的过程中也要有尾巴。
function Particle( x, y ) {
this.coordinates = [];
this.coordinateCount = 6;
while( this.coordinateCount-- ) {
this.coordinates.push( [this.x, this.y] );
}
}
3.粒子在爆炸时,方向是向四周随机的,初始速度也是随机的,但都是有一定的范围。
function Particle( x, y ) {
this.angle = random( 0, Math.PI * 2);
this.speed = random( 1, 10);
}
4.粒子在飞行过程中,受到运动方向的空气阻力friction和垂直方向的重力gravity
function Particle( x, y ) {
this.friction = 0.98;
this.gravity = 1.2;
}
5.粒子在飞行的过程中逐渐变暗,并在一定时间后消亡,因此我们定义一个粒子颜色的透明度alpha和粒子的消亡速度decay。
function Particle( x, y ) {
this.alpha = 1;// 初始时不透明
this.decay = random( 0.005, 0.05 );
}
方法
1.粒子和烟花一样,也要进行绘制和更新。
Particle.prototype.draw = function() {
ctx.beginPath();
ctx.moveTo( this.coordinates[ this.coordinates.length-1 ][0], this.coordinates[ this.coordinates.length-1 ][ 1 ] );
ctx.lineTo( this.x, this.y );
ctx.strokeStyle = 'hsla(' + this.hue + ', 100%' + this.brightness + '%, ' + this.alpha + ')';
ctx.stroke();
}
2.粒子在每次帧更新时也要进行更新
Particle.prototype.update= function() {
// 从用于实现粒子尾巴的队列中移除最早的位置
this.coordinates.pop();
// 记录粒子的当前位置
this.coordinates.unshift( [this.x, this.y] );
// 计算粒子此时的速度
this.speed *= this.fraction;
// 计算粒子移动后的水平和垂直方向的位置
this.x += Math.sin( this.angle ) * this.speed;
this.y += Math.cos( this.angle ) * this.speed + this.gravity;
// 计算粒子的颜色透明度
this.alpha -= this.decay;
// 判断粒子是否消亡
if (this.alpha < this.decay) {
// 从粒子集合中销毁粒子
}
}
三、整合及交互处理
上面主要讲解单个烟花和粒子的实现原理,那么在具体的使用场景中我们需要创建两个集合,用于保存、记录、查找动态创建的烟花和粒子。
烟花集合
var fireworks = [];
那么,在销毁烟花时我们就可以采用index进行索引查找
Firework.prototype.update = function ( index ) {
// 此处代码省略
if( this.distanceTraveled >= this.distanceToTarget ) {
// 产生爆炸
var particleCount = random(10,20);// 随机生成爆炸后产生的粒子数量
while( particleCount-- ) {
particles.push( new Particle( x, y ) );
}
// 销毁当前烟花
fireworks.splice( index, 1);
} else {// 更新当前位置
this.x += vx;
this.y += vy;
}
}
粒子集合
var particles = [];
在销毁粒子时我们就可以采用index进行索引查找
Particle.prototype.update= function() {
// 此处代码省略
// 判断粒子是否消亡
if (this.alpha < this.decay) {
// 从粒子集合中销毁粒子
particles.splice( index, 1 );
}
}
帧更新
主要用于每一帧更新时所要做的处理:重绘烟花、重绘爆炸
function frameUpdate () {
// 画布设置
ctx.globalCompositeOperation = 'destination-out';
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect( 0, 0, cw, ch );
ctx.globalCompositeOperation = 'lighter';
// 更新烟花
var i = fireworks.length;
while( i-- ) {
fireworks[ i ].draw();
fireworks[ i ].update( i );
}
// 更新粒子
var i = particles.length;
while( i-- ) {
particles[ i ].draw();
particles[ i ].update( i );
}
}
我们使用window.requestAnimationFrame() 方法来告诉浏览器需要执行的动画,并让浏览器在下一次重绘之前调用指定的函数来更新动画。
requestAnimationFrame( frameUpdate );
自动生成烟花
此时,我们还没有创建任何的烟花。我们希望设置一个定时时间timerTotal,周期性的产生一个烟花,我们也需要一个时间计数timerTick,在每次帧更新的时候加1,记下帧更新的次数。
var timerTick = 51,
timerTotal = 50;
function frameUpdate () {
conf['hue'] = ( conf['hue'] + 0.5 );
if ( conf['hue'] > 360 ) {
conf['hue'] = 0;
}
// 当计数超过指定值时,产生一个烟花
if (timerTick > timerTotal) {
conf['endX'] = random(0, cw);
conf['endY'] = random(0, ch/2);
fireworks.push( new Firework( conf ) );
timerTick = 0;
} else {
timerTick++;
}
}
鼠标点击产生烟花
在最开始的时候我们说过,当鼠标点击时,以鼠标点击的位置作为烟花的终点,产生一个烟花,鼠标按下不放滑动时会连续产生烟花。因此我们需要在鼠标单击时,记录下单击位置mouseX,mouseY,此外还要记录下鼠标的按下状态mouseDown。
var mouseX,
mouseY,
mouseDown = false;
canvas.addListener ( 'mousemove', function ( e ) {
mouseX = e.pageX - canvas.offsetLeft;
mouseY = e.pageY - canvas.offsetTop;
});
canvas.addListener( 'mousedown', function ( e ) {
e.preventDefault();
mousedown = true;
});
canvas.addListener( 'mouseup', function ( e ) {
e.preventDefault();
mousedown = false;
});
因为帧更新的速度很快,而鼠标按下弹起的速度较慢,为了限制避免在每帧更新时产生很多烟花,因此设定了一个limiterTick,只有超过limiterTotal值并且鼠标按下时,才会产生烟花。
var limiterTick = 0,
limiterTotal = 8;
function frameUpdate () {
requestAnimationFrame( frameUpdate );
// 当计数超过指定值时,产生一个烟花
if (timerTick > timerTotal) {
if( !mouseDown) {
conf['endX'] = random(0, cw);
conf['endY'] = random(0, ch/2);
fireworks.push( new Firework( conf ) );
timerTick = 0;
}
} else {
timerTick++;
}
if( limiterTick > limiterTotal ) {
if( mouseDown) {
conf['endX'] = mouseX;
conf['endY'] = mouseY;
fireworks.push( new Firework( conf ) );
limiterTick = 0;
}
} else {
limiterTick++;
}
}
初始化变量
var conf = [],
ctx = canvas.getContext( '2d' )
cw = window.innerWidth,
ch = window.innerHeight,
fireworks = [],
particles = [],
timerTick = 51,
timerTotal = 50
mouseX,
mouseY,
mouseDown = false,
limiterTick = 0,
limiterTotal = 8;
canvas.width = cw;
canvas.height = ch;
conf['startX'] = cw/2;
conf['startY'] = ch;
conf['endX'] = ;
conf['endY'] = ;
conf['hue'] = 100;
封装为jQuery插件
var firework = function(canvas, config) {
// 变量定义省略
$.extend(conf, config);
function random ( min, max ) {}
function calculateDistance( p1x, p1y, p2x, p2y ) {}
function Firework = function(){}
Firework.prototype.draw = function () {}
Firework.prototype.update= function (index) {}
function Particle( x, y ) {}
Particle.prototype.draw = function() {}
Particle.prototype.update = function( index ) {}
canvas.addListener( 'mousedown', function ( e ) {});
canvas.addListener( 'mouseup', function ( e ) {});
!frameUpdate(){}();
}
$.fn.firework = function (config) {
var canvas = document.getElementById('firework-canvas');
if ('undefined' == canvas) {
$('body').append( '<canvas id=\"firework-canvas\" />' );
canvas = document.getElementById('firework-canvas');
}
return this.each( function () {
if($(this).data('firework')) return;
$(this).data('firework', new firework(canvas, config));
} );
}
四、说明
目前,只是简单地实现了烟花的燃放效果,还需要加入一些细节处理使得烟花的燃放更加逼真,下一篇文章讲解对细节的处理