個案研究 - 瞭解 HTML5 畫布

Derek Detweiler
Derek Detweiler

簡介

在 2010 年春季,我對 HTML5 和相關技術的支援快速增加感到興趣。當時,我和朋友在為期兩週的遊戲開發競賽中互相較勁,藉此磨練程式設計和開發技能,並將我們不斷拋出來討論的遊戲構想付諸實現。因此,我自然而然地開始在競賽作品中加入 HTML5 元素,以便進一步瞭解這些元素的運作方式,並執行先前 HTML 規格幾乎無法執行的操作。

在 HTML5 的眾多新功能中,對畫布標記的支援日益增加,讓我有機會使用 JavaScript 實作互動式藝術,進而嘗試實作現在稱為「Entanglement」的拼圖遊戲。我已經使用 Catan 版圖的背面建立原型,因此以此做為藍圖,在 HTML5 畫布上製作六邊形圖塊,以便在網路上播放,共有三個重要步驟:繪製六邊形、繪製路徑,以及旋轉圖塊。以下將詳細說明我如何以目前的形式完成這些項目。

繪製六邊形

在 Entanglement 的原始版本中,我使用了幾種畫布繪圖方法來繪製六邊形,但目前的遊戲形式使用 drawImage() 繪製從 Sprite 工作表剪輯的紋理。

資訊方塊 Sprite 工作表
資訊方塊 Sprite 工作表

我將圖片合併成單一檔案,因此只會向伺服器提出一個要求,而不是像本例中那樣提出十個要求。如要在畫布上繪製所選六邊形,我們必須先將工具收集在一起:畫布、內容和圖片。

如要建立畫布,只需在 HTML 文件中加入 canvas 標記即可,如下所示:

<canvas id="myCanvas"></canvas>

我會為其指定 ID,以便將其納入指令碼:

var cvs = document.getElementById('myCanvas');

其次,我們需要擷取畫布的 2D 情境,才能開始繪圖:

var ctx = cvs.getContext('2d');

最後,我們需要圖片。如果圖片名稱為「tiles.png」,且位於與網頁相同的資料夾中,我們可以透過以下方式取得圖片:

var img = new Image();
img.src = 'tiles.png';

有了這三個元件後,我們可以使用 ctx.drawImage() 從圖像片段工作表繪製所需的單一六邊形到畫布:

ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

在本例中,我們使用的是頂端左側第四個六邊形。此外,我們會將其繪製至左上角的畫布,並保持與原始圖片相同的大小。假設六邊形的寬度為 400 像素,高度為 346 像素,整體會像這樣:

var cvs = document.getElementById('myCanvas');
var ctx = cvs.getContext('2d');
var img = new Image();
img.src = 'tiles.png';
var sourceX = 1200;
var sourceY = 0;
var sourceWidth = 400;
var sourceHeight = 346;
var destinationX = 0;
var destinationY = 0;
var destinationWidth = 400;
var destinationHeight = 346;
ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

我們已成功將圖片的一部分複製到畫布,結果如下:

六角形圖塊
六邊形資訊方塊

繪製路徑

我們已將六邊形繪製到畫布上,現在要繪製幾條線條。首先,我們將查看六邊形圖塊的幾何圖形。我們希望每個邊有兩個線條端點,每個端點沿著每個邊緣的 1/4 處結束,且彼此相距 1/2 邊緣,如下所示:

六角形圖塊上的線條端點
六邊形圖塊上的線條端點

我們也希望曲線看起來很棒,因此經過一些嘗試與錯誤,我發現如果從每個端點的邊緣畫出垂直線,以六邊形的特定角度為準,從每個端點的兩個端點交叉處,可為指定端點建立不錯的貝茲控制點:

六角形圖塊上的控制點
六邊形圖塊上的控制點

接著,我們將端點和控制點對應至與畫布圖像相符的笛卡兒平面,並準備返回程式碼。為了簡化操作,我們會先從一行開始。我們將從左上方端點開始繪製路徑,一直到右下方端點。以先前的六邊形圖片為例,其尺寸為 400 x 346,因此上端端點的寬度為 150 像素,高度為 0 像素,簡寫為 (150, 0)。其控制點為 (150, 86)。底部邊緣端點為 (250, 346),控制點為 (250, 260):

第一個貝茲曲線的座標
第一個貝茲曲線的座標

有了座標,我們現在可以開始繪圖了。我們將重新開始使用 ctx.beginPath(),然後使用以下方式移至第一個端點:

ctx.moveTo(pointX1,pointY1);

接著,我們可以使用 ctx.bezierCurveTo() 繪製線條,如下所示:

ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);

由於我們希望線條有漂亮的邊框,因此我們會使用不同的寬度和顏色,對此路徑描邊兩次。系統會使用 ctx.strokeStyle 屬性設定顏色,並使用 ctx.lineWidth 設定寬度。總而言之,繪製第一行時會如下所示:

var pointX1 = 150;
var pointY1 = 0;
var controlX1 = 150;
var controlY1 = 86;
var controlX2 = 250;
var controlY2 = 260;
var pointX2 = 250;
var pointY2 = 346;
ctx.beginPath();
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

我們現在有一個六角形圖塊,其中第一條線會蜿蜒穿過:

六角形圖塊上的單一線條
六角形圖塊上的孤立線條

輸入其他 10 個端點的座標以及對應的 Bezier 曲線控制點,我們可以重複上述步驟,並建立類似以下的圖塊:

完成的六角形圖塊。
已完成的六邊形圖塊

旋轉畫布

有了方塊之後,我們希望能夠旋轉方塊,讓遊戲中出現不同的路徑。為了使用畫布完成這項作業,我們會使用 ctx.translate()ctx.rotate()。我們希望圖塊以其中心旋轉,因此第一步是將畫布參考點移至六邊形圖塊的中心。我們會使用以下方式:

ctx.translate(originX, originY);

其中 originX 是六角形圖塊寬度的一半,originY 則是高度的一半,因此我們會得到:

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);

我們現在可以使用新的中心點旋轉資訊方塊。由於六邊形有六個邊,我們會將其旋轉 Math.PI 除以 3 的倍數。我們會簡化操作,以順時針方向轉動一次,使用以下方法:

ctx.rotate(Math.PI / 3);

不過,由於六邊形和線條使用舊的 (0,0) 座標做為原點,因此在旋轉完成後,我們會在繪製前將座標轉換回來。因此,我們現在總共擁有:

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);
ctx.rotate(Math.PI / 3);
ctx.translate(-originX, -originY);

將上述轉譯和旋轉作業置於轉譯程式碼之前,即可轉譯旋轉的圖塊:

旋轉的六角形圖塊
旋轉的六邊形圖塊

摘要

上述內容強調了 HTML5 使用畫布標記提供的部分功能,包括算繪圖片、繪製貝茲曲線和旋轉畫布。使用 HTML5 畫布標記及其 JavaScript 繪圖工具處理 Entanglement 的體驗非常愉快,我期待其他人利用這項開放式新興技術,創造出許多新的應用程式和遊戲。

程式碼參考

以下將上述所有程式碼範例合併為一,供您參考:

var cvs = document.getElementById('myCanvas');
var ctx = cvs.getContext('2d');
var img = new Image();
img.src = 'tiles.png';

var originX = 200;
var originY = 173;
ctx.translate(originX, originY);
ctx.rotate(Math.PI / 3);
ctx.translate(-originX, -originY);

var sourceX = 1200;
var sourceY = 0;
var sourceWidth = 400;
var sourceHeight = 346;
var destinationX = 0;
var destinationY = 0;
var destinationWidth = 400;
var destinationHeight = 346;
ctx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight,
            destinationX, destinationY, destinationWidth, destinationHeight);

ctx.beginPath();
var pointX1 = 150;
var pointY1 = 0;
var controlX1 = 150;
var controlY1 = 86;
var controlX2 = 250;
var controlY2 = 260;
var pointX2 = 250;
var pointY2 = 346;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 250;
pointY1 = 0;
controlX1 = 250;
controlY1 = 86;
controlX2 = 150;
controlY2 = 86;
pointX2 = 75;
pointY2 = 43;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 150;
pointY1 = 346;
controlX1 = 150;
controlY1 = 260;
controlX2 = 300;
controlY2 = 173;
pointX2 = 375;
pointY2 = 213;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 325;
pointY1 = 43;
controlX1 = 250;
controlY1 = 86;
controlX2 = 300;
controlY2 = 173;
pointX2 = 375;
pointY2 = 130;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 25;
pointY1 = 130;
controlX1 = 100;
controlY1 = 173;
controlX2 = 100;
controlY2 = 173;
pointX2 = 25;
pointY2 = 213;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();

ctx.beginPath();
pointX1 = 325;
pointY1 = 303;
controlX1 = 250;
controlY1 = 260;
controlX2 = 150;
controlY2 = 260;
pointX2 = 75;
pointY2 = 303;
ctx.moveTo(pointX1, pointY1);
ctx.bezierCurveTo(controlX1, controlY1, controlX2, controlY2, pointX2, pointY2);
ctx.lineWidth = 15;
ctx.strokeStyle = '#ffffff';
ctx.stroke();
ctx.lineWidth = 10;
ctx.strokeStyle = '#786c44';
ctx.stroke();