【问题标题】:JavaScript Point Collision with Regular HexagonJavaScript 点碰撞与正六边形
【发布时间】:2017-04-08 03:27:59
【问题描述】:

我正在制作一个基于 HTML5 画布六边形网格的系统,我需要能够检测单击画布时单击了网格中的哪些六边形图块。

几个小时的搜索和尝试我自己的方法一无所获,从其他语言移植实现简直让我感到困惑,以至于我的大脑变得迟钝。

网格由平顶正六边形组成,如下图所示:

本质上,给定一个点和此图像中指定的变量作为网格中每个六边形(R、W、S、H)的大小:

我需要能够确定一个点是否在给定的六边形内。

一个示例函数调用是pointInHexagon(hexX, hexY, R, W, S, H, pointX, pointY),其中 hexX 和 hexY 是六边形图块边界框左上角的坐标(如上图中的左上角)。

有没有人知道如何做到这一点?速度暂时不是什么大问题。

【问题讨论】:

  • 一个非数学解决方案:stackoverflow.com/questions/40509954/…
  • 以六边形的中心为轴心点,用角度函数表示到边界的距离。从 90 度(垂直)开始,每 60 度最小 -> R*Math.cos(Math.PI/6);,每 60+30 度最大 -> R。所以可能类似于 d <= Math.sin(angle * Math.PI * 6) * (R-R*Math.cos(Math.PI/6)) + R*Math.cos(Math.PI/6)。虽然我不能确定在这个晚上的这个时候。
  • @Kaiido 我想这会奏效。对于多边形遇到的任何其他碰撞问题,我会牢记这一点,但我正在寻找数学解决方案 xD
  • @Redu 我会看看那个。但是,变量代表什么?
  • 我无法确定公式,但它必须是它的变体。您必须计算出点击点到中心的angle和距离dd必须小于根据角度计算的数字。我明天去看看。

标签: javascript html canvas collision-detection hexagonal-tiles


【解决方案1】:

redblog 有完整的数学解释和工作示例。

主要思想是六边形的水平间距为六边形大小的 $3/4$,垂直方向仅为 $H$,但需要考虑列以考虑垂直偏移。通过在 1/4 W 切片上比较 x 和 y 来确定红色外壳。

【讨论】:

    【解决方案2】:

    我觉得你需要这样的东西~

    已编辑 我做了一些数学运算,给你。这不是一个完美的版本,但可能会对您有所帮助...

    啊,您只需要一个R 参数,因为您可以根据它计算HWS。从你的描述我是这么理解的。

    // setup canvas for demo
    var canvas = document.getElementById('canvas');
    canvas.width = 300;
    canvas.height = 275;
    var context = canvas.getContext('2d');
    var hexPath;
    var hex = {
      x: 50,
      y: 50,
      R: 100
    }
    
    // Place holders for mouse x,y position
    var mouseX = 0;
    var mouseY = 0;
    
    // Test for collision between an object and a point
    function pointInHexagon(target, pointX, pointY) {
      var side = Math.sqrt(target.R*target.R*3/4);
      
      var startX = target.x
      var baseX = startX + target.R / 2;
      var endX = target.x + 2 * target.R;
      var startY = target.y;
      var baseY = startY + side; 
      var endY = startY + 2 * side;
      var square = {
        x: startX,
        y: startY,
        side: 2*side
      }
    
      hexPath = new Path2D();
      hexPath.lineTo(baseX, startY);
      hexPath.lineTo(baseX + target.R, startY);
      hexPath.lineTo(endX, baseY);
      hexPath.lineTo(baseX + target.R, endY);
      hexPath.lineTo(baseX, endY);
      hexPath.lineTo(startX, baseY);
    
      if (pointX >= square.x && pointX <= (square.x + square.side) && pointY >= square.y && pointY <= (square.y + square.side)) {
        var auxX = (pointX < target.R / 2) ? pointX : (pointX > target.R * 3 / 2) ? pointX - target.R * 3 / 2 : target.R / 2;
        var auxY = (pointY <= square.side / 2) ? pointY : pointY - square.side / 2;
        var dPointX = auxX * auxX;
        var dPointY = auxY * auxY;
        var hypo = Math.sqrt(dPointX + dPointY);
        var cos = pointX / hypo;
    
        if (pointX < (target.x + target.R / 2)) {
          if (pointY <= (target.y + square.side / 2)) {
            if (pointX < (target.x + (target.R / 2 * cos))) return false;
          }
          if (pointY > (target.y + square.side / 2)) {
            if (pointX < (target.x + (target.R / 2 * cos))) return false;
          }
        }
    
        if (pointX > (target.x + target.R * 3 / 2)) {
          if (pointY <= (target.y + square.side / 2)) {
            if (pointX < (target.x + square.side - (target.R / 2 * cos))) return false;
          }
          if (pointY > (target.y + square.side / 2)) {
            if (pointX < (target.x + square.side - (target.R / 2 * cos))) return false;
          }
        }
        return true;
      }
      return false;
    }
    
    // Loop
    setInterval(onTimerTick, 33);
    
    // Render Loop
    function onTimerTick() {
      // Clear the canvas
      canvas.width = canvas.width;
    
      // see if a collision happened
      var collision = pointInHexagon(hex, mouseX, mouseY);
    
      // render out text
      context.fillStyle = "Blue";
      context.font = "18px sans-serif";
      context.fillText("Collision: " + collision + " | Mouse (" + mouseX + ", " + mouseY + ")", 10, 20);
    
      // render out square    
      context.fillStyle = collision ? "red" : "green";
      context.fill(hexPath);
    }
    
    // Update mouse position
    canvas.onmousemove = function(e) {
      mouseX = e.offsetX;
      mouseY = e.offsetY;
    }
    #canvas {
      border: 1px solid black;
    }
    &lt;canvas id="canvas"&gt;&lt;/canvas&gt;

    只需将pointInHexagon(hexX, hexY, R, W, S, H, pointX, pointY) 替换为var hover = ctx.isPointInPath(hexPath, x, y)

    var canvas = document.getElementById("canvas");
    var ctx = canvas.getContext("2d");
    
    var hexPath = new Path2D();
    hexPath.lineTo(25, 0);
    hexPath.lineTo(75, 0);
    hexPath.lineTo(100, 43);
    hexPath.lineTo(75, 86);
    hexPath.lineTo(25, 86);
    hexPath.lineTo(0, 43);
    
    
    function draw(hover) {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = hover ? 'blue' : 'red';
      ctx.fill(hexPath);
    }
    
    canvas.onmousemove = function(e) {
      var x = e.clientX - canvas.offsetLeft, y = e.clientY - canvas.offsetTop;
      var hover = ctx.isPointInPath(hexPath, x, y)
      draw(hover)
    };
    draw();
    &lt;canvas id="canvas"&gt;&lt;/canvas&gt;

    【讨论】:

    • 这是一个很好的答案,但不幸的是它没有正确碰撞......看起来它正在做边界框碰撞检测,因为六边形的左侧和右侧没有注册为碰撞.
    【解决方案3】:

    我已经为您制作了一个解决方案,展示了解决此问题的三角形方法。

    http://codepen.io/spinvector/pen/gLROEp

    下面的数学:

    isPointInside(point)
    {
        // Point in triangle algorithm from http://totologic.blogspot.com.au/2014/01/accurate-point-in-triangle-test.html
        function pointInTriangle(x1, y1, x2, y2, x3, y3, x, y)
        {
            var denominator = ((y2 - y3)*(x1 - x3) + (x3 - x2)*(y1 - y3));
            var a = ((y2 - y3)*(x - x3) + (x3 - x2)*(y - y3)) / denominator;
            var b = ((y3 - y1)*(x - x3) + (x1 - x3)*(y - y3)) / denominator;
            var c = 1 - a - b;
    
            return 0 <= a && a <= 1 && 0 <= b && b <= 1 && 0 <= c && c <= 1;
        }
    
        // A Hex is composite of 6 trianges, lets do a point in triangle test for each one.
        // Step through our triangles
        for (var i = 0; i < 6; i++) {
            // check for point inside, if so, return true for this function;
            if(pointInTriangle( this.origin.x, this.origin.y,
                                this.points[i].x, this.points[i].y,
                                this.points[(i+1)%6].x, this.points[(i+1)%6].y,
                                point.x, point.y))
                return true;
        }
        // Point must be outside.
        return false;
    }
    

    【讨论】:

    • 鉴于六边形适合矩形,它还会创建 4 个三角形,它们位于矩形的角上并且在六边形之外。此代码能否测试该点是否位于这些外部三角形内?不同之处在于这四个三角形不是等边的,而六边形中的六个是等边的。
    • 如果您希望可点击区域是图表中给出的矩形,那么矩形检查点的情况要简单得多。
    • 是的,我在考虑你的算法的逆运算,即如果点击在矩形内并且在四个外部三角形之一内,那么点击不在六边形内;但是您检查六个三角形的解决方案可能不那么令人困惑。
    • @AndyMac 我首先对每个六边形进行边界框测试,然后我(想要)对边界框测试返回 true 的任何六边形进行更准确的测试。我想如果我的十六进制地图变得足够大,我只能将边界框检查应用于鼠标相对于屏幕上显示的部分的几个六边形宽度内的六边形。
    • @AndyMac 看起来你的算法是要走的路。我看到一个AS3的东西有类似的想法,但是代码完全不可读和无法解释。谢谢。
    【解决方案4】:

    简单快速的对角矩形切片。

    查看其他答案,我发现他们的问题有点过于复杂了。以下是比接受的答案快一个数量级,并且不需要任何复杂的数据结构、迭代器或生成死内存和不需要的 GC 命中。它返回 R、H、S 或 W 的任何相关集的十六进制单元格行和列。示例使用 R = 50。

    如果矩形被对角分割,部分问题是找到一个点在矩形的哪一边。这是一个非常简单的计算,通过对要测试的点的位置进行归一化来完成。

    对角切割任何矩形

    例如一个宽度为 w 且高度为 h 的矩形从左上角到右下角分割。查找一个点是左还是右。假设矩形的左上角在 rx,ry

    var x = ?;
    var y = ?;
    x = ((x - rx) % w) / w;
    y = ((y - ry) % h) / h;
    if (x > y) { 
        // point is in the upper right triangle
    } else if (x < y) {
        // point is in lower left triangle
    } else {
        // point is on the diagonal
    }
    

    如果你想改变对角线的方向,那么只需反转一个法线

    x = 1 - x;  // invert x or y to change the direction the rectangle is split
    if (x > y) { 
        // point is in the upper left triangle
    } else if (x < y) {
        // point is in lower right triangle
    } else {
        // point is on the diagonal
    }
    

    拆分成子单元格并使用%

    剩下的问题只是将网格分成 (R / 2) x (H / 2) 单元格,每个十六进制宽度覆盖 4 列和 2 行。 3 列中的每一列都有对角线。这些列的每一秒都有对角线翻转。对于每 6 列中的第 4、5 和 6 列,该行向下移动一个单元格。通过使用 % 您可以非常快速地确定您所在的十六进制单元格。使用上面的对角线分割方法可以让数学变得简单快捷。

    还有一点。返回参数 retPos 是可选的。如果你调用函数如下

    var retPos;
    mainLoop(){
        retPos = getHex(mouse.x, mouse.y, retPos);
    }
    

    代码不会导致GC命中,进一步提高速度。

    像素到十六进制坐标

    从问题图返回十六进制单元格x,y pos。请注意,此函数仅适用于0 &lt;= x0 &lt;= y 范围内,如果您需要负坐标,请从输入中减去最小负像素 x,y 坐标

    // the values as set out in the question image
    var r = 50; 
    var w = r * 2;
    var h = Math.sqrt(3) * r;
    // returns the hex grid x,y position in the object retPos.
    // retPos is created if not supplied;
    // argument x,y is pixel coordinate (for mouse or what ever you are looking to find)
    function getHex (x, y, retPos){
        if(retPos === undefined){
            retPos = {};
        }
        var xa, ya, xpos, xx, yy, r2, h2;
        r2 = r / 2;
        h2 = h / 2;
        xx = Math.floor(x / r2);
        yy = Math.floor(y / h2);
        xpos = Math.floor(xx / 3);
        xx %= 6;
        if (xx % 3 === 0) {      // column with diagonals
            xa = (x % r2) / r2;  // to find the diagonals
            ya = (y % h2) / h2;
            if (yy % 2===0) {
                ya = 1 - ya;
            }
            if (xx === 3) {
                xa = 1 - xa;
            }
            if (xa > ya) {
                retPos.x = xpos + (xx === 3 ? -1 : 0);
                retPos.y = Math.floor(yy / 2);
                return retPos;
            }
            retPos.x = xpos + (xx === 0 ? -1 : 0);
            retPos.y = Math.floor((yy + 1) / 2);
            return retPos;
        }
        if (xx < 3) {
            retPos.x = xpos + (xx === 3 ? -1 : 0);
            retPos.y = Math.floor(yy / 2);
            return retPos;
        }
        retPos.x = xpos + (xx === 0 ? -1 : 0);
        retPos.y = Math.floor((yy + 1) / 2);
        return retPos;
    }
    

    十六进制到像素

    还有一个辅助函数,在给定单元格坐标的情况下绘制一个单元格。

    // Helper function draws a cell at hex coordinates cellx,celly
    // fStyle is fill style
    // sStyle is strock style;
    // fStyle and sStyle are optional. Fill or stroke will only be made if style given
    function drawCell1(cellPos, fStyle, sStyle){    
        var cell = [1,0, 3,0, 4,1, 3,2, 1,2, 0,1];
        var r2 = r / 2;
        var h2 = h / 2;
        function drawCell(x, y){
            var i = 0;
            ctx.beginPath();
            ctx.moveTo((x + cell[i++]) * r2, (y + cell[i++]) * h2)
            while (i < cell.length) {
                ctx.lineTo((x + cell[i++]) * r2, (y + cell[i++]) * h2)
            }
            ctx.closePath();
        }
        ctx.lineWidth = 2;
        var cx = Math.floor(cellPos.x * 3);
        var cy = Math.floor(cellPos.y * 2);
        if(cellPos.x  % 2 === 1){
            cy -= 1;
        }
        drawCell(cx, cy);
        if (fStyle !== undefined && fStyle !== null){  // fill hex is fStyle given
            ctx.fillStyle = fStyle
            ctx.fill();
        }
        if (sStyle !== undefined ){  // stroke hex is fStyle given
            ctx.strokeStyle = sStyle
            ctx.stroke();
        }
    }
    

    【讨论】:

    • 您减少到二进制左右测试可能是最直接的解决方案(== 最快的解决方案)——干得好!您可以考虑在文档中发布此十六进制命中测试。 :-)
    【解决方案5】:

    这是您问题的完整数学和函数表示。您会注意到,除了根据鼠标位置更改文本颜色的三元组之外,此代码中没有ifs 和thens。整个工作实际上只不过是一行简单的数学运算;

    (r+m)/2 + Math.cos(a*s)*(r-m)/2;
    

    并且此代码可重复用于从三角形到圆形的所有多边形。所以如果有兴趣请继续阅读。很简单。

    为了显示功能,我必须开发一个模拟问题的模型。我利用一个简单的实用函数在画布上绘制了一个多边形。所以整体解决方案应该适用于任何多边形。下面的 sn-p 将以画布上下文c、半径r、边数s 和画布中的局部中心坐标cxcy 作为参数,并在给定的图形上绘制多边形画布上下文在正确的位置。

    function drawPolgon(c, r, s, cx, cy){ //context, radius, sides, center x, center y
      c.beginPath();
      c.moveTo(cx + r,cy);
      for(var p = 1; p < s; p++) c.lineTo(cx + r*Math.cos(p*2*Math.PI/s), cy + r*Math.sin(p*2*Math.PI/s));
      c.closePath();
      c.stroke();
    }
    

    我们还有一些其他实用功能,人们可以很容易地理解它们到底在做什么。然而,最重要的部分是检查鼠标是否漂浮在我们的多边形上。它由实用函数isMouseIn 完成。它基本上是计算鼠标位置到多边形中心的距离和角度。然后,将其与多边形的边界进行比较。多边形的边界可以用简单的三角函数来表示,就像我们在drawPolygon函数中计算了顶点一样。

    我们可以把我们的多边形想象成一个圆,其半径以边数的频率振荡。振荡的峰值在给定的半径值r 处(恰好在角度2π/s 的顶点处,其中s 是边数),最小值mr*Math.cos(Math.PI/s)(每个都显示在角度2π/s + 2π/2s = 3π/s)。我很确定表达多边形的理想方式可以通过傅里叶变换来完成,但我们在这里不需要。我们所需要的只是一个恒定的半径分量,它是最小值和最大值的平均值(r+m)/2,以及带有边数频率的振荡分量s,幅度值最大 - 最小值)/2 在它上面, Math.cos(a*s)*(r-m)/2。当然,根据傅立叶状态,我们可能会继续使用更小的振荡组件,但使用六边形,您实际上并不需要进一步迭代,而使用三角形则可能需要。这是我们在数学中的多边形表示。

    (r+m)/2 + Math.cos(a*s)*(r-m)/2;
    

    现在我们只需要计算鼠标位置相对于多边形中心的角度和距离,并将其与上述表示多边形的数学表达式进行比较。所以我们的魔法函数编排如下:

    function isMouseIn(r,s,cx,cy,mx,my){
      var m = r*Math.cos(Math.PI/s),   // the min dist from an edge to the center
          d = Math.hypot(mx-cx,my-cy), // the mouse's distance to the center of the polygon
          a = Math.atan2(cy-my,mx-cx); // angle of the mouse pointer
      return d <= (r+m)/2 + Math.cos(a*s)*(r-m)/2;
    }
    

    所以下面的代码展示了你可以如何解决你的问题。

    // Generic function to draw a polygon on the canvas
    
    function drawPolgon(c, r, s, cx, cy){ //context, radius, sides, center x, center y
      c.beginPath();
      c.moveTo(cx + r,cy);
      for(var p = 1; p < s; p++) c.lineTo(cx + r*Math.cos(p*2*Math.PI/s), cy + r*Math.sin(p*2*Math.PI/s));
      c.closePath();
      c.stroke();
    }
    
    // To write the mouse position in canvas local coordinates
    
    function writeText(c,x,y,msg,col){
      c.clearRect(0, 0, 300, 30);
      c.font = "10pt Monospace";
      c.fillStyle = col;
      c.fillText(msg, x, y);
    }
    
    // Getting the mouse position and coverting into canvas local coordinates
    
    function getMousePos(c, e) {
      var rect = c.getBoundingClientRect();
      return { x: e.clientX - rect.left,
               y: e.clientY - rect.top
             };
    }
    
    // To check if mouse is inside the polygone
    
    function isMouseIn(r,s,cx,cy,mx,my){
      var m = r*Math.cos(Math.PI/s),
          d = Math.hypot(mx-cx,my-cy),
          a = Math.atan2(cy-my,mx-cx);
      return d <= (r+m)/2 + Math.cos(a*s)*(r-m)/2;
    }
    
    // the event listener callback
    
    function mouseMoveCB(e){
      var mp = getMousePos(cnv, e),
         msg = 'Mouse at: ' + mp.x + ',' + mp.y,
         col = "black",
      inside = isMouseIn(radius,sides,center[0],center[1],mp.x,mp.y);
      writeText(ctx, 10, 25, msg, inside ? "turquoise" : "red");
    }
    
    // body of the JS code
    
    var cnv = document.getElementById("myCanvas"),
        ctx = cnv.getContext("2d"),
      sides = 6,
     radius = 100,
     center = [150,150];
    cnv.addEventListener('mousemove', mouseMoveCB, false);
    drawPolgon(ctx, radius, sides, center[0], center[1]);
    #myCanvas { background: #eee;
                     width: 300px;
                    height: 300px;
                    border: 1px #ccc solid
              }
    &lt;canvas id="myCanvas" width="300" height="300"&gt;&lt;/canvas&gt;

    【讨论】:

    • 不错!谢谢你!很遗憾,尽管 xD
    • @Tobsta 我相信我解决这个问题的方法有点不正统,但同时我相信它非常有效。实际上,我已经开始考虑如何将它应用于任何不规则形状的多边形,例如国家地图等。然后当然因为它不能用单个正弦分量来完成,我想我将不得不坐下来研究一些 FFT。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-22
    • 2012-03-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多