在昨天我們講到了物件導向的基礎觀念,今天我們就來加深理解並且稍微談談資料驅動這件事情吧!
我們在寫程式的時候會很常用到 事件驅動,也就是我們會直接透過 Don Tree 去選取元素並且綁定事件操作元素。通常這樣的寫法會應用在資料與畫面混雜的程式碼內。
但是,由於我們在撰寫 Canvas 的時候很常會需要先建立 Canvas 畫布的設定、圖形元素的設定,在針對這些設定把元素填入、把元素動畫的時間填入。因此若是沒有透過 資料驅動 來管理程式碼,整坨的 Canvas 程式碼會變得非常難以閱讀。
用 MDN 上的範例來說:
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var raf;
var ball = {
x: 100,
y: 100,
vx: 5,
vy: 2,
radius: 25,
color: 'blue',
draw: function() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
}
};
function draw() {
ctx.clearRect(0,0, canvas.width, canvas.height);
ball.draw();
ball.x += ball.vx;
ball.y += ball.vy;
raf = window.requestAnimationFrame(draw);
}
canvas.addEventListener('mouseover', function(e) {
raf = window.requestAnimationFrame(draw);
});
canvas.addEventListener('mouseout', function(e) {
window.cancelAnimationFrame(raf);
});
ball.draw();
我們可以看到範例在 ball 這個變數內寫了有關於 圖形元素「球」的設定,並且在內部建立的一個 function 來執行「繪製圖形」這件事。
但是像這樣的寫法我們很難去管理這個元素,且若我們要使用 「球」這個類別的設定,就只能畫出 寬 100、高 100、藍色 的圓,若是想要畫一個紅色的圓,就必須要在建立一個全新的變數去設定。
到這邊大家應該就會發現,像工程師這種懶到不行的生物,怎麼可能重複撰寫一大包程式只為了讓藍色的球變成紅色的球呢~
下面我們來做個小範例:
class Arrow {
constructor(options) {
this.imagePath = options.imagePath;
this.image = this.createImage();
this.position = options.position || { x: 0, y: 0 };
this.drawArea = options.drawArea;
this.ticksPerFrame = 40;
this.tickCount = 0;
}
createImage() {
const image = new Image();
image.src = this.imagePath;
return image;
}
updateFrame() {
this.tickCount++;
if (this.tickCount > this.ticksPerFrame) {
this.tickCount = 0;
} else {
this.position.x = this.tickCount * 10;
console.log(this.position, this.tickCount);
}
}
}
class CanvasAnimation {
constructor(options) {
this.width = options.width;
this.height = options.height;
this.canvas = document.querySelector(options.selector);
this.initCanvas();
this.context = this.getCanvasContext();
this.elements = options.elements || [];
}
initCanvas() {
this.canvas.width = this.width;
this.canvas.height = this.height;
}
getCanvasContext() {
return this.canvas.getContext('2d');
}
update() {
this.elements.forEach(element => element.updateFrame());
}
render() {
for (const element of this.elements) {
this.context.clearRect(0, 0, 512, 512);
this.context.drawImage(
element.image,
0, 0,
this.width, this.height,
element.position.x, 0,
element.drawArea.width, element.drawArea.height
);
}
}
play() {
window.requestAnimationFrame(this.play.bind(this));
this.update();
this.render();
}
}
const arrow = new Arrow({
imagePath: 'https://i.imgur.com/6PhDMp8.png',
position: { x: 0, y: 0 },
drawArea: { width: 64, height: 64 }
});
const canvasAnimation = new CanvasAnimation({
selector: '.canvas',
width: 512,
height: 512,
elements: [arrow]
});
// 為了確保圖形元素 100% 的載入後才跑動畫的寫法
arrow.image.addEventListener('load', () => {
canvasAnimation.play();
});
像這樣的寫法,我們可以將每一個設定動作都封裝在 class 裡面,讓閱讀程式的人(比如 N 個月後的你)能更更聚焦在每一段程式碼執行的動作。
並且我們透過資料驅動,讓我們可以更彈性的去設定每一個元素,如此一來程式碼就會更加簡潔。
今天我們透過 Canvas 更深入理解物件導向、ES6 class、跟資料驅動程式碼的差異,不知道大家有沒有理解呢?
也許我們在程式碼上很少會使用到這些觀念,但是學會了這些語法,就會讓人有種特別厲害、特別有成長的感覺呢!
那麼今天就到這邊,大家明天見!