65.9K
CodeProject 正在变化。 阅读更多。
Home

算法: 计算凸包并绘制 HTML5 Canvas( 第 3 部分, 共 N 部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (4投票s)

2016年4月27日

CPOL

9分钟阅读

viewsIcon

13398

downloadIcon

318

我们学习如何生成随机点(很简单),并启用了用户可以在网格上抓取任意点并实时移动它的功能(见动画 GIF)。

引言

完成本文后,TrapPoints_v008.zip 中的最终代码将允许您移动点并在 Canvas 上实时重绘它们。

move points in real time

本文继续我们在 CP 上发布的两篇前文章中所做的工作。

算法:计算凸包和绘制 HTML5 Canvas(第 1 部分,共 2 部分)[^]

算法:计算凸包和绘制 HTML5 Canvas(第 2 部分,共 N 部分)[^]

本文将涵盖的内容

  1. 添加一个按钮和功能以创建随机生成的点
  2. 实现用户可以移动点——这似乎是一个有趣的挑战

注意:好的,我将开始讲解凸包算法。但是,所有这些绘图工作让我有点分心。:)

查看实时版本

尝试包含这些更新的实时版本,网址为:
  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() 方法。该方法会清除我们的 allPointsselectedTriangle 点数组,因为这些点将不再有效。

之后,我们对要创建的每个点循环执行一段代码。

在循环中,我们使用内置的 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,因为我们不再需要该值,如果用户再次单击,它将再次被设置。

现在,代码变得非常有趣且非常简单。之所以代码非常简单,是因为我们拥有一套由函数组成的框架。

函数框架支持我们的工作

唯一实现的另外的代码是 onMouseMoveonMouseUp 方法,它们非常简单。

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[] 数组中点的原始索引设置为这个新点值。这会更新它的 xy 值。之后,我们调用 draw()该方法每次鼠标移动时都会绘制背景和 allPoints[] 数组中的所有点。这有效地使得该点看起来像是在网格上移动。

由于我们之前的每个方法都写得很紧凑,所以我们可以在不破坏之前所做工作的情况下添加这项工作。

本次就到这里。

继续学习,继续编码。

历史

本文及包含的两个代码版本(第 7 步和第 8 步)的第一个版本:2016 年 4 月 27 日

© . All rights reserved.