算法: 计算凸包并绘制 HTML5 Canvas( 第 3 部分, 共 N 部分)
我们学习如何生成随机点(很简单),并启用了用户可以在网格上抓取任意点并实时移动它的功能(见动画 GIF)。
引言
完成本文后,TrapPoints_v008.zip 中的最终代码将允许您移动点并在 Canvas 上实时重绘它们。
本文继续我们在 CP 上发布的两篇前文章中所做的工作。
算法:计算凸包和绘制 HTML5 Canvas(第 1 部分,共 2 部分)[^]
算法:计算凸包和绘制 HTML5 Canvas(第 2 部分,共 N 部分)[^]
本文将涵盖的内容
- 添加一个按钮和功能以创建随机生成的点
- 实现用户可以移动点——这似乎是一个有趣的挑战
注意:好的,我将开始讲解凸包算法。但是,所有这些绘图工作让我有点分心。:)
查看实时版本
尝试包含这些更新的实时版本,网址为:
http://raddev.us/TrapPoints[^]
背景
本文中完成的所有工作都受到了我阅读本书的启发: Amazon.com:算法精粹(英文名:Algorithms in a Nutshell: A Practical Guide)(9781491948927):George T. Heineman、Gary Pollice、Stanley Selkow:书籍[^]
这是一本非常有趣的书,我只读到第一个算法,但它相当易读。
HTML5 Canvas
我想找到一种好的方法来研究如何解决这个算法,而 HTML5 Canvas 为我提供了最普遍的图形绘制功能,这些功能都有很好的文档记录,并且
第 7 步开始
添加点自动生成
您可以通过下载本文顶部的 TrapPoints_v007.zip 来查看已完成的代码。
这是一项非常简单的任务。首先,我们添加一个新的 Bootstrap 风格的按钮,以便我们可以运行该功能。
HTML 将如下所示:
<button onclick="generateRandomPoints(50);" class="btn btn-primary">Generate Points</button>
我设置了 onclick 事件来调用一个名为 generateRandomPoints()
的方法,该方法接受我们想要生成的点数(在本例中为 50)。
generateRandomPoints()
这个方法实现起来非常简单。
function generateRandomPoints(pointCount){
clearPoints(); // empty out both arrays
for (var i = 0; i < pointCount;i++){
var X = Math.floor(Math.random() * CANVAS_SIZE); // gen number 0 to 649
var Y = Math.floor(Math.random() * CANVAS_SIZE); // gen number 0 to 649
var p = {};
p.x = X;
p.y = Y;
allPoints.push(p);
}
draw();
}
首先,我们调用上一篇文章中编写的 clearPoints()
方法。该方法会清除我们的 allPoints
和 selectedTriangle
点数组,因为这些点将不再有效。
之后,我们对要创建的每个点循环执行一段代码。
在循环中,我们使用内置的 JavaScript 方法 Math.random()
生成一个随机值。Math.random()
生成一个双精度值 Z,其中 1 > Z >= 0。
我在 trappoints.js 的顶部添加了一个新的常量*,看起来像这样:var CANVAS_SIZE = 650;
由于此值代表 Canvas 的最大高度和宽度,因此我永远不想超过这些值。
为点生成 X 和 Y 值
另外,请注意,我们在生成值时还实现了另一个内置的 JavaScript 方法 (Math.floor())。
Mozilla Developer Network 文档描述该方法为:
引用
Math.floor()
函数返回小于或等于给定数字的最大整数。
这可以确保值始终为整数,因为它只是截断任何小数部分(不进行四舍五入)。
*JavaScript 不支持所有浏览器中的 const 关键字,所以我只是创建了一个 var 并称之为 const。
我们使用此随机方法生成两个不同的值(一个用于 x 值,一个用于 y 值)。
我们将每个值赋给一个局部变量,用这两个值创建一个局部点对象,然后将新点推送到 allPoints
数组。循环运行,每次生成一个点。
最后,当所有点都添加到 allPoints
后,我们调用一次 draw()
方法,该方法将它们全部绘制到 Canvas 上。
第 8 步开始
您可以通过下载本文顶部的 TrapPoints_v008.zip 来查看已完成的代码。
允许用户移动一个点
现在,我们想允许用户移动一个点。这让我很感兴趣,因为我预期的最终效果会很酷,而且我认为这不会太难。
使用 Ctrl 键
由于我使用了 Shift 键来允许用户为选定的三角形突出显示点,因此我现在将使用 Ctrl 键来指示用户正在移动一个点。
添加代码非常简单。我们只需要进入 mouseDownHandler() 函数,并在 if...else... 语句中添加以下代码。
else if (event.ctrlKey){
console.log("control key is pressed...");
}
如果添加了这些代码并再次运行,您会看到,如果按住 Ctrl 键并在网格上的任意位置单击,则不会绘制任何点。相反,只会向 Web 浏览器控制台窗口输出一条消息。
稍后我们将填写代码来完成这项工作。
在用户拖动 Canvas 上的点时重绘该点
最酷的功能是让用户看到正在拖动的点在 Canvas 上移动,因此我希望用户正在移动的点持续重绘,直到他松开鼠标按钮。
为了完成这项工作,我们需要添加一个 mouseMove 处理程序。我们现在就来做。
回到第一篇文章中编写的 initApp()
方法,并添加一行(在下面的代码中粗体显示)
function initApp()
{
theCanvas = document.getElementById("gamescreen");
ctx = theCanvas.getContext("2d");
ctx.canvas.height = CANVAS_SIZE;
ctx.canvas.width = ctx.canvas.height;
window.addEventListener("mousedown", mouseDownHandler);
window.addEventListener("mousemove", mouseMoveHandler);
initBoard();
}
这样,我们就注册了一个新方法(我们将编写其实现),名为 mouseMoveHandler
。
点移动工作原理摘要
这是我关于这一切如何工作的想法。如果用户按住 Ctrl 键并单击 Canvas 上的一个位置,那么我们将对用户单击的点进行命中测试。这基本上与用户突出显示选定三角形的点时运行的代码相同(请参阅第二篇文章中的更多内容)。
如果命中了一个点,这次我们将做一些稍微不同的事情。以前,我们只是返回一个具有相同坐标的点,然后我们在原始点上方绘制一个高亮的(红色)圆圈。这效果很好。
移动一个点意味着重绘网格和其他所有点
然而,现在我们要绘制(擦除)我们正在移动的点,然后每次移动它时都重新绘制它。这意味着我们实际上必须重绘背景和其他所有点。我们知道 draw() 方法现在可以做到这一点,所以这会有帮助。
我们只希望在用户捕获了一个点(Ctrl 键按下且命中测试返回一个点)时才执行此操作。另外,当用户松开鼠标按钮(mouseup)时,我们需要停止进行这项工作。这意味着我们还需要另一个需要通知的鼠标事件处理程序,所以让我们也在 initApp()
方法中添加它。目前,它看起来就像这样简单的一行。
window.addEventListener("mouseup", mouseUpHandler);
mouseDownHandler()
中新 else if {} 子句中的代码将非常少,所以我现在将向您展示最终版本。
else if (event.ctrlKey){
isCtrlKeyPressed = true;
hitTest(currentPoint);
}
我创建了一个名为 isCtrlKeyPressed
的新全局标志,作为确定我们是否正在处理此点移动代码的简单方法。我已将该变量添加到 trapponts.js
的顶部,并默认设置为 false。
当用户按下 Ctrl 键并单击鼠标时,我将其设置为 true,然后调用 hitTest()
方法来确定我们是否命中了一个网格上的点。在这种情况下,我们不关心返回的点,因为当按下 Ctrl 键时,mouseMoveHandler
将接管并执行一些工作。
修改 hitTest() 方法
为了完成这个移动点的操作,我需要稍微修改一下 hitTest() 方法。这是修改代码以完成我们工作的最简单方法。首先,我将向您展示代码,然后解释它的作用。
function hitTest(p){
// iterate through all points
for (var x = 0;x<allPoints.length;x++){
if ((Math.abs(p.x - allPoints[x].x) <= RADIUS) && Math.abs(p.y - allPoints[x].y) <=RADIUS){
console.log("It's a hit..." + allPoints[x]);
if (isCtrlKeyPressed){
capturedIndex = x;
selectedTriangle = [];
isCtrlKeyPressed = false;
}
return allPoints[x];
}
}
}
我添加了前面代码示例中粗体显示的 if 语句。在以前的版本中,我只是返回命中的点。现在,我将一个名为 capturedIndex
的全局变量设置为在 allPoints[]
数组中找到的点当前所在的索引。这是一种提供此方法外部值的简单方法,也是一种小小的“作弊”。
重置选定的三角形
您可以看到,我还将 selectedTriangle
设置为 null,这样所有红色的高亮显示的点都将消失。我也不想处理更新选定的三角形,所以我只是重置了它。我可以稍后完成这项工作,现在这样就足够了。用户重新选择三个点并不是什么大事。
之后,我将 isCtrlKeyPressed
设置回 false,因为我们不再需要该值,如果用户再次单击,它将再次被设置。
现在,代码变得非常有趣且非常简单。之所以代码非常简单,是因为我们拥有一套由函数组成的框架。
函数框架支持我们的工作
唯一实现的另外的代码是 onMouseMove
和 onMouseUp
方法,它们非常简单。
onMouseUp
只是将 selectedIndex = null
设置为表示没有选中任何内容,并且 mouseMove 不需要处理,因为用户没有选择要移动的任何点。
整个方法如下所示:
function mouseUpHandler(event){
capturedIndex = null;
}
mouseMoveHandler 非常简单
由于我们在此之前所做的工作,mouseMoveHandler 比我预期的要简单得多。
function mouseMoveHandler(event){
if (capturedIndex !== null){
allPoints[capturedIndex] = getMousePos(event);
draw();
}
}
我们所要做的就是检查 capturedIndex
值是否不为 null。如果不是,我们就知道在 hitTest()
方法中捕获了一个点。
然后,我们通过调用 getMousePos()
(每次鼠标移动时都会更新)来获取当前鼠标位置,并将 allPoints[]
数组中点的原始索引设置为这个新点值。这会更新它的 x
和 y
值。之后,我们调用 draw()
,该方法每次鼠标移动时都会绘制背景和 allPoints[]
数组中的所有点。这有效地使得该点看起来像是在网格上移动。
由于我们之前的每个方法都写得很紧凑,所以我们可以在不破坏之前所做工作的情况下添加这项工作。
本次就到这里。
继续学习,继续编码。
历史
本文及包含的两个代码版本(第 7 步和第 8 步)的第一个版本:2016 年 4 月 27 日