從左到右依序執行,最後該函式會再呼叫自己一次,圖中淡化的區塊是下個章節的主題
然後把它跟程式碼做對應:
function AnimationLoop(){
Resize("#game-box", canvas, context, '#000');
Redraw();
requestAnimationFrame(AnimationLoop);
}
做動畫肯定是推薦requestAnimationFrame,而不是SetTimeout,因為這個函式如字面意思所說,它是請求瀏覽器在下一偵之前執行要求的代碼,如此一來你無須擔心每一偵間隔幾秒而去計算,畫面上也會是最流暢的。
先從遊戲畫面開始講吧!一般在做初始化設置,理想上只需要取得一次裝置的寬度和高度,做一次整個遊戲畫面的基礎設置就好,不過,這邊考量到一些情況如:手機用戶翻轉畫面、電腦用戶打開網頁後才調整視窗大小,那就得對整體布局做調整了。
如下方,最一開始未對Canvas做設定時,其預設的畫布大小只有300x150,在網頁上的像素值也是300x150,咦?你說這兩者有差別嗎?不瞞你說還真的有差別!Canvas的畫布大小其實是給你畫畫用的,而網頁上的實際寬高又是由Css做調整的,假如Canvas畫布大小比實際像素值多的話,Canvas會進行縮放,從大畫布變成小畫面,那麼解析度就會更高,畫面會更細緻,當然對於效能的損耗也越多,這邊就先將比例設為2:1,後續可以視情況調整。
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
const RATIO = 2;
let WIDTH, HEIGHT;
設計邏輯: Canvas畫布寬高 = 實際像素值 * RATIO
另外還設置了一個WIDTH和HEIGHT對於以後的布局會很方便
承上所述,下面會有這樣的寫法,分別對Canvas本身的寬高,以及Canvas.style的寬高進行動態調整,另外在HTML還設計了一個容器gamebox來裝下canvas,目前是滿頻,使canvas跟容器一樣大,未來可以根據不同的需求做調整,例如正方形畫面置中、保留左側選單列、或保留上方選單列等設計。
function Resize(boxID, canvas, context, fillStyle=undefined){
if(WIDTH != window.innerWidth * RATIO || HEIGHT != window.innerHeight * RATIO){
WIDTH = window.innerWidth * RATIO;
HEIGHT = window.innerHeight * RATIO;
let box = document.querySelector(boxID);
canvas.width = WIDTH;
canvas.height = HEIGHT;
canvas.style.width = WIDTH/RATIO + "px";
canvas.style.height = HEIGHT/RATIO + "px";
box.style.width = WIDTH/RATIO + "px";
box.style.height = HEIGHT/RATIO + "px";
if(fillStyle != undefined){
context.beginPath();
context.rect(0, 0, WIDTH, HEIGHT);
context.fillStyle = fillStyle;
context.fill();
}
}
}
因為WIDTH是畫布大小,要換算回實際的寬度要除以縮放比例: WIDTH/RATIO + "px"
另一種寫法 canvas.style.width 和 canvas.style.height 可以不用覆寫,可以直接在Css中將其寬高設為100%,便可以隨著容器gamebox調整寬高了。
另外,由於對畫面布局的調整會引起Reflow和Repaint影響效能(這邊不細談原理),開頭處才會加上一個判斷式,來檢查畫面有沒有改變,如果沒有,那就不要做不必要的設定。
function Redraw(){
clear(context);
AudioProcess();
}
咦?怎麼只有兩行,一樣直接上圖比較好懂,底層可以分成4步驟:
有人可能想問了,那你把這麼簡單的步驟畫得這麼複雜,何苦呢?其實,這個結構有著高度彈性的優勢,保留同一個函式重複利用的可能性,易於拆裝重組結構,對於初期開發來講,經常有高度變動性時,相當適合,只需要注意深度不要過深,像這樣往下到第四層已經差不多了。
function clear(context){
// context.clearRect(0, 0, WIDTH, HEIGHT);
context.beginPath();
context.rect(0, 0, canvas.width, canvas.height);
context.fillStyle = 'rgba(0, 0, 0, 0.5)';
context.fill();
}
這沒什麼好說的(被揍)
我是認真的啦!除非大家想看我再寫一篇清空畫布的一百種方法(繼續被揍)
哎呀別這麼火爆,想怎麼清空畫布其實全看個人喜好,差異不大,如果真想要我分享點東西,那就是透明度議題了吧!
context.globalAlpha
人如其名,alpha就是透明度的意思,而且看清楚了,它可是global呀! 表示不管你畫了什麼玩意兒,都會呈現幽靈狀態半透明狀態,具體的應用就有點類似我上面的代碼,顏色使用的是'rgba(0, 0, 0, 0.5)',可以製造些微的殘影效果,相當於把globalAlpha調整成0.5並用'rgb(0, 0, 0)'著色一樣。
不過不建議直接調整它,畢竟,它都寫global了,如果要調整它,就要做好分別對每一個用到context的段落重新設定globalAlpha的準備了!
Hooray~~終於銜接至上回的音訊處理拉!希望大家都還記得我們計算頻寬的方法,忘記的罰你回去看!
function AudioProcess(){
let bands = audioCtx.sampleRate / analyserNode.fftSize * 2; // 每個區段的頻寬
let HighestBands = 16000; // 16kHz高音頻以下的音樂
let index = HighestBands / bands;
bufferLength = analyserNode.frequencyBinCount;
if(bufferLength != undefined){
dataArray = new Uint8Array(bufferLength);
analyserNode.getByteFrequencyData(dataArray);
FrequencyVisualization(dataArray, index, window.shrink);
}
else FrequencyVisualization(new Array(256).fill(0), index, window.shrink);
}
做個簡單的檢查機制,雖然我們前面有定義過analyserNode,若它沒有被正確賦值,或被修改掉,顯然analyserNode.frequencyBinCount這個方法就不存在,並且會取得undefined。因此判斷當該值不為undefined時,才繼續後面的處理,否則,就提供一個全空的陣列。
我們先繼續往後,再來解釋window.shrink(我設來給大家方便修改的變數):
function FrequencyVisualization(dataArray, index, shrink){
const INDEX = index - index%shrink;
ChartArray(context, ReArray(dataArray), WIDTH*0.05, WIDTH*0.9, HEIGHT*0.6, HEIGHT*0.2);
function ReArray(array){
let newArray = new Array();
for (let N = 0; N <= INDEX; N = N + shrink) {
newArray[N] = 0;
for (let n = 0; n < shrink; n++) {
newArray[N + 0] = newArray[N + 0] + array[N + n] / shrink;
}
}
return newArray;
}
function ChartArray(context, array, left, right, middle, height=255){
const WIDTH = (right - left) / INDEX;
const THICK = window.thick;
const PADDING = window.padding;
context.fillStyle = Background.Transform(1.5);
context.strokeStyle = Background.Transform(1.5);
for (let N = 0 ; N <= INDEX; N = N + shrink) {
context.fillRect(left + (N) * WIDTH, middle,
PADDING * WIDTH, -(THICK + array[N] / 255 * height));
context.strokeRect(left + (N) * WIDTH, middle,
PADDING * WIDTH, THICK + array[N] / 255 * height);
}
}
}
這就是直方圖的畫法了,設計一個函式,允許傳入畫布、左邊界、右邊界、垂直中心點、高度,就會自動去計算每個直方圖的位置了。
補充一點就是,這邊我有提供幾個特別的幾個參數,分別有:
也把這幾個變數放到window物件身上,方便給大家在console裡面,可以直接調整玩玩看的,試試看吧
還沒吃飯的我,在這邊苦苦奮戰~因為待會吃飽飯要去一趟圖書館,怕來不及回來,就先寫,沒想到還是花了不少時間,中間一度考慮拆成兩篇的,只是這章節從原本計畫的2篇結束,到現在已經篇幅拉到第4篇了,實在是覺得不該再拖了,結尾沒能講的詳細的部分,就祈禱我程式碼寫得夠乾淨,大家能看得懂了!如果要需要進一步解釋再跟我說吧,再評估看看要不要加開一篇。