利用HTML canvas製做酷炫星星墜地粒子特效

去年在電影院看過的電影,印象最深入的,算是電影《你的名字》了,並且被其中的畫面深深吸引了,尤爲是隕石劃過天空的場景,太美啦!因此想着哪天作一個canvas的流星效果。最近恰好看到油管上的一個視頻,做者的主頁就是隕石墜落的粒子效果爲背景,雖然沒有《你的名字》中那麼寫實,但也是很漂亮了,效果大概長這樣,附上連接https://codepen.io/christopher4lis/pen/PzONKRcanvas

在這個基礎上,我作了一些修改,將圓形粒子換成五角星,背景星空無限右移,且隨機產生流星。數組

下面我把製做過程詳細講解一下。app

初始設置

生成canvas標籤,設置canvas長寬dom

const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

由於要將canvas設置成背景,因此要絕對定位canvas函數

const canvas = document.createElement('canvas');
canvas.style.position = 'absolute';
canvas.style.top = 0;
canvas.style.left = 0;
canvas.style.zIndex = -1;
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const c = canvas.getContext('2d');

先畫個漸變色做爲背景動畫

//createLinearGradient的4個參數:startX,startY,EndX,EndY
const backgroundGradient = c.createLinearGradient(0, 0, 0, canvas.height); 
backgroundGradient.addColorStop(0, 'rgba(23, 30, 38, 0.7)');
backgroundGradient.addColorStop(1, 'rgba(63, 88, 107, 0.7)');
c.fillStyle = backgroundGradient;
c.fillRect(0, 0, canvas.width, canvas.height);

看下效果this

接下去畫幾座山,用三角形來模擬spa

畫山要有幾個設置參數:1.位置 2.顏色 3.畫幾個prototype

從簡單開始,咱們將山的座標點設在三角形的最左邊那個角,先畫三角形的底邊,而後畫上頂角3d

c.beginPath();
c.moveTo(0, canvas.height / 2);
c.lineTo(300, canvas.height / 2);
c.lineTo(150, canvas.height / 4);
c.closePath();
c.fill();

接下去,咱們想畫連起來的山

顯然這山畫得...咱們要想有那種峯巒疊嶂的感受,那就必須把山腳和山腳之間重疊起來,像這樣

That's it! 下面咱們來分析一下如何用一個通用的方法來畫出這樣的峯巒疊嶂

首先,定義一個函數drawMountains(),要傳入的參數:一、山的數量number,二、山的座標y(座標x直接根據山的數量計算出來,三、山的高度height,四、山的顏色color),五、重疊偏移量offset,好了就這些,讓咱們來實現它,go

這裏來講一下x座標如何計算,咱們假設繪製出的山橫跨整個canvas,那麼每座山的寬度就是canvas.width / number了,若是都按這個x座標來繪製,結果就是畫出沒重疊的山。那麼如何解決重疊問題呢?咱們能夠在繪製底邊時給起點和終點加一個偏移量:在起點加一個負的偏移量,在終點加一個相同大小的正的偏移量

function drawMountains(number, y, height, color, offset) {
  c.save();
  c.fillStyle = color;
  const width = canvas.width / number;
  // 循環繪製
  for (let i = 0; i < number; i++) {
    c.beginPath();
    c.moveTo(width * i - offset, y);
    c.lineTo(width * i + width + offset, y);
    c.lineTo(width * i + width / 2, y - height);
    c.closePath();
    c.fill();
  }
  c.restore();
}
drawMountains(3, canvas.height / 2, 200, '#384551', 100);

看下畫成什麼樣了

Bingo!成功了。接着畫剩下的山

drawMountains(1, canvas.height, canvas.height * 0.78, '#384551', 300);
drawMountains(2, canvas.height, canvas.height * 0.64, '#2B3843', 400);
drawMountains(3, canvas.height, canvas.height * 0.42, '#26333E', 150);

繪製背景星空

咱們先從簡單的開始,就只是在背景上繪製星空。這裏的思路是:定義一個星空星星的類Skystar,而後經過循環建立出一堆的skystar,這些skystar實例擁有各自獨立的位置和半徑大小,最後將這些skystar一個個畫到canvas上。Let's code!

定義Skystar及一些實例方法

function Skystar() {
  this.x = Math.random() * canvas.width;
  this.y = Math.random() * canvas.height;
  this.color = '#ccc';
  this.shadowColor = '#E3EAEF';
  this.radius = Math.random() * 3;
}

Skystar.prototype.draw = function() {
  c.save();
  c.beginPath();
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
  c.shadowColor = this.shadowColor;
  c.shadowBlur = Math.random() * 10 + 10;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = this.color;
  c.fill();
  c.closePath();
  c.restore();
};

Skystar.prototype.update = function() {
  this.draw();
};

接着定義一個全局數組skystarsArray,經過循環將多個skystar實例存入數組

const skyStarsArray = []; // 星空星星數組
const skyStarsCount = 400; // 星空初始生成星星數量
function drawSkyStars() {
  for (let i = 0; i < skyStarsCount; i++) {
      skyStarsArray.push(new Skystar());
  }
}

drawSkyStars();
skyStarsArray.forEach(skyStar => skyStar.update());

這樣就畫好靜止不動的背景星星。

接着咱們來思考下如何讓星星動起來。這裏定義一個animation函數,每一幀的繪製都在這個函數中進行,最後用requestAnimationFrame來重複調用animation函數,達到動畫的效果。

背景星星要動起來,就要在每一幀根據星星的位置對星星進行從新繪製。每一個星星的實例中,都定義了一個update方法,用來對星星的一些屬性進行更新,因此咱們如今修改update方法

const skyStarsVelocity = 0.1; // 星空平移速度
Skystar.prototype.update = function() {
  this.draw();
  // 星空一直接二連三向右移
  this.x += skyStarsVelocity;
};

而後在animation中將以前畫山和畫星空的方法加進去

function animation() {
  requestAnimationFrame(animation);
  // 畫背景
  c.fillStyle = backgroundGradient;
  c.fillRect(0, 0, canvas.width, canvas.height);
  // 畫星星
  skyStarsArray.forEach(skyStar => skyStar.update());
  // 畫山
  drawMountains(1, canvas.height, canvas.height * 0.78, '#384551', 300);
  drawMountains(2, canvas.height, canvas.height * 0.64, '#2B3843', 400);
  drawMountains(3, canvas.height, canvas.height * 0.42, '#26333E', 150);
}

在此以前,還有一些初始化的工做,這裏定義一個init函數,專門用來初始化一些數據

function init() {
  drawSkyStars();    // 初始化背景星星
}

完整代碼:

const canvas = document.createElement('canvas');
canvas.style.position = 'absolute';
canvas.style.top = 0;
canvas.style.left = 0;
canvas.style.zIndex = -1;
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const c = canvas.getContext('2d');
const skyStarsArray = []; // 星空星星數組
const skyStarsCount = 400; // 星空初始生成星星數量
const skyStarsVelocity = 0.1; // 星空平移速度
const backgroundGradient = c.createLinearGradient(0, 0, 0, canvas.height);    //4個參數:startX,startY,EndX,EndY
backgroundGradient.addColorStop(0, 'rgba(23, 30, 38, 0.7)');
backgroundGradient.addColorStop(1, 'rgba(63, 88, 107, 0.7)');

function init() {
  drawSkyStars();    // 初始化背景星星
}

// 畫山
function drawMountains(number, y, height, color, offset) {
  c.save();
  c.fillStyle = color;
  const width = canvas.width / number;
  // 循環繪製
  for (let i = 0; i < number; i++) {
    c.beginPath();
    c.moveTo(width * i - offset, y);
    c.lineTo(width * i + width + offset, y);
    c.lineTo(width * i + width / 2, y - height);
    c.closePath();
    c.fill();
  }
  c.restore();
}

function Skystar() {
  this.x = Math.random() * canvas.width;
  this.y = Math.random() * canvas.height;
  this.color = '#ccc';
  this.shadowColor = '#E3EAEF';
  this.radius = Math.random() * 3;
}

Skystar.prototype.draw = function() {
  c.save();
  c.beginPath();
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
  c.shadowColor = this.shadowColor;
  c.shadowBlur = Math.random() * 10 + 10;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = this.color;
  c.fill();
  c.closePath();
  c.restore();
};

Skystar.prototype.update = function() {
  this.draw();
  // 星空一直接二連三向右移
  this.x += skyStarsVelocity;
};

function drawSkyStars() {
  for (let i = 0; i < skyStarsCount; i++) {
    skyStarsArray.push(new Skystar());
  }
}

function animation() {
  requestAnimationFrame(animation);
  // 畫背景
  c.fillStyle = backgroundGradient;
  c.fillRect(0, 0, canvas.width, canvas.height);
  // 畫星星
  skyStarsArray.forEach(skyStar => skyStar.update());
  // 畫山
  drawMountains(1, canvas.height, canvas.height * 0.78, '#384551', 300);
  drawMountains(2, canvas.height, canvas.height * 0.64, '#2B3843', 400);
  drawMountains(3, canvas.height, canvas.height * 0.42, '#26333E', 150);
}

init();
animation();

嗯,效果有了,但是...

一段時間後,左邊沒有星星了。。。緣由是,星星都移出canvas了,因此作個修正,擴大星星的繪製寬度爲兩倍的canvas寬度,在超出canvas的右側區域也繪製星星,而後讓星星向左移,每當星星的x座標超出canvas寬度,立刻在skystarArray數組中將其移除,同時新建立一個skystar存入數組中,這個新生成的星星必須位於canvas左側看不見的區域,這樣就會有無限個星星一直向右移的效果了。

修改Skystar構造函數:

function Skystar(x) {
  this.x = x || (Math.random() - 0.5) * 2 * canvas.width;
  this.y = Math.random() * canvas.height;
  this.color = '#ccc';
  this.shadowColor = '#E3EAEF';
  this.radius = Math.random() * 3;
}

修改animation函數:

function animation() {
  ...
  // 畫星星
  skyStarsArray.forEach((skyStar, index) => {
      // 若是超出canvas,則去除這顆星星,在canvas左側從新生成一顆
      if (skyStar.x - skyStar.radius - 20 > canvas.width ) {
        skyStarsArray.splice(index, 1);
        skyStarsArray.push(new Skystar(-Math.random() * canvas.width));
        return;
    }
    skyStar.update()
  });
  ...
}

目前背景星星是平行地向右移動,可是現實中,因爲地球自轉,才讓咱們看到星星在移動,因此現實中的星星的移動應該是一個繞着地球移動的效果。所以,咱們加一個更真實的效果:星星從左向右移動期間,它的軌跡先是慢慢遠離地面,而後再慢慢接近地面。

修改Skystar的update方法:

Skystar.prototype.update = function() {
  this.draw();
  // 星空一直接二連三向右移
  this.x += skyStarsVelocity;
  // y方向上有一個從上到下的偏移量,這裏用cos函數來表示,模擬地球自轉時看到的星空
  let angle = Math.PI / (canvas.width / skyStarsVelocity) * (this.x / skyStarsVelocity);
  this.y += this.x > 0 ? -Math.cos(angle) * 0.03 : 0;
};

繪製墜落的星星

接下來,咱們繪製掉到地板上的星星,首先,咱們畫一個地板,修改animation函數

function animation() {
  ...
  // 畫山
  drawMountains(1, canvas.height, canvas.height * 0.78, '#384551', 300);
  drawMountains(2, canvas.height, canvas.height * 0.64, '#2B3843', 400);
  drawMountains(3, canvas.height, canvas.height * 0.42, '#26333E', 150);
  // 畫地面
  c.fillStyle = '#182028';
  c.fillRect(0, canvas.height * 0.85, canvas.width, canvas.height * 0.15);
}

接着來定義墜落的星星,方法跟畫背景星星同樣,先定義一個構造函數Star,而後添加構造函數的方法,最後用一個數組來存放Star的實例。一步一步來

構造函數Star

function Star() {
  this.radius = Math.random() * 10 + 5;
  this.x = Math.random() * (canvas.width - this.radius * 2) + this.radius;
  this.y = -Math.random() * canvas.height;
  this.velocity = {
    x: (Math.random() - 0.5) * 20,
    y: 5,
    rotate: 5
  };
  this.rotate = Math.sign(this.velocity.x) * Math.random() * Math.PI * 2;
  this.friction = 0.7;
  this.gravity = 0.5;
  this.opacity = 1;
  this.shadowColor = '#E3EAEF';
  this.shadowBlur = 20;
  this.timeToLive = 200;
  this.die = false;
}

解釋一下這些屬性:x,y表示墜星初始的位置,咱們把這些星星定義在canvas的上方,星星會從天而降;velocity表示星星各個方向的初始速度,包括星星自轉的初始速度;rotate就是星星的角度;gravity表示重力做用,起到一個模擬重力加速的物理效果;timeToLive表示當星星被標識爲die時(也就是當星星通過碰撞損失後,半徑已經不能再小的時候),通過多長時間才消失

Star繪製五角星的方法draw:

Star.prototype.draw = function() {
  c.save();
  c.beginPath();
  // 畫五角星
  for (let i = 0; i < 5; i++) {
    c.lineTo(Math.cos((18 + i * 72 - this.rotate) / 180 * Math.PI) * this.radius + this.x, -Math.sin((18 + i * 72 - this.rotate) / 180 * Math.PI) * this.radius + this.y );
      c.lineTo(Math.cos((54 + i * 72 - this.rotate) / 180 * Math.PI) * this.radius * 0.5 + this.x, -Math.sin((54 + i * 72 - this.rotate) / 180 * Math.PI) * this.radius * 0.5 + this.y);
  }
  c.shadowColor = this.shadowColor;
  c.shadowBlur = this.shadowBlur;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = 'rgba(255,255,255,' + this.opacity + ')';
  c.fill();
  c.closePath();
  c.restore();
};

五角星的畫法,參看下圖

Star的update方法:

Star.prototype.update = function() {
  this.draw();
  // 碰到兩邊牆壁
  if (
    this.x + this.radius + this.velocity.x > canvas.width ||
    this.x - this.radius + this.velocity.x < 0
  ) {
    this.velocity.x *= -this.friction;    // 碰到兩邊牆壁,橫向速度損失,同時方向反轉
    this.velocity.rotate *= -this.friction;    // 旋轉速度也損失,同時方向反轉
  }
  // 碰到地面
  if (this.y + this.radius + this.velocity.y > canvas.height) {
    this.velocity.y *= -this.friction;    // 每次碰撞,速度都損失,方向反轉
    this.velocity.rotate *= (Math.random() - 0.5) * 20;    // 每次碰到地面旋轉速度都隨機
    this.radius -= 3;
    // 修正若是半徑小等於1,直接定爲1
    if (this.radius <= 1) {
      this.radius = 1;      
    }
  } else {
    this.velocity.y += this.gravity;    // 沒碰到地面,速度增長
  }

  this.x += this.velocity.x;
  this.y += this.velocity.y;
  this.rotate += this.velocity.rotate;
  
  // 進入消失倒計時
  if (this.radius - 1 <= 0 && !this.die) {
    this.timeToLive--;
    this.opacity -= 1 / Math.max(1, this.timeToLive);    // 不透明從慢到快
    if (this.timeToLive < 0) {
      this.die = true;
    }
  }
};

每次碰撞到地面,星星的半徑都會少,當半徑小到小於1時,咱們就認爲這顆星星能夠進入消失倒計時了,同時也將慢慢變透明

如今,定義一個初始化函數drawStars,將墜落的星星存在starsArray中

// 先畫5個星星
function drawStars() {
  for (let i = 0; i < 5; i++) {
    starsArray.push(new Star());
  }
}

修改init函數

function init() {
  drawSkyStars(); // 初始化背景星星
  drawStars(); // 初始化墜落的星星
}

修改animation函數

function animation() {
  ...
  // 畫墜落的星星
  starsArray.forEach((star, index) => {
    if (star.die) {
      starsArray.splice(index, 1);
      return;
    }
    star.update();
  });
}

同時還要記得加一個全局的數組starsArray

const starsArray = []; // 墜落星星數組

完整代碼

const canvas = document.createElement('canvas');
canvas.style.position = 'absolute';
canvas.style.top = 0;
canvas.style.left = 0;
canvas.style.zIndex = -1;
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const c = canvas.getContext('2d');
const skyStarsArray = []; // 星空星星數組
const starsArray = []; // 墜落星星數組
const skyStarsCount = 400; // 星空初始生成星星數量
const skyStarsVelocity = 0.1; // 星空平移速度
const backgroundGradient = c.createLinearGradient(0, 0, 0, canvas.height);    //4個參數:startX,startY,EndX,EndY
backgroundGradient.addColorStop(0, 'rgba(23, 30, 38, 0.7)');
backgroundGradient.addColorStop(1, 'rgba(63, 88, 107, 0.7)');

function init() { 
  drawSkyStars(); // 初始化背景星星
  drawStars(); // 初始化墜落的星星
}

// 畫山
function drawMountains(number, y, height, color, offset) {
  c.save();
  c.fillStyle = color;
  const width = canvas.width / number;
  // 循環繪製
  for (let i = 0; i < number; i++) {
    c.beginPath();
    c.moveTo(width * i - offset, y);
    c.lineTo(width * i + width + offset, y);
    c.lineTo(width * i + width / 2, y - height);
    c.closePath();
    c.fill();
  }
  c.restore();
}

function Skystar(x) {
  this.x = x || (Math.random() - 0.5) * 2 * canvas.width;
  this.y = Math.random() * canvas.height;
  this.color = '#ccc';
  this.shadowColor = '#E3EAEF';
  this.radius = Math.random() * 3;
}

Skystar.prototype.draw = function() {
  c.save();
  c.beginPath();
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
  c.shadowColor = this.shadowColor;
  c.shadowBlur = Math.random() * 10 + 10;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = this.color;
  c.fill();
  c.closePath();
  c.restore();
};

Skystar.prototype.update = function() {
  this.draw();
  // 星空一直接二連三向右移
  this.x += skyStarsVelocity;
  // y方向上有一個從上到下的偏移量,這裏用cos函數來表示,模擬地球自轉時看到的星空
  let angle = Math.PI / (canvas.width / skyStarsVelocity) * (this.x / skyStarsVelocity);
  this.y += this.x > 0 ? -Math.cos(angle) * 0.03 : 0;
};

function drawSkyStars() {
  for (let i = 0; i < skyStarsCount; i++) {
    skyStarsArray.push(new Skystar());
  }
}

function Star() {
  this.radius = Math.random() * 10 + 5;
  this.x = Math.random() * (canvas.width - this.radius * 2) + this.radius;
  this.y = -Math.random() * canvas.height;
  this.velocity = {
    x: (Math.random() - 0.5) * 20,
    y: 5,
    rotate: 5
  };
  this.rotate = Math.sign(this.velocity.x) * Math.random() * Math.PI * 2;
  this.friction = 0.7;
  this.gravity = 0.5;
  this.opacity = 1;
  this.shadowColor = '#E3EAEF';
  this.shadowBlur = 20;
  this.timeToLive = 200;
  this.die = false;
}

Star.prototype.draw = function() {
  c.save();
  c.beginPath();
  // 畫五角星
  for (let i = 0; i < 5; i++) {
    c.lineTo(
      Math.cos((18 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius +
      this.x,
      -Math.sin((18 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius +
      this.y
    );
    c.lineTo(
      Math.cos((54 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius *
      0.5 +
      this.x,
      -Math.sin((54 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius *
      0.5 +
      this.y
    );
  }
  c.shadowColor = this.shadowColor;
  c.shadowBlur = this.shadowBlur;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = 'rgba(255,255,255,' + this.opacity + ')';
  c.fill();
  c.closePath();
  c.restore();
};

Star.prototype.update = function() {
  this.draw();
  // 碰到兩邊牆壁
  if (
    this.x + this.radius + this.velocity.x > canvas.width ||
    this.x - this.radius + this.velocity.x < 0
  ) {
    this.velocity.x *= -this.friction; // 碰到兩邊牆壁,橫向速度損失,同時方向反轉
    this.velocity.rotate *= -this.friction; // 旋轉速度也損失,同時方向反轉反
  }
  // 碰到地面
  if (this.y + this.radius + this.velocity.y > canvas.height) {
    this.velocity.y *= -this.friction; // 每次碰撞,速度都損失,方向反轉
    this.velocity.rotate *= (Math.random() - 0.5) * 20; // 每次碰到地面旋轉速度都隨機
    this.radius -= 3;
    // 修正若是半徑小等於1,直接定爲1
    if (this.radius <= 1) {
      this.radius = 1;
    }
  } else {
    this.velocity.y += this.gravity; // 沒碰到地面,速度增長
  }

  this.x += this.velocity.x;
  this.y += this.velocity.y;
  this.rotate += this.velocity.rotate;

  // 進入消失倒計時
  if (this.radius - 1 <= 0 && !this.die) {
    this.timeToLive--;
    this.opacity -= 1 / Math.max(1, this.timeToLive); // 不透明從慢到快
    if (this.timeToLive < 0) {
      this.die = true;
    }
  }
};

// 先畫5個星星
function drawStars() {
  for (let i = 0; i < 5; i++) {
    starsArray.push(new Star());
  }
}

function animation() {
  requestAnimationFrame(animation);
  // 畫背景
  c.fillStyle = backgroundGradient;
  c.fillRect(0, 0, canvas.width, canvas.height);
  // 畫星星
  skyStarsArray.forEach((skyStar, index) => {
    // 若是超出canvas,則去除這顆星星,在canvas左側從新生成一顆
    if (skyStar.x - skyStar.radius - 20 > canvas.width) {
      skyStarsArray.splice(index, 1);
      skyStarsArray.push(new Skystar(-Math.random() * canvas.width));
      return;
    }
    skyStar.update();
  });
  // 畫山
  drawMountains(1, canvas.height, canvas.height * 0.78, '#384551', 300);
  drawMountains(2, canvas.height, canvas.height * 0.64, '#2B3843', 400);
  drawMountains(3, canvas.height, canvas.height * 0.42, '#26333E', 150);
  // 畫地面
  c.fillStyle = '#182028';
  c.fillRect(0, canvas.height * 0.85, canvas.width, canvas.height * 0.15);
  // 畫墜落的星星
  starsArray.forEach((star, index) => {
    if (star.die) {
      starsArray.splice(index, 1);
      return;
    }
    star.update();
  });
}

init();
animation();

繪製碰撞粒子

接下來分析一下如何產生碰撞後粒子飛出去的效果。首先,觸發碰撞的條件是在星星墜落碰到地面的那個時刻,此時,生成了一個爆炸點。因爲單個星星能產生多個爆炸點,所以咱們用一個數組explosionsArray來保存爆炸點。這裏沿用以前產生墜星的方法,咱們定義爆炸點的構造函數及其相關的方法,而後在碰撞的時候,建立一個爆炸點實例。

先定義一個全局的變量

const explosionsArray = []; // 爆炸點數組

爆炸點的構造函數Explosion

function Explosion(star) {
  // ...
}

由於爆炸點的位置和墜星的位置有密切關係,咱們在構造爆炸點的實例時傳入一個star實例,從而獲取到相關的信息。

爆炸的時候,會飛出小顆粒,也就是生成了新的物體,因此這裏還要定義粒子的構造函數。並且在建立爆炸點的那個時候,就要生成幾個粒子實例。

粒子的構造函數Particle

function Particle() {
  // ...
}

如今來捋一捋過程:墜星碰到地面的那一時刻 -> 生成爆炸點實例 -> 生成幾個粒子實例 -> 循環繪製粒子,更新粒子位置

墜星碰到地面的那一時刻 -> 生成爆炸點實例

墜星碰到地面的那一時刻是在Star的update方法中判斷,故這裏要修改update方法

Star.prototype.update = function() {
  ...
  // 碰到地面
  if (this.y + this.radius + this.velocity.y > canvas.height) {
    // 若是沒到最小半徑,則產生爆炸效果
    if (this.radius > 1) {
      explosionsArray.push(new Explosion(this));
    }
    this.velocity.y *= -this.friction; // 每次碰撞,速度都損失,方向反轉
    this.velocity.rotate *= (Math.random() - 0.5) * 20; // 每次碰到地面旋轉速度都隨機
    ...
};
生成爆炸點實例 -> 生成幾個粒子實例

這裏就要對爆炸點的構造函數進行完善和補充。由於在生成爆炸點的同時,也要同時生成爆炸粒子,因此這裏定義一個init方法,在此方法中生成爆炸粒子實例,而後這個init方法在爆炸點實例化時當即執行。

function Explosion(star) {
  this.init(star);
}

假設在碰撞的瞬間生成隨機個粒子,所以須要在各個爆炸點設置一個存放爆炸粒子的數組

function Explosion(star) {
  this.particles=[];    // 用來存放爆炸粒子
  this.init(star);
}

init函數

Explosion.prototype.init = function(star) {
  for (let i = 0; i < 4 + Math.random() * 10; i++) {
    const dx = (Math.random() - 0.5) * 8;    // 隨機生成的x方向速度
    const dy = (Math.random() - 0.5) * 20;    // 隨機生成的y方向速度
    this.particles.push(new Particle(star.x, star.y, dx, dy));    // 把座標和速度傳給Particle構造函數
  }
};
循環繪製粒子,更新粒子位置

除了在碰撞時要生成粒子以外,在每次animation函數執行時,也要更新每一個粒子的位置,所以,還要給Explosion定義一個update函數來更新粒子狀態

Explosion.prototype.update = function() {
  this.particles.forEach(particle => {
    particle.update();
  });
};

接着來完善Particle構造函數,跟前面墜星的構造函數相似,不一樣的是粒子用一個正方形表示

function Particle(x, y, dx, dy) {
  this.x = x;
  this.y = y;
  this.dx = dx;
  this.dy = dy;
  this.size = {
    width: 2,
    height: 2
  };
  this.friction = 0.7;
  this.gravity = 0.5;
  this.opacity = 1;
  this.timeToLive = 200;
  this.shadowColor = '#E3EAEF';
}

Particle的draw方法

Particle.prototype.draw = function() {
  c.save();
  c.fillStyle = 'rgba(227, 234, 239,' + this.opacity + ')';
  c.shadowColor = this.shadowColor;
  c.shadowBlur = 20;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillRect(this.x, this.y, this.size.width, this.size.height);
  c.restore();
};

Particle的update函數,與墜星不一樣的是,碰撞粒子一產生就開始消失倒計時

Particle.prototype.update = function() {
  this.draw();
  // 碰到兩邊牆壁
  if (
    this.x + this.size.width + this.dx > canvas.width ||
    this.x + this.dx < 0
  ) {
    this.dx *= -this.friction;
  }
  // 碰到地面
  if (this.y + this.size.height + this.dy > canvas.height) {
    this.dy *= -this.friction;
  } else {
    this.dy += this.gravity;
  }
  this.x += this.dx;
  this.y += this.dy;

  this.timeToLive--;
  this.opacity -= 1 / this.timeToLive; //不透明度ease-in效果
};

倒計時結束後,要及時把這個爆炸粒子從particles數組中移除。修改Explosion的update方法

Explosion.prototype.update = function() {
  this.particles.forEach((particle, index, particles) => {
    if (particle.timeToLive <= 0) {    // 生命週期結束
      particles.splice(index, 1);
      return;
    }
    particle.update();
  });
};

同理,若是當前爆炸點中全部的粒子都已經超過生命週期,那麼就要從explosionsArray中移除。修改animation函數

function animation() {
  ...
  // 畫墜落的星星
  starsArray.forEach((star, index) => {
    if (star.die) {
      starsArray.splice(index, 1);
      return;
    }
    star.update();
  });
  // 循環更新爆炸點
  explosionsArray.forEach((explosion, index) => {
    if (explosion.particles.length === 0) {
      explosionsArray.splice(index, 1);
      return;
    }
    explosion.update();
  });
}

完整代碼

const canvas = document.createElement('canvas');
canvas.style.position = 'absolute';
canvas.style.top = 0;
canvas.style.left = 0;
canvas.style.zIndex = -1;
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const c = canvas.getContext('2d');
const skyStarsArray = []; // 星空星星數組
const starsArray = []; // 墜落星星數組
const explosionsArray = []; // 爆炸粒子數組
const skyStarsCount = 400; // 星空初始生成星星數量
const skyStarsVelocity = 0.1; // 星空平移速度
const backgroundGradient = c.createLinearGradient(0, 0, 0, canvas.height); //4個參數:startX,startY,EndX,EndY
backgroundGradient.addColorStop(0, 'rgba(23, 30, 38, 0.7)');
backgroundGradient.addColorStop(1, 'rgba(63, 88, 107, 0.7)');

function init() {
  drawSkyStars(); // 初始化背景星星
  drawStars(); // 初始化墜落的星星
}

// 畫山
function drawMountains(number, y, height, color, offset) {
  c.save();
  c.fillStyle = color;
  const width = canvas.width / number;
  // 循環繪製
  for (let i = 0; i < number; i++) {
    c.beginPath();
    c.moveTo(width * i - offset, y);
    c.lineTo(width * i + width + offset, y);
    c.lineTo(width * i + width / 2, y - height);
    c.closePath();
    c.fill();
  }
  c.restore();
}

function Skystar(x) {
  this.x = x || (Math.random() - 0.5) * 2 * canvas.width;
  this.y = Math.random() * canvas.height;
  this.color = '#ccc';
  this.shadowColor = '#E3EAEF';
  this.radius = Math.random() * 3;
}

Skystar.prototype.draw = function() {
  c.save();
  c.beginPath();
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
  c.shadowColor = this.shadowColor;
  c.shadowBlur = Math.random() * 10 + 10;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = this.color;
  c.fill();
  c.closePath();
  c.restore();
};

Skystar.prototype.update = function() {
  this.draw();
  // 星空一直接二連三向右移
  this.x += skyStarsVelocity;
  // y方向上有一個從上到下的偏移量,這裏用cos函數來表示,模擬地球自轉時看到的星空
  let angle = Math.PI / (canvas.width / skyStarsVelocity) * (this.x / skyStarsVelocity);
  this.y += this.x > 0 ? -Math.cos(angle) * 0.03 : 0;
};

function drawSkyStars() {
  for (let i = 0; i < skyStarsCount; i++) {
    skyStarsArray.push(new Skystar());
  }
}

function Star() {
  this.radius = Math.random() * 10 + 5;
  this.x = Math.random() * (canvas.width - this.radius * 2) + this.radius;
  this.y = -Math.random() * canvas.height;
  this.velocity = {
    x: (Math.random() - 0.5) * 20,
    y: 5,
    rotate: 5
  };
  this.rotate = Math.sign(this.velocity.x) * Math.random() * Math.PI * 2;
  this.friction = 0.7;
  this.gravity = 0.5;
  this.opacity = 1;
  this.shadowColor = '#E3EAEF';
  this.shadowBlur = 20;
  this.timeToLive = 200;
  this.die = false;
}

Star.prototype.draw = function() {
  c.save();
  c.beginPath();
  // 畫五角星
  for (let i = 0; i < 5; i++) {
    c.lineTo(
      Math.cos((18 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius +
      this.x,
      -Math.sin((18 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius +
      this.y
    );
    c.lineTo(
      Math.cos((54 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius *
      0.5 +
      this.x,
      -Math.sin((54 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius *
      0.5 +
      this.y
    );
  }
  c.shadowColor = this.shadowColor;
  c.shadowBlur = this.shadowBlur;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = 'rgba(255,255,255,' + this.opacity + ')';
  c.fill();
  c.closePath();
  c.restore();
};

Star.prototype.update = function() {
  this.draw();
  // 碰到兩邊牆壁
  if (
    this.x + this.radius + this.velocity.x > canvas.width ||
    this.x - this.radius + this.velocity.x < 0
  ) {
    this.velocity.x *= -this.friction; // 碰到兩邊牆壁,橫向速度損失,同時方向反轉
    this.velocity.rotate *= -this.friction; // 旋轉速度也損失,同時方向反轉
  }
  // 碰到地面
  if (this.y + this.radius + this.velocity.y > canvas.height) {
    // 若是沒到最小半徑,則產生爆炸效果
    if (this.radius > 1) {
      explosionsArray.push(new Explosion(this));
    }
    this.velocity.y *= -this.friction; // 每次碰撞,速度都損失,同時方向反轉
    this.velocity.rotate *= (Math.random() - 0.5) * 20; // 每次碰到地面旋轉速度都隨機
    this.radius -= 3;
    // 修正若是半徑小等於1,直接定爲1
    if (this.radius <= 1) {
      this.radius = 1;
    }
  } else {
    this.velocity.y += this.gravity; // 沒碰到地面,速度增長
  }

  this.x += this.velocity.x;
  this.y += this.velocity.y;
  this.rotate += this.velocity.rotate;

  // 進入消失倒計時
  if (this.radius - 1 <= 0 && !this.die) {
    this.timeToLive--;
    this.opacity -= 1 / Math.max(1, this.timeToLive); // 不透明從慢到快
    if (this.timeToLive < 0) {
      this.die = true;
    }
  }
};

// 先畫5個星星
function drawStars() {
  for (let i = 0; i < 5; i++) {
    starsArray.push(new Star());
  }
}

function Explosion(star) {
  this.particles = []; // 用來存放爆炸粒子
  this.init(star);
}

Explosion.prototype.init = function(star) {
  for (let i = 0; i < 4 + Math.random() * 10; i++) {
    const dx = (Math.random() - 0.5) * 8; // 隨機生成的x方向速度
    const dy = (Math.random() - 0.5) * 20; // 隨機生成的y方向速度
    this.particles.push(new Particle(star.x, star.y, dx, dy)); // 把座標和速度傳給Particle構造函數
  }
};

Explosion.prototype.update = function() {
  this.particles.forEach((particle, index, particles) => {
    if (particle.timeToLive <= 0) {
      // 生命週期結束
      particles.splice(index, 1);
      return;
    }
    particle.update();
  });
};

function Particle(x, y, dx, dy) {
  this.x = x;
  this.y = y;
  this.dx = dx;
  this.dy = dy;
  this.size = {
    width: 2,
    height: 2
  };
  this.friction = 0.7;
  this.gravity = 0.5;
  this.opacity = 1;
  this.timeToLive = 200;
  this.shadowColor = '#E3EAEF';
}

Particle.prototype.draw = function() {
  c.save();
  c.fillStyle = 'rgba(227, 234, 239,' + this.opacity + ')';
  c.shadowColor = this.shadowColor;
  c.shadowBlur = 20;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillRect(this.x, this.y, this.size.width, this.size.height);
  c.restore();
};

Particle.prototype.update = function() {
  this.draw();
  // 碰到兩邊牆壁
  if (
    this.x + this.size.width + this.dx > canvas.width ||
    this.x + this.dx < 0
  ) {
    this.dx *= -this.friction;
  }
  // 碰到地面
  if (this.y + this.size.height + this.dy > canvas.height) {
    this.dy *= -this.friction;
  } else {
    this.dy += this.gravity;
  }
  this.x += this.dx;
  this.y += this.dy;

  this.timeToLive--;
  this.opacity -= 1 / this.timeToLive; //不透明度ease-in效果
};

function animation() {
  requestAnimationFrame(animation);
  // 畫背景
  c.fillStyle = backgroundGradient;
  c.fillRect(0, 0, canvas.width, canvas.height);
  // 畫星星
  skyStarsArray.forEach((skyStar, index) => {
    // 若是超出canvas,則去除這顆星星,在canvas左側從新生成一顆
    if (skyStar.x - skyStar.radius - 20 > canvas.width) {
      skyStarsArray.splice(index, 1);
      skyStarsArray.push(new Skystar(-Math.random() * canvas.width));
      return;
    }
    skyStar.update();
  });
  // 畫山
  drawMountains(1, canvas.height, canvas.height * 0.78, '#384551', 300);
  drawMountains(2, canvas.height, canvas.height * 0.64, '#2B3843', 400);
  drawMountains(3, canvas.height, canvas.height * 0.42, '#26333E', 150);
  // 畫地面
  c.fillStyle = '#182028';
  c.fillRect(0, canvas.height * 0.85, canvas.width, canvas.height * 0.15);
  // 畫墜落的星星
  starsArray.forEach((star, index) => {
    if (star.die) {
      starsArray.splice(index, 1);
      return;
    }
    star.update();
  });
  // 循環更新爆炸點
  explosionsArray.forEach((explosion, index) => {
    if (explosion.particles.length === 0) {
      explosionsArray.splice(index, 1);
      return;
    }
    explosion.update();
  });
}

init();
animation();

隨機掉落和隨機產生背景流星

如今效果已經基本完成了,不過目前是一開始就掉5顆,咱們想隨機一段時間後掉落一顆,看看如何修改。

定義一個全局變量spawnTimer來存放隨機生成時間

let spawnTimer = Math.random() * 500; // 隨機生成墜落星星的時間

接着在animation循環時,對spawnTimer遞減,而後判斷spawnTimer的值,若是小於0,表示能夠生成新的墜星了。修改animation函數

function animation() {
  ...
  // 循環更新爆炸點
  explosionsArray.forEach((explosion, index) => {
    if (explosion.particles.length === 0) {
      explosionsArray.splice(index, 1);
      return;
    }
    explosion.update();
  });
  // 控制隨機生成墜星
  spawnTimer--;
  if (spawnTimer < 0) {
    spawnTimer = Math.random() * 500;
    starsArray.push(new Star());
  }
}

一開始改爲畫2顆星星

// 畫2個星星
function drawStars() {
  for (let i = 0; i < 2; i++) {
    starsArray.push(new Star());
  }
}

忽然感受背景星空只能移動彷佛有點單調,那就加上幾顆流星吧

修改Skystar構造函數,增長几個屬性,falling表示是否觸發流星劃破天際的效果

function Skystar(x) {
  ...
  // 流星屬性
  this.falling = false;
  this.dx = Math.random() * 4 + 4;
  this.dy = 2;
  this.timeToLive = 200;
}

以上定義了變成流星後的速度、加速度、生命週期等屬性

除了修改Skystar構造函數,在animation函數中也要加上一些流星的判斷

function animation() {
  requestAnimationFrame(animation);
  // 畫背景
  c.fillStyle = backgroundGradient;
  c.fillRect(0, 0, canvas.width, canvas.height);
  // 畫背景星星
  // 隨機將一個背景星星定義成流星,這裏利用墜星的隨機生成時間來隨機生成流星
  if (~~spawnTimer % 103 === 0) {
    skyStarsArray[
      ~~(Math.random() * skyStarsArray.length)
    ].falling = true;
  }
  skyStarsArray.forEach((skyStar, index) => {
    // 若是超出canvas或者做爲流星滑落結束,則去除這顆星星,在canvas左側從新生成一顆
    if (skyStar.x - skyStar.radius - 20 > canvas.width || skyStar.timeToLive < 0) {
      skyStarsArray.splice(index, 1);
      skyStarsArray.push(new Skystar(-Math.random() * canvas.width));
      return;
    }
    // 星空隨機產生流星
    if (skyStar.falling) {
      skyStar.x += skyStar.dx;
      skyStar.y += skyStar.dy;
      skyStar.color = '#fff';
      // 半徑慢慢變小
      if (skyStar.radius > 0.05) {
        skyStar.radius -= 0.05;
      } else {
        skyStar.radius = 0.05;
      }
      skyStar.timeToLive--;
    }
    skyStar.update();
  });
  ...
}

最後,咱們加上一個resize事件,當畫面大小改變時,從新繪製

window.addEventListener('resize',() => {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  skyStarsArray = [];
  starsArray = [];
  explosionsArray = [];
  spawnTimer = Math.random() * 500;
  init();
}, false);

最終代碼

const canvas = document.createElement('canvas');
canvas.style.position = 'absolute';
canvas.style.top = 0;
canvas.style.left = 0;
canvas.style.zIndex = -1;
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const c = canvas.getContext('2d');
const skyStarsArray = []; // 星空星星數組
const starsArray = []; // 墜落星星數組
const explosionsArray = []; // 爆炸粒子數組
const skyStarsCount = 400; // 星空初始生成星星數量
const skyStarsVelocity = 0.1; // 星空平移速度
const backgroundGradient = c.createLinearGradient(0, 0, 0, canvas.height); //4個參數:startX,startY,EndX,EndY
backgroundGradient.addColorStop(0, 'rgba(23, 30, 38, 0.7)');
backgroundGradient.addColorStop(1, 'rgba(63, 88, 107, 0.7)');
let spawnTimer = Math.random() * 500; // 隨機生成墜落星星的時間

window.addEventListener('resize',() => {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
  skyStarsArray = [];
  starsArray = [];
  explosionsArray = [];
  spawnTimer = Math.random() * 500;
  init();
}, false);

function init() {
  drawSkyStars(); // 初始化背景星星
  drawStars(); // 初始化墜落的星星
}

// 畫山
function drawMountains(number, y, height, color, offset) {
  c.save();
  c.fillStyle = color;
  const width = canvas.width / number;
  // 循環繪製
  for (let i = 0; i < number; i++) {
    c.beginPath();
    c.moveTo(width * i - offset, y);
    c.lineTo(width * i + width + offset, y);
    c.lineTo(width * i + width / 2, y - height);
    c.closePath();
    c.fill();
  }
  c.restore();
}

function Skystar(x) {
  this.x = x || (Math.random() - 0.5) * 2 * canvas.width;
  this.y = Math.random() * canvas.height;
  this.color = '#ccc';
  this.shadowColor = '#E3EAEF';
  this.radius = Math.random() * 3;
  // 流星屬性
  this.falling = false;
  this.dx = Math.random() * 4 + 4;
  this.dy = 2;
  this.timeToLive = 200;
}

Skystar.prototype.draw = function() {
  c.save();
  c.beginPath();
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
  c.shadowColor = this.shadowColor;
  c.shadowBlur = Math.random() * 10 + 10;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = this.color;
  c.fill();
  c.closePath();
  c.restore();
};

Skystar.prototype.update = function() {
  this.draw();
  // 星空一直接二連三向右移
  this.x += skyStarsVelocity;
  // y方向上有一個從上到下的偏移量,這裏用cos函數來表示,模擬地球自轉時看到的星空
  let angle =
      Math.PI /
      (canvas.width / skyStarsVelocity) *
      (this.x / skyStarsVelocity);
  this.y += this.x > 0 ? -Math.cos(angle) * 0.03 : 0;
};

function drawSkyStars() {
  for (let i = 0; i < skyStarsCount; i++) {
    skyStarsArray.push(new Skystar());
  }
}

function Star() {
  this.radius = Math.random() * 10 + 5;
  this.x = Math.random() * (canvas.width - this.radius * 2) + this.radius;
  this.y = -Math.random() * canvas.height;
  this.velocity = {
    x: (Math.random() - 0.5) * 20,
    y: 5,
    rotate: 5
  };
  this.rotate = Math.sign(this.velocity.x) * Math.random() * Math.PI * 2;
  this.friction = 0.7;
  this.gravity = 0.5;
  this.opacity = 1;
  this.shadowColor = '#E3EAEF';
  this.shadowBlur = 20;
  this.timeToLive = 200;
  this.die = false;
}

Star.prototype.draw = function() {
  c.save();
  c.beginPath();
  // 畫五角星
  for (let i = 0; i < 5; i++) {
    c.lineTo(
      Math.cos((18 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius +
      this.x,
      -Math.sin((18 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius +
      this.y
    );
    c.lineTo(
      Math.cos((54 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius *
      0.5 +
      this.x,
      -Math.sin((54 + i * 72 - this.rotate) / 180 * Math.PI) *
      this.radius *
      0.5 +
      this.y
    );
  }
  c.shadowColor = this.shadowColor;
  c.shadowBlur = this.shadowBlur;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillStyle = 'rgba(255,255,255,' + this.opacity + ')';
  c.fill();
  c.closePath();
  c.restore();
};

Star.prototype.update = function() {
  this.draw();
  // 碰到兩邊牆壁
  if (
    this.x + this.radius + this.velocity.x > canvas.width ||
    this.x - this.radius + this.velocity.x < 0
  ) {
    this.velocity.x *= -this.friction; // 碰到兩邊牆壁,橫向速度損失,同時方向反轉
    this.velocity.rotate *= -this.friction; // 旋轉速度也損失,同時方向反轉
  }
  // 碰到地面
  if (this.y + this.radius + this.velocity.y > canvas.height) {
    // 若是沒到最小半徑,則產生爆炸效果
    if (this.radius > 1) {
      explosionsArray.push(new Explosion(this));
    }
    this.velocity.y *= -this.friction; // 每次碰撞,速度都損失,同時方向反轉
    this.velocity.rotate *= (Math.random() - 0.5) * 20; // 每次碰到地面旋轉速度都隨機
    this.radius -= 3;
    // 修正若是半徑小等於1,直接定爲1
    if (this.radius <= 1) {
      this.radius = 1;
    }
  } else {
    this.velocity.y += this.gravity; // 沒碰到地面,速度增長
  }

  this.x += this.velocity.x;
  this.y += this.velocity.y;
  this.rotate += this.velocity.rotate;

  // 進入消失倒計時
  if (this.radius - 1 <= 0 && !this.die) {
    this.timeToLive--;
    this.opacity -= 1 / Math.max(1, this.timeToLive); // 不透明從慢到快
    if (this.timeToLive < 0) {
      this.die = true;
    }
  }
};

// 畫2個星星
function drawStars() {
  for (let i = 0; i < 2; i++) {
    starsArray.push(new Star());
  }
}

function Explosion(star) {
  this.particles = []; // 用來存放爆炸粒子
  this.init(star);
}

Explosion.prototype.init = function(star) {
  for (let i = 0; i < 4 + Math.random() * 10; i++) {
    const dx = (Math.random() - 0.5) * 8; // 隨機生成的x方向速度
    const dy = (Math.random() - 0.5) * 20; // 隨機生成的y方向速度
    this.particles.push(new Particle(star.x, star.y, dx, dy)); // 把座標和速度傳給Particle構造函數
  }
};

Explosion.prototype.update = function() {
  this.particles.forEach((particle, index, particles) => {
    if (particle.timeToLive <= 0) {
      // 生命週期結束
      particles.splice(index, 1);
      return;
    }
    particle.update();
  });
};

function Particle(x, y, dx, dy) {
  this.x = x;
  this.y = y;
  this.dx = dx;
  this.dy = dy;
  this.size = {
    width: 2,
    height: 2
  };
  this.friction = 0.7;
  this.gravity = 0.5;
  this.opacity = 1;
  this.timeToLive = 200;
  this.shadowColor = '#E3EAEF';
}

Particle.prototype.draw = function() {
  c.save();
  c.fillStyle = 'rgba(227, 234, 239,' + this.opacity + ')';
  c.shadowColor = this.shadowColor;
  c.shadowBlur = 20;
  c.shadowOffsetX = 0;
  c.shadowOffsetY = 0;
  c.fillRect(this.x, this.y, this.size.width, this.size.height);
  c.restore();
};

Particle.prototype.update = function() {
  this.draw();
  // 碰到兩邊牆壁
  if (
    this.x + this.size.width + this.dx > canvas.width ||
    this.x + this.dx < 0
  ) {
    this.dx *= -this.friction;
  }
  // 碰到地面
  if (this.y + this.size.height + this.dy > canvas.height) {
    this.dy *= -this.friction;
  } else {
    this.dy += this.gravity;
  }
  this.x += this.dx;
  this.y += this.dy;

  this.timeToLive--;
  this.opacity -= 1 / this.timeToLive; //不透明度ease-in效果
};

function animation() {
  requestAnimationFrame(animation);
  // 畫背景
  c.fillStyle = backgroundGradient;
  c.fillRect(0, 0, canvas.width, canvas.height);
  // 畫背景星星
  // 隨機將一個背景星星定義成流星
  if (~~spawnTimer % 103 === 0) {    // 這裏選擇一個質數來求餘,使得一個生成周期內最多觸發一次
    skyStarsArray[
      ~~(Math.random() * skyStarsArray.length)
    ].falling = true;
  }
  skyStarsArray.forEach((skyStar, index) => {
    // 若是超出canvas或者做爲流星滑落結束,則去除這顆星星,在canvas左側從新生成一顆
    if (
      skyStar.x - skyStar.radius - 20 > canvas.width ||
      skyStar.timeToLive < 0
    ) {
      skyStarsArray.splice(index, 1);
      skyStarsArray.push(new Skystar(-Math.random() * canvas.width));
      return;
    }
    // 星空隨機產生流星
    if (skyStar.falling) {
      skyStar.x += skyStar.dx;
      skyStar.y += skyStar.dy;
      skyStar.color = '#fff';
      // 半徑慢慢變小
      if (skyStar.radius > 0.05) {
        skyStar.radius -= 0.05;
      } else {
        skyStar.radius = 0.05;
      }
      skyStar.timeToLive--;
    }
    skyStar.update();
  });
  // 畫山
  drawMountains(1, canvas.height, canvas.height * 0.78, '#384551', 300);
  drawMountains(2, canvas.height, canvas.height * 0.64, '#2B3843', 400);
  drawMountains(3, canvas.height, canvas.height * 0.42, '#26333E', 150);
  // 畫地面
  c.fillStyle = '#182028';
  c.fillRect(0, canvas.height * 0.85, canvas.width, canvas.height * 0.15);
  // 畫墜落的星星
  starsArray.forEach((star, index) => {
    if (star.die) {
      starsArray.splice(index, 1);
      return;
    }
    star.update();
  });
  // 循環更新爆炸點
  explosionsArray.forEach((explosion, index) => {
    if (explosion.particles.length === 0) {
      explosionsArray.splice(index, 1);
      return;
    }
    explosion.update();
  });
  // 控制隨機生成墜星
  spawnTimer--;
  if (spawnTimer < 0) {
    spawnTimer = Math.random() * 500;
    starsArray.push(new Star());
  }
}

init();
animation();

整個效果和製做流程就是這樣,但願大家能喜歡。快過年了,提早祝你們春節快樂,過年要放煙花,接下去也想研究一下製做煙花的效果,有興趣的朋友一塊兒交流吧~~

相關文章
相關標籤/搜索