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篇了,实在是觉得不该再拖了,结尾没能讲的详细的部分,就祈祷我程序码写得够乾净,大家能看得懂了!如果要需要进一步解释再跟我说吧,再评估看看要不要加开一篇。


<<:  讯息是怎麽进到网际网路的(二)?区网内的装置:AP, Switch, Router

>>:  Day12 经常搞混的CSS Position

ASP.NET MVC 从入门到放弃 (Day4) -C#运算值介绍

接着来讲讲常用的运算值.... +加 1+1=2 -减 1-1= 0 *乘 2*2=4 /除 2/2...

Day 19 - C strings 字串,我好想吃串烧

Outline Characters C strings C string processing f...

Day 12 | 同步与非同步执行

当应用程序为了执行耗时任务而无法处里使用者操作时,就会产生ANR,解决方式就是用非同步处理。 执行绪...

Day 33 | 常见 Livewire 问题:解决 Livewire.on() 没有作用的问题

这个问题其实在 Day8 的文章有稍微提到过,但大多数人看文件时都大致看一下而会忽略一些小细节,包含...

#Day2-- 卖药仔是我!你想要用哪种盒子装?

前言 我记得我之前在Medium写文的时候,刚开始提到的就是「药与盒子」的概念。所谓的药就是被指派的...