案例研究 - 探秘 HTML5 画布

Derek Detweiler
Derek Detweiler

简介

去年春季(2010 年),我对 HTML5 及相关技术的支持力度非常大。当时,我和我的一个朋友在为期两周的游戏开发竞赛中相互挑战,以磨练我们的编程和开发技能,并将我们不断提出的游戏创意变为现实。于是,我自然而然地开始将 HTML5 元素融入我的参赛作品中,以便更好地了解这些元素的运作方式,并能够用早期的 HTML 规范做一些几乎不可能完成的事情。

在 HTML5 的众多新功能中,对画布标记的支持不断增加,这为我提供了使用 JavaScript 实现互动式艺术效果的绝佳机会,这促使我尝试实现一款目前名为 Entanglement 的益智游戏。我已经使用 Catan 图块的后部创建了一个原型,因此将此原型用作某种蓝图,要在用于网络游戏的 HTML5 画布上设计六边形图块,需要具备三个基本部分:绘制六边形、绘制路径和旋转图块。 下文将详细介绍我以目前的形式实现上述各项的途径。

绘制六边形

在 Entanglement 的原始版本中,我使用了几种画布绘制方法来绘制六边形,但当前的游戏形式使用 drawImage() 绘制从精灵表裁剪的纹理。

Tiles Sprite 工作表
图块雪碧图

我将这些图片合并成了一个文件,这样它就是一个向服务器发出的请求,而不是在本例中为 10 个。要在画布上绘制选定的六边形,我们首先必须将以下工具集中到一起:画布、上下文和图片。

要创建画布,我们只需在 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/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);

由于我们希望线条有漂亮的边框,因此我们将每次使用不同的宽度和颜色描边路径两次。设置颜色时需使用 gradle.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 将是六边形图块宽度的一半,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();