在 PHP 中用像素画一条线

从“我没有意识到那有多复杂”的抽屉里,我前几天想知道如何仅使用像素绘制一条线。结果证明这比我想象的要复杂。

通常在 PHP 中,您会使用该imageline()函数在两点之间画一条线。以下代码块创建图像并从坐标 50x,50y 到 200x,150y 绘制一条白线。

// 生成宽高为 250 像素的图像资源。
$im = imagecreatetruecolor(250, 250);
 
// 创建颜色。
$white = imagecolorallocate($im, 255, 255, 255);
 
// 从 50x,50y 到 200x,150y 画一条白线。
imageline($im, 50, 50, 200, 150, $white);
 
// 将图像资源写入名为 line.png 的文件。
imagepng($im, 'line.png');
 
// 销毁图像资源。
imagedestroy($im);

这将创建一个看起来像这样的图像。

作为练习,我正在研究如何通过沿线绘制像素来绘制同一条线。起点是函数imagesetpixel(),它允许我们为图像中的任何像素设置不同的颜色。我知道我必须沿着线条一步一步地为线条中的每个像素着色。经过一番研究,我发现了Bresenham's line algorithm,它允许在两点之间画一条线。Bresenham 的线条算法是允许在一个点和另一个点之间绘制线条的一系列算法之一,但我将在这篇文章中只关注这个算法。

Bresenham 算法的工作原理是查看起点和终点之间的栅格点。光栅点是一个像素显示在屏幕上的点,因为两点之间的实际线将穿过许多不同的像素,我们需要决定哪些像素最能描述这条线。该算法沿着这条线前进,并决定像素应该“开”还是“关”,从而绘制这条线。

实际上,这在技术上是不正确的。让我们取一条从 0x,1y 到 5x,4y 的线。我们所做的是沿着从 0 到 5 的 x 值运行,然后通过查看线的梯度来决定是否在 y 轴上的某个点绘制一个像素。如果端点较高,则将像素画得更高,如果端点较低,则将像素画得更低。算法遵循以下等式(取自维基百科)。

在循环遍历 x 坐标之前,我们需要先计算出一些值。

$dx = $x1 - $x0;
 
$dy = $y1 - $y0;
 
$slope = $dy / $dx;
 
$pitch = $y0 - $slope * $x0;

这为我们提供了 0.6 的斜率值和 1 的音高值。对于沿 x 坐标的每一步,我们可以使用简化方程计算出 y 坐标的位置。 

$y = $slope * $x + $pitch;

每个点都会导致以下计算。 

xCalculation to work out yy
00.6 * 0 + 11
10.6 * 1 + 12
20.6 * 2 + 12
30.6 * 3 + 13
40.6 * 4 + 13
5不适用4

这会在以下位置生成像素(最后绘制的最后一个像素)。

.0123456
0.......
1x......
2.xx....
3...xx..
4.....x.
5.......

您可能认为这就是我们需要做的一切,但是当我们尝试计算向上的斜率时会出现问题。上述计算依赖于 x 值增加的事实,因此我们需要添加一些代码来考虑从高 x 值开始并减少的行。如果终点高于起点,我们也会遇到问题。因此,我们需要增加或减少 x 或 y 以正确绘制线条。

我在互联网上看到过一些例子,其中 Bresenham 的线算法被分成两部分,在不同的函数中处理向上和向下倾斜的线。这在高度优化事物时很有用,但通常单个函数就可以了。

最后,我们还需要添加一点代码,以防开始和结束坐标相同,因为我们不需要运行任何这些代码。

使用 Bresenham's line algorithm 绘制线条的最终代码如下。

/**
  * Draw a line using Bresenham's line algorithm.
  *
  * @param resource $im
  *   The image resource.
  * @param int $x0
  *   The x part of the starting coordinate.
  * @param int $y0
  *   The y part of the starting coordinate.
  * @param int $x1
  *   The x part of the ending coordinate.
  * @param int $y1
  *   The y part of the ending coordinate.
  * @param int $color
  *   The color of the line, created from imagecolorallocate().
  */
function drawLine($im, $x0, $y0, $x1, $y1, $color) {
  if ($x0 == $x1 && $y0 == $y1) {
    // 开始和结束是一样的。
    imagesetpixel($im, $x0, $y0, $color);
    return;
  }
 
  $dx = $x1 - $x0;
  if ($dx < 0) {
    // x1 低于 x0。
    $sx = -1;
  } else {
    // x1 高于 x0。
    $sx = 1;
  }
 
  $dy = $y1 - $y0;
  if ($dy < 0) {
    // y1 低于 y0。
    $sy = -1;
  } else {
    // y1 高于 y0。
    $sy = 1;
  }
 
  if (abs($dy) < abs($dx)) {
    // 坡度正在下降。
    $slope = $dy / $dx;
    $pitch = $y0 - $slope * $x0;
 
    while ($x0 != $x1) {
      imagesetpixel($im, $x0, round($slope * $x0 + $pitch), $color);
      $x0 += $sx;
    }
  } else {
    // 坡度正在上升。
    $slope = $dx / $dy;
    $pitch = $x0 - $slope * $y0;
 
    while ($y0 != $y1) {
      imagesetpixel($im, round($slope * $y0 + $pitch), $y0, $color);
      $y0 += $sy;
    }
  }
 
  // 通过添加最终像素完成。
  imagesetpixel($im, $x1, $y1, $color);
}

使用随机坐标运行此函数的几个示例会生成下图。

Bresenham 的线条算法在许多不同的应用程序中被广泛使用,但它在创建抗锯齿线条方面并不是那么好,因此在需要抗锯齿时不会使用它。

顺便说一下,  imageline()PHP 中的gdImageLine()函数调用 了 GD 库中的函数。从gdImageLine()这个函数的源码可以看出,实际上实现了Bresenham's line algorithm。这比我这里的简单示例更复杂(并且测试得更好!),但它仍然遵循相同的基本规则。