简介
去年春天(2010 年),我对 HTML5 及相关技术的支持日益增加这一点产生了兴趣。当时,我和一位朋友一直在进行为期两周的游戏开发竞赛,互相挑战,以磨练我们的编程和开发技能,并将我们不断抛出的游戏想法变为现实。因此,我自然而然地开始在比赛作品中加入 HTML5 元素,以便更好地了解它们的运作方式,并能够执行使用早期 HTML 规范几乎不可能执行的操作。
在 HTML5 的众多新功能中,对画布标记的支持越来越多,这让我有机会使用 JavaScript 实现互动艺术,这促使我尝试实现一款名为 Entanglement 的拼图游戏。我已经使用《海盗 Catan》功能块的背面创建了一个原型,因此,将其用作蓝图,在 HTML5 画布上制作用于 Web 游戏的六边形功能块有三个基本部分:绘制六边形、绘制路径和旋转功能块。以下内容详细介绍了我如何以当前形式实现这些功能。
绘制六边形
在《纠缠》的原始版本中,我使用了多种画布绘制方法来绘制六边形,但游戏的当前形式使用 drawImage()
来绘制从精灵图片中剪裁的纹理。

我将这些图片合并到一个文件中,这样只会向服务器发出一个请求,而不是 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/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 将是六边形图块宽度的一半,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 Canvas 标记及其 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();