Nghiên cứu điển hình – Sử dụng HTML5 Canvas

Derek Detweiler
Derek Detweiler

Giới thiệu

Mùa xuân vừa qua (2010) tôi quan tâm đến việc hỗ trợ ngày càng tăng nhanh chóng cho HTML5 và các công nghệ liên quan. Vào thời điểm đó, tôi và một người bạn đã thách đấu nhau trong các cuộc thi phát triển trò chơi kéo dài hai tuần để trau dồi kỹ năng lập trình và phát triển cũng như hiện thực hoá những ý tưởng trò chơi mà chúng tôi liên tục thảo luận với nhau. Vì vậy, tôi tự nhiên đã bắt đầu kết hợp phần tử HTML5 vào các mục nhập cạnh tranh của mình để hiểu rõ hơn về cách các phần tử này hoạt động và có thể làm những việc gần như không thể khi sử dụng thông số HTML trước đó.

Trong số nhiều tính năng mới trong HTML5, việc ngày càng hỗ trợ thẻ canvas mang đến cho tôi một cơ hội thú vị để triển khai nghệ thuật tương tác bằng JavaScript, khiến tôi thử triển khai một trò chơi giải đố hiện có tên là Entanglement. Tôi đã tạo một nguyên mẫu bằng cách sử dụng mặt sau của các thẻ thông tin Settlers của Catan. Vì vậy, khi sử dụng mẫu này làm thiết kế chi tiết, có 3 phần thiết yếu để tạo hình thẻ thông tin lục giác trên canvas HTML5 khi chơi trên web: vẽ hình lục giác, vẽ đường dẫn và xoay thẻ thông tin. Phần sau đây sẽ trình bày chi tiết về cách tôi đã thực hiện từng yếu tố này ở dạng hiện tại.

Vẽ hình lục giác

Trong phiên bản gốc của Entanglement, tôi đã sử dụng một số phương thức vẽ canvas để vẽ hình lục giác, nhưng dạng hiện tại của trò chơi sử dụng drawImage() để vẽ hoạ tiết được cắt từ một tấm sprite.

Trang tính sprite
Trang tính sprite

Tôi đã kết hợp các hình ảnh với nhau thành một tệp duy nhất nên chỉ có một yêu cầu đối với máy chủ thay vì, trong trường hợp này là mười. Để vẽ hình lục giác đã chọn lên canvas, trước tiên, chúng tôi phải tập hợp các công cụ lại: canvas, bối cảnh và hình ảnh.

Để tạo canvas, chúng ta chỉ cần thẻ canvas trong tài liệu html như sau:

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

Tôi cung cấp cho mã đó một mã nhận dạng để chúng ta có thể đưa vào tập lệnh của mình:

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

Thứ hai, chúng ta cần lấy bối cảnh 2D cho canvas để có thể bắt đầu vẽ:

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

Cuối cùng, chúng ta cần hình ảnh. Nếu tệp này có tên là "tiles.png" trong cùng một thư mục với trang web, chúng ta có thể lấy tệp bằng cách:

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

Giờ đây, khi đã có 3 thành phần, chúng ta có thể sử dụng ctx.drawImage() để vẽ một hình lục giác mà chúng ta muốn từ bảng sprite vào canvas:

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

Trong trường hợp này, chúng ta sẽ sử dụng hình lục giác thứ tư từ bên trái ở hàng trên cùng. Ngoài ra, chúng ta sẽ vẽ bản vẽ này vào canvas ở góc trên cùng bên trái, giữ nguyên kích thước như bản gốc. Giả sử các hình lục giác có chiều rộng 400 pixel và chiều cao 346 pixel, tổng cộng sẽ có dạng như sau:

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

Chúng tôi đã sao chép thành công một phần của hình ảnh vào canvas với kết quả như sau:

Ô hình lục giác
Thẻ thông tin dạng lục giác

Vẽ đường dẫn

Bây giờ, chúng ta đã vẽ hình lục giác trên canvas, chúng ta muốn vẽ một vài đường trên đó. Trước tiên, chúng ta sẽ xem xét một số hình học liên quan đến thẻ thông tin hình lục giác. Chúng ta muốn 2 đường kết thúc ở mỗi cạnh, trong đó mỗi đường kết thúc là 1/4 từ các đầu dọc theo mỗi cạnh và 1/2 cạnh cách nhau 1/2 cạnh nhau, như sau:

Điểm cuối của đường kẻ trên ô hình lục giác
Điểm cuối của đường kẻ trên ô lục giác

Chúng tôi cũng muốn có một đường cong đẹp mắt, vì vậy, sử dụng phương pháp thử và sai một chút, tôi thấy rằng, nếu tạo một đường vuông góc từ cạnh ở mỗi điểm cuối, giao điểm từ mỗi cặp điểm cuối xung quanh một góc nhất định của hình lục giác sẽ tạo thành một điểm điều khiển bezier đẹp hơn cho các điểm cuối đã cho:

Các điểm điều khiển trên ô hình lục giác
Điểm điều khiển trên ô lục giác

Bây giờ, chúng ta ánh xạ cả các điểm cuối và các điểm điều khiển đến một mặt phẳng Descartes tương ứng với ảnh canvas của chúng ta và đã sẵn sàng quay lại đoạn mã. Để đơn giản, chúng ta sẽ bắt đầu với một dòng. Chúng ta sẽ bắt đầu bằng cách vẽ một đường dẫn từ điểm cuối trên cùng bên trái đến điểm cuối dưới cùng bên phải. Với hình ảnh lục giác trước đó có kích thước 400 x 346, điểm cuối trên cùng của chúng tôi có chiều ngang 150 pixel và giảm 0 pixel (150, 0). Điểm điều khiển của nó sẽ là (150, 86). Điểm cuối cạnh dưới là (250, 346) với điểm điều khiển là (250, 260):

Toạ độ cho đường cong bezier thứ nhất
Toạ độ cho đường cong bezier đầu tiên

Với các toạ độ trong tay, giờ đây chúng ta đã sẵn sàng để bắt đầu vẽ. Chúng tôi sẽ bắt đầu từ ctx.beginPath(), sau đó chuyển đến điểm cuối đầu tiên bằng cách sử dụng:

ctx.moveTo(pointX1,pointY1);

Sau đó chúng ta có thể tự vẽ đường đó bằng cách sử dụng ctx.bezierCurveTo() như sau:

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

Vì muốn đường kẻ có đường viền đẹp mắt, chúng ta sẽ vẽ đường dẫn này hai lần bằng cách sử dụng chiều rộng và màu khác nhau. Màu sắc sẽ được đặt bằng thuộc tính ctx.strokeStyle và chiều rộng sẽ được đặt bằng ctx.lineWidth. Nhìn chung, việc vẽ dòng đầu tiên sẽ có dạng như sau:

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

Bây giờ, chúng ta có một ô hình lục giác với dòng đầu tiên uốn khúc ngang qua:

Một đường đơn trên ô hình lục giác
Một đường đơn trên thẻ thông tin hình lục giác

Khi nhập toạ độ cho 10 điểm cuối khác cũng như các điểm điều khiển đường cong bezier tương ứng, chúng ta có thể lặp lại các bước ở trên và có thể tạo một thẻ thông tin như sau:

Đã hoàn tất ô hình lục giác.
Ô hình lục giác đã hoàn chỉnh

Xoay Canvas

Sau khi tạo thẻ thông tin, chúng ta muốn có thể xoay thẻ thông tin đó để có thể đi đến các đường dẫn khác nhau trong trò chơi. Để thực hiện việc này bằng canvas, chúng ta sẽ sử dụng ctx.translate()ctx.rotate(). Chúng ta muốn thẻ thông tin xoay quanh tâm, vì vậy, bước đầu tiên là di chuyển điểm tham chiếu canvas đến giữa thẻ thông tin lục giác. Để làm việc này, chúng tôi sử dụng:

ctx.translate(originX, originY);

Trong đó originX sẽ bằng một nửa chiều rộng của thẻ thông tin hình lục giác và originY sẽ bằng một nửa chiều cao, kết quả:

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

Bây giờ, chúng ta có thể xoay thẻ thông tin bằng điểm giữa mới. Vì một hình lục giác có sáu cạnh, nên chúng ta sẽ muốn xoay hình lục giác đó theo bội số của một số Math.PI chia cho 3. Chúng ta sẽ đơn giản hoá và thực hiện một vòng theo chiều kim đồng hồ bằng cách sử dụng:

ctx.rotate(Math.PI / 3);

Tuy nhiên, vì hình lục giác và các đường của chúng ta đang sử dụng toạ độ cũ (0,0) làm gốc, nên sau khi xoay xong, chúng ta sẽ muốn dịch ngược lại trước khi vẽ. Như vậy, nói chung, chúng ta hiện có:

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

Khi dịch và xoay ở trên trước mã kết xuất của chúng ta, mã kết xuất sẽ kết xuất thẻ thông tin được xoay:

Ô hình lục giác đã xoay
Thẻ thông tin hình lục giác xoay

Tóm tắt

Ở trên, tôi đã làm nổi bật một số tính năng mà HTML5 cung cấp bằng cách sử dụng thẻ canvas, bao gồm kết xuất hình ảnh, vẽ đường cong bezier và xoay canvas. Việc sử dụng thẻ canvas HTML5 và các công cụ vẽ JavaScript của nó cho Entanglement đã chứng minh đây là một trải nghiệm thú vị và tôi rất mong chờ nhiều ứng dụng và trò chơi mới mà những người khác tạo ra bằng công nghệ mở và mới nổi này.

Tham chiếu mã

Tất cả mã ví dụ được cung cấp ở trên được kết hợp dưới đây dưới dạng tài liệu tham khảo:

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