把網頁當畫布揮灑的 Canvas 08:開發架構與模板

開發過彈跳球和貪吃蛇遊戲,今天我們來分析 Canvas 專案的程式架構,製作開發模板,方便我們未來快速使用!

Canvas 物件導向開發架構

Canvas繪圖的程式架構

上面這張圖簡介 Canvas 繪圖的程式架構,以下分別說明每個模組所負責的功能,拼組在一起便是我們未來快速開發 Canvas 專案的模板囉!

設定環境變數

在全域設定我們可能會用到的變數,主要會有這兩個:

  • updateFPS :更新物件狀態/資料的頻率。
  • time :累計的時間。

1
2
3
4
const updateFPS = 30;
let time = 0;
const bgColor = "black";
let showMouse = true;

向量類別

透過向量類別來計算物件的位置變化,這裡用 class 來宣告:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Vec2 {
constructor(x, y) {
this.x = x || 0;
this.y = y || 0;
}
set(x, y) {
this.x = x;
this.y = y;
}
move(x, y) {
this.x += x;
this.y += y;
}
add(v) {
return new Vec2(this.x + v.x, this.y + v.y);
}
sub(v) {
return new Vec2(this.x - v.x, this.y - v.y);
}
mul(s) {
return new Vec2(this.x * s, this.y * s);
}
clone() {
return new Vec2(this.x, this.y);
}
toString() {
return `(${this.x}, ${this.y})`;
}
equal(v) {
return this.x === v.x && this.y === v.y;
}
get length() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
get angle() {
return Math.atan2(this.y, this.x);
}
get unit() {
return this.mul(1 / this.length);
}
set length(nv) {
const temp = this.unit.mul(nv);
this.set(temp.x, temp.y);
}
}

initCanvas()

initCanvas() 初始化 canvas 元素並設定其寬高,同時綁定 resize 事件,當視窗大小改變時,同步改變 canvas 的大小。此外,我們也可以自訂 ctx 方法,方便我們繪製常見的圖形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Init Canvas
const canvas = document.getElementById("my-canvas");
const ctx = canvas.getContext("2d");
function initCanvas() {
// do not declare ww & wh with 'var' or 'let' to make them global variables
ww = canvas.width = window.innerWidth;
wh = canvas.height = window.innerHeight;
}
initCanvas();

// Customize ctx methods
ctx.circle = function(v, r) {
this.arc(v.x, v.y, r, 0, Math.PI * 2);
};
ctx.line = function(v1, v2) {
this.moveTo(v1.x, v1.y);
this.lineTo(v2.x, v2.y);
};

// Resize the size of canvas when the window is resized
window.addEventListener("resize", initCanvas);

init()

init() 用來初始化專案中的物件或變數,好比先前彈力球的 ball = new Ball()

1
function init() {}

update()

update() 只管更新物件資料或判斷遊戲的流程。譬如貪吃蛇遊戲,我們在 update() 裡判斷貪吃蛇吃到食物後長度會加 1 ,貪吃蛇撞到牆壁和自己身體的時候會遊戲結束。 這裡的 time++ 會累計時間,透過時間變化來製作動畫。

1
2
3
function update() {
time++;
}

draw()

darw() 負責所有 canvas 畫面的繪製,包括透過 fillRect() 更新畫布背景和物件繪製,譬如貪吃蛇遊戲中,透過 snake.draw() 繪圖;彈力球則是透過 ball.draw()

要注意的是,繪製物件之前要先用 ctx.save()ctx.restore() 來儲存畫布座標系的位置,避免畫布位置被洗掉。

這裡額外繪製滑鼠的十字瞄準效果,提醒我們目前滑鼠的座標位置。

執行最後呼叫 requestAnimationFrame(draw) ,預定下次更新,也確保畫面更新頻率與瀏覽器同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function draw() {
// Clear the background of canvas
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, ww, wh);

// ------------------------------------------------- //
// Draw here ---------------------------------------
ctx.save();
// ball.draw()
ctx.restore();
// ------------------------------------------------- //

// Draw mouse target
ctx.fillStyle = "red";
ctx.beginPath();
ctx.circle(mousePos, 3);
ctx.fill();

ctx.save();
ctx.beginPath();
ctx.translate(mousePos.x, mousePos.y);
ctx.strokeStyle = "red";
const len = 20;
ctx.line(new Vec2(-len, 0), new Vec2(len, 0));
ctx.fillText(mousePos, 10, -10);
ctx.rotate(Math.PI / 2);
ctx.line(new Vec2(-len, 0), new Vec2(len, 0));
ctx.stroke();
ctx.restore();

requestAnimationFrame(draw);
}

load 事件

當網頁載入完成後,便呼叫 initCanvasinitdraw 以及 update 函式。

1
2
3
4
5
6
7
window.addEventListener("load", loaded);
function loaded() {
initCanvas();
init();
requestAnimationFrame(draw);
setInterval(update, 1000 / updateFPS);
}

mouse 事件

紀錄滑鼠的座標位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const mousePos = new Vec2(0, 0);
let mousePosDown = new Vec2(0, 0);
let mousePosUp = new Vec2(0, 0);

window.addEventListener("mousemove", mousemove);
window.addEventListener("mouseup", mouseup);
window.addEventListener("mousedown", mousedown);

function mousemove(e) {
mousePos.set(e.x, e.y);
}
function mouseup(e) {
mousePos.set(e.x, e.y);
mousePosUp = mousePos.clone();
}
function mousedown(e) {
mousePos.set(e.x, e.y);
mousePosDown = mousePos.clone();
}

Dat-gui

Dat-gui 讓我們快速控制物理參數,也可以在模板中引進來,別忘了先透過 CDN 載入:

1
2
3
4
5
6
7
8
// Dat-gui Control
const controls = {
value: 0
};
const gui = new dat.GUI();
gui.add(controls, "value", -2, 2)
.step(0.01)
.onChange(value => {});


參考資料

  1. 動畫互動網頁特效入門(JS/CANVAS):5-6 ES6 類別定義與模板製作
把網頁當畫布揮灑的 Canvas 09:粒子特效 把網頁當畫布揮灑的 Canvas 07:貪吃蛇

評論

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×