下面我把制作過程詳細講解一下。
生成canvas標簽,設置canvas長寬
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);
看下效果

畫山要有幾個設置參數:1.位置 2.顏色 3.畫幾個
從簡單開始,我們將山的坐標點設在三角形的最左邊那個角,先畫三角形的底邊,然后畫上頂角
c.beginPath(); c.moveTo(0, canvas.height / 2); c.lineTo(300, canvas.height / 2); c.lineTo(150, canvas.height / 4); c.closePath(); c.fill();

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

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

首先,定義一個函數drawMountains(),要傳入的參數:1、山的數量number,2、山的坐標y(坐標x直接根據山的數量計算出來,3、山的高度height,4、山的顏色color),5、重疊偏移量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();
嗯,效果有了,可是...

修改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
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繪制五角星的方法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; } } };
現在,定義一個初始化函數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) { // ... }
爆炸的時候,會飛出小顆粒,也就是生成了新的物體,所以這里還要定義粒子的構造函數。而且在創建爆炸點的那個時候,就要生成幾個粒子實例。
粒子的構造函數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();
整個效果和制作流程就是這樣,希望你們能喜歡。快過年了,提前祝大家春節快樂,過年要放煙花,接下去也想研究一下制作煙花的效果,有興趣的朋友一起交流吧~~

