关于 javascript:HTML5 Canvas: 分割/计算线条

HTML5 Canvas: Splitting/Calculating Lines

我已经在键盘上敲了大约一个星期,但我无法为我的问题找到合适的解决方案。我认为它比 HTML Canvas 更与数学相关......希望有人能指出我正确的方向。

我有一个 HTML 画布,用户可以在其中使用鼠标和非常简单的 moveTo() 和 lineTo() 函数绘制线条。用户完成后,我将坐标保存在 MongoDB 中。当用户稍后再次点击该页面时,我想显示他的绘图但我不想一次加载具有所有存储坐标的整个图片,我想将它以图块的形式返回(通过缓存每个图块来获得更好的性能)。

图块为 200x200 像素(固定偏移和宽度,从 0 -> 200-> 400 ->...开始)。
现在,当用户从 50,50(x/y) 到 250,250(x/y) 画一条线时,每个边界框(平铺)中只有一个点。我需要分割线并计算每个边界框(图块)中每条线的起点和终点。否则我无法部分绘制图像(在瓷砖中)。当一条线穿过多个边界框(图块)时,它会变得更加复杂。例如:100,100 (x/y) -> -1234,-300 (x/y)。
线条可以从任意点 (/-) 到任意方向任意距离。

当然,我查看了 Bresenham 的旧算法,它确实有效 - 部分有效,但对我来说,它似乎是最长且最耗费资源的解决方案。

所以,我在这里的原因是我希望有人可以用(也许)另一种计算每个边界框的线的起点/终点的方法将我指向正确的方向。

代码示例在 JavaScript 或 PHP 中非常受欢迎。

感谢您的阅读和思考 :)


tl;dr:使用飞机,数学解释如下。底部有一个画布示例。

鉴于您的所有单元格都是轴对齐的边界框,您可以使用平面方程来找到您的线与边缘的交点。

飞机

你可以把你的盒子想象成一组四个几何平面。每个平面都有一个法线或长度为 1 的向量,指示哪个方向是平面的"前"。构成细胞侧面的平面的法线是:

1
2
3
4
   top = {x:  0, y: -1};
bottom = {x:  0, y:  1};
  left = {x: -1, y:  0};
 right = {x:  1, y:  0};

给定平面上的一点,平面有方程:

1
distance = (normal.x * point.x) + (normal.y * point.y)

你可以使用这个方程来计算平面的距离。在这种情况下,您知道框的左上角(假设 x 为 10,y 为 100)在顶平面上,因此您可以这样做:

1
2
distance = (0 * 10) + (-1 * 100)
distance = -100

根据平面检查点

一旦你有了距离,你就可以重新使用方程来检查任何点相对于平面的位置。对于随机点 p(其中 x 为 -50,y 为 90),您可以执行以下操作:

1
2
3
4
5
result = (normal.x * p.x) + (normal.y * p.y) - distance
result = (0 * -50) + (-1 * 90) - (-100)
result = 0 + (-90) - (-100)
result = -90 + 100
result = 10

有两种可能的结果:

1
2
3
4
5
6
if (result >= 0) {
    // point is in front of the plane, or coplanar.
    // zero means it is coplanar, but we don't need to distinguish.
} else {
    // point is behind the plane
}

对照平面检查一条线

您可以通过这种方式检查从 ab 的线的两个端点:

1
2
result1 = (normal.x * a.x) + (normal.y * a.y) - distance
result2 = (normal.x * b.x) + (normal.y * b.y) - distance

有四种可能的结果:

1
2
3
4
5
6
7
8
9
if (result1 >= 0 && result2 >= 0) {
  // the line is completely in front of the plane
} else if (result1 < 0 && result2 < 0) {
  // the line is completely behind the plane
} else if (result1 >= 0 && result2 < 0) {
  // a is in front, but b is behind, line is entering the plane
} else if (result1 < 0 && result2 >= 0) {
  // a is behind, but b is in front, line is exiting the plane
}

当直线与平面相交时,您要找到交点。用矢量术语来思考一条线会很有帮助:

1
a + t * (b - a)

如果t == 0,你在行首,t == 1 是行尾。在这种情况下,您可以将交点计算为:

1
time = result1 / (result1 - result2)

而交点为:

1
2
hit.x = a.x + (b.x - a.x) * time
hit.y = a.y + (b.y - a.y) * time

对照方框检查一条线

通过该数学运算,您可以计算出与您的框相交的线。您只需要针对每个平面测试线的端点,并找到时间的最小值和最大值。

因为你的盒子是一个凸多边形,所以这个检查有一个早期的结果:如果这条线完全在你盒子里的任何一个平面的前面,它就不能与你的盒子相交。您可以跳过检查其余飞机。

在 JavaScript 中,您的结果可能如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
 * Find the points where a line intersects a box.
 *
 * @param a Start point for the line.
 * @param b End point for the line.
 * @param tl Top left of the box.
 * @param br Bottom right of the box.
 * @return Object {nearTime, farTime, nearHit, farHit}, or false.
 */

function intersectLineBox(a, b, tl, br) {
  var nearestTime = -Infinity;
  var furthestTime = Infinity;
  var planes = [
    {nx:  0, ny: -1, dist: -tl.y},  // top
    {nx:  0, ny:  1, dist:  br.y},  // bottom
    {nx: -1, ny:  0, dist: -tl.x},  // left
    {nx:  1, ny:  0, dist:  br.x}   // right
  ];
  for (var i = 0; i < 4; ++i) {
    var plane = planes[i];
    var nearDist = (plane.nx * a.x + plane.ny * a.y) - plane.dist;
    var farDist = (plane.nx * b.x + plane.ny * b.y) - plane.dist;
    if (nearDist >= 0 && farDist >= 0) {
      // both are in front of the plane, line doesn't hit box
      return false;
    } else if (nearDist < 0 && farDist < 0) {
      // both are behind the plane
      continue;
    } else {
      var time = nearDist / (nearDist - farDist);
      if (nearDist >= farDist) {
        // entering the plane
        if (time > nearestTime) {
          nearestTime = time;
        }
      } else {
        // exiting the plane
        if (time < furthestTime) {
          furthestTime = time;
        }
      }
    }
  }
  if (furthestTime < nearestTime) {
    return false;
  }
  return {
    nearTime: nearestTime,
    farTime: furthestTime,
    nearHit: {
      x: a.x + (b.x - a.x) * nearestTime,
      y: a.y + (b.y - a.y) * nearestTime
    },
    farHit: {
      x: a.x + (b.x - a.x) * furthestTime,
      y: a.y + (b.y - a.y) * furthestTime
    }
  };
}

如果这仍然太慢,您还可以通过将世界划分为大矩形并为这些矩形分配线来进行广泛相位剔除。如果您的线和单元格不在同一个矩形中,它们就不会发生冲突。

我已经上传了一个画布示例。


这看起来你必须弄清楚每条线在什么点与每个图块的边界相交。

查看这个问题的答案:是否有一种简单的方法来检测线段交叉点?

答案不提供代码,但将方程式转换为 PHP 或 Javascript 应该不会太难...

编辑:

为什么,确切地说,你想分割线?我了解您不想一次加载所有行,因为这可能需要一段时间。但是,只加载和绘制前几行,然后再绘制其余部分有什么问题?

我认为这比切割每一行以适应特定的图块要简单得多。平铺是优化位图加载的好方法;我不认为它非常适合基于矢量的绘图。

您也可以考虑发送一个 Ajax 请求,并在收到请求时开始绘制整个内容;这不会干扰页面的加载。