AudioContext技術和音樂視覺化(2)
Intro
轉載請註明來源,可以在測試部落格檢視完成效果。
本篇講述如何繪製動態的星空,其實關聯到頻域資料已經沒什麼懸念了。
一、使用Canvas繪圖
1.1 位置和大小
繪製背景的第一要務便是把canvas元素放置在背景這一層次上,避免遮蓋其他元素。
對我而言,個人習慣用css來設定大小和位置,用html來確定渲染順序而不是z-index。
下面是html程式碼。
<html> <body> <canvas id="background-canvas"></canvas> <!-- other elements --> </body> </html>
下面是css程式碼。
#background-canvas {
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background-color: black;
}
fixed
確保拖動頁面不會令背景也跟隨移動。
其餘部分我想應該沒什麼有疑問的地方。
1.2 CanvasContext2D
對於canvas元素的繪圖操作我想很多人應該接觸過。
以繪製圓形為例,使用如下程式碼。
const canvas = document.getElementById("background-canvas"); const ctx = canvas.getContext("2d"); ctx.fillStyle='#fff'; ctx.beginPath(); ctx.arc(100,100,50,0,Math.PI*2); // 引數分別為座標x,y,半徑,起始弧度,結束弧度 ctx.fill();
這樣就畫完了一個實心圓。
需要注意,canvas的大小通過css設定可能導致畫面被拉伸變形模糊,所以最好的辦法是繪製前確定一下canvas的大小。
此外需要注意的是,重置大小會導致畫面清空,用這種方式可以替代fillRect
或者clearRect
,有的瀏覽器平臺更快但也有瀏覽器更慢。可以查閱這篇博文來參考如何提升canvas繪圖效能。
fillStyle
可以使用css的顏色程式碼,也就是說我們可以寫下諸如rgba
、hsla
之類的顏色,這給我們編寫程式碼提供了很多方便。
1.3 繪製星星
星空是由星星組成的這顯然不用多說了,先來看如何繪製單個星星。
星星的繪製方法很多,貼圖雖然便利但顯然不夠靈活,我們的星星是要隨節奏改變亮度和大小的,利用貼圖的話就只能在alpha
drawImage
縮放來處理了。雖然是一種不錯的辦法,不過這裡我使用了RadialGradient
來控制繪圖。
PS:
RadialGradient
的效能比較差,大量使用會導致明顯的效能下降,這是一個顯著降低繪製效率的地方。
那麼,我們先畫一個圓(加點細節預警)。
const canvas = document.getElementById("background-canvas");
const ctx = canvas.getContext("2d");
// 確保不會變形
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
// 引數分別為起始座標x,y,半徑,結束座標x,y,半徑
const gradient = ctx.createRadialGradient(100, 100, 0, 100, 100, 50);
gradient.addColorStop(0.025, "#fff"); // 中心的亮白色
gradient.addColorStop(0.1, "rgba(255, 255, 255, 0.9)"); // 核心光點和四周的分界線
gradient.addColorStop(0.25, "hsla(198, 66%, 75%, 0.9)"); // 核心亮點往四周發散的藍光
gradient.addColorStop(0.75, "hsla(198, 64%, 33%, 0.4)"); // 藍光邊緣
gradient.addColorStop(1, "hsla(198, 64%, 33%, 0)"); // 淡化直至透明
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.fill();
可以在codepen檢視效果或直接編輯你的星(圈)星(圈)。
看上去還不錯?
讓我們用程式碼控制它的亮度和大小。
const canvas = document.getElementById("background-canvas");
const ctx = canvas.getContext("2d");
// 確保不會變形
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
// 通過energy控制亮度和大小
let energy = 255;
let radius = 50;
let energyChangeRate = -1;
function draw() {
requestAnimationFrame(draw); // 定時繪製,requestAnimationFrame比setTimeout更好。
energy += energyChangeRate; // 見過呼吸燈吧?我們讓它變亮~再變暗~反覆迴圈~
if (energy <= 0 || energy >= 255) energyChangeRate = -energyChangeRate;
// 計算出當前的大小
const r = radius + energy * 0.1;
// 清空螢幕
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 引數分別為起始座標x,y,半徑,結束座標x,y,半徑
const gradient = ctx.createRadialGradient(100, 100, 0, 100, 100, r);
gradient.addColorStop(0.025, "#fff"); // 中心的亮白色
gradient.addColorStop(0.1, "rgba(255, 255, 255, 0.9)"); // 核心光點和四周的分界線
gradient.addColorStop(0.25, `hsla(198, 66%, ${Math.min(75+energy*0.01,100)}%, 0.9)`); // 核心亮點往四周發散的藍光
gradient.addColorStop(0.75, `hsla(198, 64%, ${Math.min(33+energy*0.01,100)}%, 0.4)`); // 藍光邊緣
gradient.addColorStop(1, "hsla(198, 64%, 33%, 0)"); // 淡化直至透明
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(100, 100, r, 0, Math.PI * 2);
ctx.fill();
}
draw();
可以在codepen檢視並編輯效果。
1.4 封裝星星
通常來說粒子系統不大會把單個粒子封裝成類,因為函式呼叫的開銷還是蠻大的。。。
不過在這裡我們這裡就先這樣了,方便理解和閱讀。渲染的瓶頸解決之前,粒子函式呼叫這點開銷根本不是回事兒。
const canvas = document.getElementById("background-canvas");
const ctx = canvas.getContext("2d");
// 確保不會變形
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
// 用javascript原生的class而不是prototype
class Star {
constructor(x, y, radius, lightness) {
this.radius = radius;
this.x = x;
this.y = y;
this.lightness;
}
draw(ctx, energy) {
// 計算出當前的大小
const r = this.radius + energy * 0.1;
// 引數分別為起始座標x,y,半徑,結束座標x,y,半徑
const gradient = ctx.createRadialGradient(
this.x,
this.y,
0,
this.x,
this.y,
r
);
gradient.addColorStop(0.025, "#fff"); // 中心的亮白色
gradient.addColorStop(0.1, "rgba(255, 255, 255, 0.9)"); // 核心光點和四周的分界線
gradient.addColorStop(
0.25,
`hsla(198, 66%, ${Math.min(75 + energy * 0.01, 100)}%, 0.9)`
); // 核心亮點往四周發散的藍光
gradient.addColorStop(
0.75,
`hsla(198, 64%, ${Math.min(33 + energy * 0.01, 100)}%, 0.4)`
); // 藍光邊緣
gradient.addColorStop(1, "hsla(198, 64%, 33%, 0)"); // 淡化直至透明
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(this.x, this.y, r, 0, Math.PI * 2);
ctx.fill();
}
}
const star = new Star(100, 100, 50);
let energy = 255;
let energyChangeRate = -1;
// 渲染函式來迴圈渲染!
function render() {
requestAnimationFrame(render);
energy += energyChangeRate;
if (energy <= 0 || energy >= 255) energyChangeRate = -energyChangeRate;
// 清空螢幕
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
star.draw(ctx, energy);
}
// 開始渲染動畫!
render();
可以在codepen檢視程式碼效果。
完成!
1.5 銀河
繪製銀河的核心在於隨機分佈的星星繞著同一中心點旋轉,分為兩步來講,第一步是隨機分佈,這很簡單,用Math.random
就好了。
// star 部分略
class Galaxy {
constructor(canvas) {
this.stars = [];
this.canvas = canvas;
this.ctx = canvas.getContext("2d");
this.energy = 255;
this.energyChangeRate = -2;
}
init(num) {
for (let i = 0; i < num; i++) {
this.stars.push(
// 隨機生成一定數量的星星,初始化星星位置和大小。
new Star(
Math.random() * this.canvas.width,
Math.random() * this.canvas.height,
Math.random() * 10 + 1,
Math.random() * 30 + 33
)
);
}
}
render() {
this.energy += this.energyChangeRate;
if (this.energy <= 0 || this.energy >= 255)
this.energyChangeRate = -this.energyChangeRate;
// 清空螢幕
this.ctx.fillStyle = "black";
this.ctx.fillRect(0, 0, canvas.width, canvas.height);
for (const star of this.stars) {
star.draw(this.ctx, this.energy);
}
}
}
const canvas = document.getElementById("background-canvas");
// 確保不會變形
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
const galaxy = new Galaxy(canvas);
galaxy.init(50);
function render() {
requestAnimationFrame(render);
galaxy.render();
}
render();
可以在codepen檢視效果和完整程式碼。
1.6 旋轉起來!
【加點細節預警】
接下來我們為星星準備軌道引數,讓它們動起來!
首先修改Star
類,加入幾個欄位。
class Star {
constructor(x, y, radius, lightness, orbit, speed, t) {
this.radius = radius;
this.x = x;
this.y = y;
this.lightness;
this.orbit = orbit; // 軌道
this.speed = speed; // 運動速度
this.t = t; // 三角函式x軸引數,用 sin/cos 組合計算位置
}
// 下略
}
修改初始化程式碼。
// 前略
init(num) {
const longerAxis = Math.max(this.canvas.width, this.canvas.height);
const diameter = Math.round(
Math.sqrt(longerAxis * longerAxis + longerAxis * longerAxis)
);
const maxOrbit = diameter / 2;
for (let i = 0; i < num; i++) {
this.stars.push(
// 隨機生成一定數量的星星,初始化星星位置和大小。
new Star(
Math.random() * this.canvas.width,
Math.random() * this.canvas.height,
Math.random() * 10 + 1,
Math.random() * 30 + 33,
Math.random() * maxOrbit, // 隨機軌道
Math.random() / 1000, // 隨機速度
Math.random() * 100 // 隨機位置
)
);
}
// 後略
然後在Galaxy
里加入控制移動的程式碼。
move() {
for (const star of this.stars) {
console.log(star.orbit)
star.x = this.canvas.width/2+ Math.cos(star.t) * star.orbit;
star.y = this.canvas.height/2+ Math.sin(star.t) * star.orbit/2;
star.t += star.speed;
}
然後每一幀進行移動!
function render() {
requestAnimationFrame(render);
galaxy.render();
galaxy.move(); // 動起來!
}
大功告成!
在codepen檢視完整原始碼!
1.7 待續
PS:不保證貼上的程式碼都能跑,反正codepen上是都能的。