Nghiên cứu điển hình – Gặp vấn đề với Canvas HTML5

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 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á các ý tưởng trò chơi mà chúng tôi liên tục đưa ra cho nhau. Vì vậy, tôi bắt đầu tích hợp các phần tử HTML5 vào các bài dự thi của mình để hiểu rõ hơn về cách hoạt động của các phần tử này và có thể làm những việc gần như không thể bằng cách sử dụng các thông số HTML trước đó.

Trong số nhiều tính năng mới trong HTML5, việc hỗ trợ ngày càng tăng cho 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. Điều này đã giúp tôi thử triển khai một trò chơi đố vui hiện có tên là Entanglement (Mối quan hệ phức tạp). Tôi đã tạo một nguyên mẫu bằng cách sử dụng mặt sau của thẻ Settlers of Catan, vì vậy, hãy sử dụng nguyên mẫu này làm bản thiết kế, có ba phần thiết yếu để tạo thẻ hình lục giác trên canvas HTML5 để chơi trên web: vẽ hình lục giác, vẽ đường dẫn và xoay thẻ. Phần sau đây trình bày chi tiết cách tôi hoàn thành từng mục trong số 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 hình thức hiện tại của trò chơi sử dụng drawImage() để vẽ hoạ tiết được cắt từ một trang ảnh động.

Tấm sprite của thẻ thông tin
Tấm sprite của Thẻ thông tin

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

Để tạo một canvas, tất cả những gì chúng ta cần là thẻ canvas trong tài liệu html như sau:

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

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

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

Thứ hai, chúng ta cần lấy ngữ 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 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ó ba thành phần, chúng ta có thể sử dụng ctx.drawImage() để vẽ hình lục giác duy nhất mà chúng ta muốn từ trang tính ảnh động sang 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 trên hàng trên cùng. Ngoài ra, chúng ta sẽ vẽ hình ảnh này lên canvas ở góc trên cùng bên trái, giữ nguyên kích thước như hình ảnh 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 thể 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 ta đã sao chép thành công một phần hình ảnh vào canvas với kết quả như sau:

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

Vẽ đường dẫn

Bây giờ, chúng ta đã vẽ hình lục giác lê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 có hai đầu dòng ở mỗi bên, mỗi đầu dòng cách đầu bên cạnh 1/4 và cách đầu bên kia 1/2, như sau:

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

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

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

Bây giờ, chúng ta ánh xạ cả các điểm cuối và điểm điều khiển đến một mặt phẳng Descartes tương ứng với hình ảnh canvas và chúng ta đã sẵn sàng quay lại 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 hình lục giác trước đó là 400x346, điểm cuối trên cùng của chúng ta sẽ có chiều rộng 150 pixel và chiều cao 0 pixel, viết tắt là (150, 0). Điểm điều khiển của đường này sẽ là (150, 86). Điểm cuối của cạnh dưới cùng là (250, 346) với điểm điều khiển là (250, 260):

Toạ độ cho đường cong bezier đầu tiên
Toạ độ cho đường cong bezier đầu tiên

Giờ đây, khi đã có toạ độ, chúng ta đã sẵn sàng để bắt đầu vẽ. Chúng ta sẽ bắt đầu lại với ctx.beginPath() rồi 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 kẻ bằng cách sử dụng ctx.bezierCurveTo() như sau:

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

Vì muốn đường có đường viền đẹp, 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 sắc khác nhau mỗi lần. Màu sắc sẽ được đặt bằng cách sử dụng thuộc tính ctx.strokeStyle và chiều rộng sẽ được đặt bằng cách sử dụng ctx.lineWidth. Tổng cộng, 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 thẻ thông tin hình lục giác với dòng đầu tiên uốn lượn:

Dòng đơn trên thẻ thông tin hình lục giác
Dòng đơn trên thẻ thông tin hình lục giác

Nhập toạ độ cho 10 điểm cuối khác cũng như các điểm điều khiển tương ứng của đường cong bezier, 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:

Thẻ thông tin hình lục giác đã hoàn tất.
Thẻ thông tin hình lục giác đã hoàn thành

Xoay Canvas

Sau khi có thẻ thông tin, chúng ta muốn có thể xoay thẻ đó để có thể đi theo nhiều đường dẫn trong trò chơi. Để thực hiện việc này bằng canvas, chúng ta sử dụng ctx.translate()ctx.rotate(). Chúng ta muốn thẻ thông tin xoay quanh tâm của thẻ thông tin, vì vậy, bước đầu tiên là di chuyển điểm tham chiếu canvas đến tâm của thẻ thông tin hình lục giác. Để thực hiện việc này, chúng ta 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, cho chúng ta:

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 tâm mới. Vì lục giác có 6 cạnh, nên chúng ta sẽ xoay lục giác đó theo một số bội số của Math.PI chia cho 3. Chúng ta sẽ đơn giản hoá và chỉ 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ạ độ (0,0) cũ làm gốc, nên sau khi xoay xong, chúng ta sẽ muốn dịch lại trước khi vẽ. Vậy là tổng cộng chúng ta có:

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

Việc đặt phép dịch và xoay ở trên trước mã kết xuất sẽ khiến mã này hiện kết xuất thẻ thông tin đã xoay:

Thẻ thông tin 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 đã nêu 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 thẻ này cho Entanglement đã mang lại trải nghiệm thú vị. Tôi rất mong chờ nhiều ứng dụng và trò chơi mới mà mọi người tạo ra bằng công nghệ mới mẻ và cởi mở này.

Tài liệu tham khảo mã

Tất cả ví dụ về mã được cung cấp ở trên được kết hợp dưới đây để 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();