Intro
转载请注明来源,可以在测试博客查看完成效果。
本篇讲述如何绘制动态的星空,其实关联到频域数据已经没什么悬念了。
一、使用Canvas绘图
1.1 位置和大小
绘制背景的第一要务便是把canvas元素放置在背景这一层次上,避免遮盖其他元素。
对我而言,个人习惯用css来设置大小和位置,用html来确定渲染顺序而不是z-index。
下面是html代码。
1<html> 2 3<body> 4 <canvas id="background-canvas"></canvas> 5 <!-- other elements --> 6</body> 7 8</html>
下面是css代码。
1#background-canvas { 2 position: fixed; 3 left: 0; 4 top: 0; 5 width: 100vw; 6 height: 100vh; 7 background-color: black; 8}
fixed确保拖动页面不会令背景也跟随移动。
其余部分我想应该没什么有疑问的地方。
1.2 CanvasContext2D
对于canvas元素的绘图操作我想很多人应该接触过。
以绘制圆形为例,使用如下代码。
1const canvas = document.getElementById("background-canvas"); 2const ctx = canvas.getContext("2d"); 3 4ctx.fillStyle='#fff'; 5ctx.beginPath(); 6ctx.arc(100,100,50,0,Math.PI*2); // 参数分别为坐标x,y,半径,起始弧度,结束弧度 7ctx.fill();
这样就画完了一个实心圆。
需要注意,canvas的大小通过css设置可能导致画面被拉伸变形模糊,所以最好的办法是绘制前确定一下canvas的大小。
此外需要注意的是,重置大小会导致画面清空,用这种方式可以替代fillRect或者clearRect,有的浏览器平台更快但也有浏览器更慢。可以查阅这篇博文来参考如何提升canvas绘图性能。
fillStyle可以使用css的颜色代码,也就是说我们可以写下诸如rgba、hsla之类的颜色,这给我们编写代码提供了很多方便。
1.3 绘制星星
星空是由星星组成的这显然不用多说了,先来看如何绘制单个星星。
星星的绘制方法很多,贴图虽然便利但显然不够灵活,我们的星星是要随节奏改变亮度和大小的,利用贴图的话就只能在alpha值和drawImage缩放来处理了。虽然是一种不错的办法,不过这里我使用了RadialGradient来控制绘图。
PS:
RadialGradient的性能比较差,大量使用会导致明显的性能下降,这是一个显著降低绘制效率的地方。
那么,我们先画一个圆(加点细节预警)。
1const canvas = document.getElementById("background-canvas"); 2const ctx = canvas.getContext("2d"); 3 4// 确保不会变形 5canvas.width = canvas.offsetWidth; 6canvas.height = canvas.offsetHeight; 7 8// 参数分别为起始坐标x,y,半径,结束坐标x,y,半径 9const gradient = ctx.createRadialGradient(100, 100, 0, 100, 100, 50); 10gradient.addColorStop(0.025, "#fff"); // 中心的亮白色 11gradient.addColorStop(0.1, "rgba(255, 255, 255, 0.9)"); // 核心光点和四周的分界线 12gradient.addColorStop(0.25, "hsla(198, 66%, 75%, 0.9)"); // 核心亮点往四周发散的蓝光 13gradient.addColorStop(0.75, "hsla(198, 64%, 33%, 0.4)"); // 蓝光边缘 14gradient.addColorStop(1, "hsla(198, 64%, 33%, 0)"); // 淡化直至透明 15ctx.fillStyle = gradient; 16ctx.beginPath(); 17ctx.arc(100, 100, 50, 0, Math.PI * 2); 18ctx.fill();
可以在codepen查看效果或直接编辑你的星(圈)星(圈)。
看上去还不错?
让我们用代码控制它的亮度和大小。
1const canvas = document.getElementById("background-canvas"); 2const ctx = canvas.getContext("2d"); 3 4// 确保不会变形 5canvas.width = canvas.offsetWidth; 6canvas.height = canvas.offsetHeight; 7 8// 通过energy控制亮度和大小 9let energy = 255; 10let radius = 50; 11let energyChangeRate = -1; 12 13function draw() { 14 requestAnimationFrame(draw); // 定时绘制,requestAnimationFrame比setTimeout更好。 15 energy += energyChangeRate; // 见过呼吸灯吧?我们让它变亮~再变暗~反复循环~ 16 if (energy <= 0 || energy >= 255) energyChangeRate = -energyChangeRate; 17 18 // 计算出当前的大小 19 const r = radius + energy * 0.1; 20 21 // 清空屏幕 22 ctx.fillStyle = "black"; 23 ctx.fillRect(0, 0, canvas.width, canvas.height); 24 25 // 参数分别为起始坐标x,y,半径,结束坐标x,y,半径 26 const gradient = ctx.createRadialGradient(100, 100, 0, 100, 100, r); 27 gradient.addColorStop(0.025, "#fff"); // 中心的亮白色 28 gradient.addColorStop(0.1, "rgba(255, 255, 255, 0.9)"); // 核心光点和四周的分界线 29 gradient.addColorStop(0.25, `hsla(198, 66%, ${Math.min(75+energy*0.01,100)}%, 0.9)`); // 核心亮点往四周发散的蓝光 30 gradient.addColorStop(0.75, `hsla(198, 64%, ${Math.min(33+energy*0.01,100)}%, 0.4)`); // 蓝光边缘 31 gradient.addColorStop(1, "hsla(198, 64%, 33%, 0)"); // 淡化直至透明 32 ctx.fillStyle = gradient; 33 ctx.beginPath(); 34 ctx.arc(100, 100, r, 0, Math.PI * 2); 35 ctx.fill(); 36} 37draw();
可以在codepen查看并编辑效果。
1.4 封装星星
通常来说粒子系统不大会把单个粒子封装成类,因为函数调用的开销还是蛮大的。。。
不过在这里我们这里就先这样了,方便理解和阅读。渲染的瓶颈解决之前,粒子函数调用这点开销根本不是回事儿。
1const canvas = document.getElementById("background-canvas"); 2const ctx = canvas.getContext("2d"); 3 4// 确保不会变形 5canvas.width = canvas.offsetWidth; 6canvas.height = canvas.offsetHeight; 7 8// 用javascript原生的class而不是prototype 9class Star { 10 constructor(x, y, radius, lightness) { 11 this.radius = radius; 12 this.x = x; 13 this.y = y; 14 this.lightness; 15 } 16 17 draw(ctx, energy) { 18 // 计算出当前的大小 19 const r = this.radius + energy * 0.1; 20 21 // 参数分别为起始坐标x,y,半径,结束坐标x,y,半径 22 const gradient = ctx.createRadialGradient( 23 this.x, 24 this.y, 25 0, 26 this.x, 27 this.y, 28 r 29 ); 30 gradient.addColorStop(0.025, "#fff"); // 中心的亮白色 31 gradient.addColorStop(0.1, "rgba(255, 255, 255, 0.9)"); // 核心光点和四周的分界线 32 gradient.addColorStop( 33 0.25, 34 `hsla(198, 66%, ${Math.min(75 + energy * 0.01, 100)}%, 0.9)` 35 ); // 核心亮点往四周发散的蓝光 36 gradient.addColorStop( 37 0.75, 38 `hsla(198, 64%, ${Math.min(33 + energy * 0.01, 100)}%, 0.4)` 39 ); // 蓝光边缘 40 gradient.addColorStop(1, "hsla(198, 64%, 33%, 0)"); // 淡化直至透明 41 ctx.fillStyle = gradient; 42 ctx.beginPath(); 43 ctx.arc(this.x, this.y, r, 0, Math.PI * 2); 44 ctx.fill(); 45 } 46} 47 48const star = new Star(100, 100, 50); 49 50let energy = 255; 51let energyChangeRate = -1; 52 53// 渲染函数来循环渲染! 54function render() { 55 requestAnimationFrame(render); 56 energy += energyChangeRate; 57 if (energy <= 0 || energy >= 255) energyChangeRate = -energyChangeRate; 58 // 清空屏幕 59 ctx.fillStyle = "black"; 60 ctx.fillRect(0, 0, canvas.width, canvas.height); 61 62 star.draw(ctx, energy); 63} 64 65// 开始渲染动画! 66render(); 67
可以在codepen查看代码效果。
完成!
1.5 银河
绘制银河的核心在于随机分布的星星绕着同一中心点旋转,分为两步来讲,第一步是随机分布,这很简单,用Math.random就好了。
1// star 部分略 2 3class Galaxy { 4 constructor(canvas) { 5 this.stars = []; 6 this.canvas = canvas; 7 this.ctx = canvas.getContext("2d"); 8 9 this.energy = 255; 10 this.energyChangeRate = -2; 11 } 12 13 init(num) { 14 for (let i = 0; i < num; i++) { 15 this.stars.push( 16 // 随机生成一定数量的星星,初始化星星位置和大小。 17 new Star( 18 Math.random() * this.canvas.width, 19 Math.random() * this.canvas.height, 20 Math.random() * 10 + 1, 21 Math.random() * 30 + 33 22 ) 23 ); 24 } 25 } 26 27 render() { 28 this.energy += this.energyChangeRate; 29 if (this.energy <= 0 || this.energy >= 255) 30 this.energyChangeRate = -this.energyChangeRate; 31 32 // 清空屏幕 33 this.ctx.fillStyle = "black"; 34 this.ctx.fillRect(0, 0, canvas.width, canvas.height); 35 36 for (const star of this.stars) { 37 star.draw(this.ctx, this.energy); 38 } 39 } 40} 41 42const canvas = document.getElementById("background-canvas"); 43// 确保不会变形 44canvas.width = canvas.offsetWidth; 45canvas.height = canvas.offsetHeight; 46const galaxy = new Galaxy(canvas); 47galaxy.init(50); 48 49function render() { 50 requestAnimationFrame(render); 51 galaxy.render(); 52} 53 54render();
可以在codepen查看效果和完整代码。
1.6 旋转起来!
【加点细节预警】
接下来我们为星星准备轨道参数,让它们动起来!
首先修改Star类,加入几个字段。
1class Star { 2 constructor(x, y, radius, lightness, orbit, speed, t) { 3 this.radius = radius; 4 this.x = x; 5 this.y = y; 6 this.lightness; 7 this.orbit = orbit; // 轨道 8 this.speed = speed; // 运动速度 9 this.t = t; // 三角函数x轴参数,用 sin/cos 组合计算位置 10 } 11 // 下略 12}
修改初始化代码。
1// 前略 2 init(num) { 3 const longerAxis = Math.max(this.canvas.width, this.canvas.height); 4 const diameter = Math.round( 5 Math.sqrt(longerAxis * longerAxis + longerAxis * longerAxis) 6 ); 7 const maxOrbit = diameter / 2; 8 9 for (let i = 0; i < num; i++) { 10 this.stars.push( 11 // 随机生成一定数量的星星,初始化星星位置和大小。 12 new Star( 13 Math.random() * this.canvas.width, 14 Math.random() * this.canvas.height, 15 Math.random() * 10 + 1, 16 Math.random() * 30 + 33, 17 Math.random() * maxOrbit, // 随机轨道 18 Math.random() / 1000, // 随机速度 19 Math.random() * 100 // 随机位置 20 ) 21 ); 22 } 23 // 后略
然后在Galaxy里加入控制移动的代码。
1move() { 2 for (const star of this.stars) { 3 console.log(star.orbit) 4 star.x = this.canvas.width/2+ Math.cos(star.t) * star.orbit; 5 star.y = this.canvas.height/2+ Math.sin(star.t) * star.orbit/2; 6 star.t += star.speed; 7 }
然后每一帧进行移动!
1function render() { 2 requestAnimationFrame(render); 3 galaxy.render(); 4 galaxy.move(); // 动起来! 5}
大功告成!
在codepen查看完整源码!
1.7 待续
PS:不保证粘贴的代码都能跑,反正codepen上是都能的。