从左到右依序执行,最後该函式会再呼叫自己一次,图中淡化的区块是下个章节的主题
然後把它跟程序码做对应:
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篇了,实在是觉得不该再拖了,结尾没能讲的详细的部分,就祈祷我程序码写得够乾净,大家能看得懂了!如果要需要进一步解释再跟我说吧,再评估看看要不要加开一篇。
<<: 讯息是怎麽进到网际网路的(二)?区网内的装置:AP, Switch, Router
接着来讲讲常用的运算值.... +加 1+1=2 -减 1-1= 0 *乘 2*2=4 /除 2/2...
Outline Characters C strings C string processing f...
当应用程序为了执行耗时任务而无法处里使用者操作时,就会产生ANR,解决方式就是用非同步处理。 执行绪...
这个问题其实在 Day8 的文章有稍微提到过,但大多数人看文件时都大致看一下而会忽略一些小细节,包含...
前言 我记得我之前在Medium写文的时候,刚开始提到的就是「药与盒子」的概念。所谓的药就是被指派的...