canvas
數組
下面我把製做過程詳細講解一下。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();
接下去,咱們想畫連起來的山
顯然這山畫得...咱們要想有那種峯巒疊嶂的感受,那就必須把山腳和山腳之間重疊起來,像這樣
首先,定義一個函數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();
嗯,效果有了,但是...
修改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();
整個效果和製做流程就是這樣,但願大家能喜歡。快過年了,提早祝你們春節快樂,過年要放煙花,接下去也想研究一下製做煙花的效果,有興趣的朋友一塊兒交流吧~~