1. 程式人生 > >Canvas實現煙花效果

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( ty-sy, tx-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.cos( this.angle ) * this.speed;
  this.y += Math.sin( 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,15);// 隨機生成爆炸後產生的粒子數量
    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++;
  }
}
滑鼠點選產生煙花

在最開始的時候我們說過,當滑鼠點選時,以滑鼠點選的位置作為煙花的終點,產生一個煙花。因此我們需要在滑鼠單擊時,記錄下單擊位置mouseXmouseY,此外還要記錄下滑鼠的按下狀態mouseDown

var mouseX,
    mouseY,
    mouseDown = false;

canvas.addListener( 'mousedown',  function ( e ) {
  e.preventDefault();
  mouseX = e.pageX - canvas.offsetLeft;
  mouseY = e.pageY - canvas.offsetTop;
  mousedown = true;
});

canvas.addListener( 'mouseup',  function ( e ) {
  e.preventDefault();
  mousedown = false;
});

因為幀更新的速度很快,而滑鼠按下彈起的速度較慢,為了限制避免在每幀更新時產生很多煙花,因此設定了一個limiterTick,只有超過limiterTotal值並且滑鼠按下時,才會產生煙花。

var limiterTick = 0,
    limiterTotal = 8;

function 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));
  } );
}