iT邦幫忙

2021 iThome 鐵人賽

DAY 5
0
Modern Web

從零開始打造網頁遊戲-造輪子你也辦的到!系列 第 5

Chapter1-DJ最愛的音頻動感圖像(IV)讓音樂動起來!開篇基礎設定和動畫框架

話不多說先上圖

https://ithelp.ithome.com.tw/upload/images/20210912/20135197L5M2yB4Pzt.jpg

從左到右依序執行,最後該函式會再呼叫自己一次,圖中淡化的區塊是下個章節的主題

然後把它跟程式碼做對應:

function AnimationLoop(){
    Resize("#game-box", canvas, context, '#000');
    Redraw();
    requestAnimationFrame(AnimationLoop);
}

做動畫肯定是推薦requestAnimationFrame,而不是SetTimeout,因為這個函式如字面意思所說,它是請求瀏覽器在下一偵之前執行要求的代碼,如此一來你無須擔心每一偵間隔幾秒而去計算,畫面上也會是最流暢的。

Resize 重整視窗

先從遊戲畫面開始講吧!一般在做初始化設置,理想上只需要取得一次裝置的寬度和高度,做一次整個遊戲畫面的基礎設置就好,不過,這邊考量到一些情況如:手機用戶翻轉畫面、電腦用戶打開網頁後才調整視窗大小,那就得對整體布局做調整了。

如下方,最一開始未對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影響效能(這邊不細談原理),開頭處才會加上一個判斷式,來檢查畫面有沒有改變,如果沒有,那就不要做不必要的設定。

Redraw 重繪畫布

function Redraw(){
    clear(context);
    AudioProcess();
}

咦?怎麼只有兩行,一樣直接上圖比較好懂,底層可以分成4步驟:

  1. 清空畫布
  2. 從節點取得頻譜
  3. 陣列運算
  4. 繪製直方圖
    (分別包成了不同的function放在不同位置)
    https://ithelp.ithome.com.tw/upload/images/20210912/20135197oPQMd2uGS1.jpg

有人可能想問了,那你把這麼簡單的步驟畫得這麼複雜,何苦呢?其實,這個結構有著高度彈性的優勢,保留同一個函式重複利用的可能性,易於拆裝重組結構,對於初期開發來講,經常有高度變動性時,相當適合,只需要注意深度不要過深,像這樣往下到第四層已經差不多了。

Clear 清空畫布

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的準備了!

AudioProcess 處理音訊

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.shrink 陣列縮減的比率(相當於每個直方圖的間隔)
  • window.padding 每個直方圖的padding(建議要小於shrink)
  • window.think 每個直方圖的初始厚度(高度)

也把這幾個變數放到window物件身上,方便給大家在console裡面,可以直接調整玩玩看的,試試看吧

後記

還沒吃飯的我,在這邊苦苦奮戰~因為待會吃飽飯要去一趟圖書館,怕來不及回來,就先寫,沒想到還是花了不少時間,中間一度考慮拆成兩篇的,只是這章節從原本計畫的2篇結束,到現在已經篇幅拉到第4篇了,實在是覺得不該再拖了,結尾沒能講的詳細的部分,就祈禱我程式碼寫得夠乾淨,大家能看得懂了!如果要需要進一步解釋再跟我說吧,再評估看看要不要加開一篇。


上一篇
Chapter1-DJ最愛的音頻動感圖像(III)媽媽叫你不要玩音樂,現在知道當DJ很難了吧
下一篇
Chapter1 - 補充 CORS + autoplay政策 + requestAnimeFrame致命缺點
系列文
從零開始打造網頁遊戲-造輪子你也辦的到!31

尚未有邦友留言

立即登入留言