去年在电影院看过的电影,印象最深刻的,算是电影《你的名字》了,而且被其中的画面深深吸引了,尤其是陨石划过天空的场景,太美啦!所以想着哪天做一个canvas的流星效果。最近刚好看到油管上的一个视频,作者的主页就是陨石坠落的粒子效果为背景,虽然没有《你的名字》中那么写实,但也是很漂亮了,效果大概长这样,附上链接https://codepen.io/christopher4lis/pen/PzONKR
在这个基础上,我做了一些修改,将圆形粒子换成五角星,背景星空无限右移,且随机产生流星。
下面我把制作过程详细讲解一下。
初始设置
生成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();
接下去,我们想画连起来的山
显然这山画得...我们要想有那种峰峦叠嶂的感觉,那就必须把山脚和山脚之间重叠起来,像这样
That‘s it! 下面我们来分析一下如何用一个通用的方法来画出这样的峰峦叠嶂
首先,定义一个函数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();
嗯,效果有了,可是...
一段时间后,左边没有星星了。。。原因是,星星都移出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();
整个效果和制作流程就是这样,希望你们能喜欢。快过年了,提前祝大家春节快乐,过年要放烟花,接下去也想研究一下制作烟花的效果,有兴趣的朋友一起交流吧~~
原文地址:https://www.cnblogs.com/zealingstar/p/8277177.html