個案研究 - 深入探索 HTML5 Canvas

Derek Detweiler
Derek Detweiler

簡介

在過去的春季 (2010 年),我特別關注 HTML5 和相關技術快速增加的支援。當時,有位好友和我在為期兩週的遊戲開發競賽中不斷挑戰彼此,藉此精進我們的程式設計和開發技能,並實現我們一直在彼此不斷挑戰的遊戲想法。因此,我自然就開始將 HTML5 元素融入競爭項目中,以進一步瞭解其運作方式,並且能夠使用舊版 HTML 規格幾乎不可能達成的工作。

在 HTML5 的眾多新功能中,隨著對畫布標記的支援功能日益增加,我是個大好機會,可以使用 JavaScript 實作互動式藝術,這促使我嘗試實作名為 Entanglement 的益智遊戲。我已經使用 Catan 圖塊的後端來建立原型,因此將此項目做為排序藍圖使用,在設計網頁遊用 HTML5 畫布上的十六進位圖塊前,必需有三大要素:繪製六邊形、繪製路徑及旋轉圖塊。以下說明我如何以目前的形式完成這些工作。

繪製六邊形

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

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

我將多張圖片合併成單一檔案,因此只會對伺服器發出一項要求 而不是傳送 10 個要求如要在畫布上繪製選定的六邊形,首先必須一併收集畫布、背景資料和圖片等工具。

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

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

請提供 ID,以便我們將該 ID 導入指令碼:

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

第二,我們需要擷取畫布的 2D 內容,以便開始繪製:

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

最後我們需要圖片。如果該資料夾的名稱是「tiles.png」,我們可透過下列方式取得:

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

現在您已擁有三個元件,我們可以使用 ctx.drawImage() 繪製要從 Sprite 工作表到畫布的單一六邊形:

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 間隔,例如:

六角動態磚上的線條端點
六角圖塊上的線條端點

我們也希望建立漂亮的曲線,因此,透過稍微試驗和錯誤的方式,我發現如果從每個端點的邊緣建立垂直線,每對端點以指定角度為六角的交集,就能為給定端點建立更佳的細長控制點:

六角動態磚上的控制點
控制六角圖塊上的點

現在,我們將端點和控制點對應至與畫布圖片相對應的笛卡兒平面,就可以回到程式碼了。為保持簡單,我們會從一行開始著手。首先,繪製一個從左上角端點到右下角的路徑我們先前的六邊形映像檔為 400x346,因此會讓我們的頂部端點達到 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 個端點及對應的白比曲線控制點,我們可以重複上述步驟,可能會建立類似下方的資訊方塊:

已完成的六角形動態磚。
已完成的六角形圖塊

旋轉畫布

取得資訊方塊後,我們希望能交替使用不同路徑,在遊戲中接受不同路徑。如要使用畫布完成此操作,我們會使用 ctx.translate()ctx.rotate()。我們希望資訊方塊能從中心旋轉,因此第一步是將畫布參照點移至六角形圖塊的中心。我們使用:

ctx.translate(originX, originY);

其中 originX 將為六邊形圖塊寬度的一半,原點 Y 就是高度的一半,讓我們能瞭解:

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 畫布標記和針對 Entanglement 使用的 JavaScript 繪圖工具,提供令人滿意的體驗,而且我很期待其他人運用這種開放性新興技術製作許多新的應用程式和遊戲。

程式碼參考

以下彙整了上述所有程式碼範例,方便您參考:

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();