在 PHP 中用像素画一个圆圈

自从我上一篇文章谈到在 PHP 中用像素画一条线以来,我一直在研究画圆。事实证明,画圆有几种不同的方法,所以我将在这里介绍几个选项。

首先,PHP 中有一些内置函数可用于绘制圆弧、圆或椭圆作为填充形状或线。PHP 中调用的内置函数imageellipse()可用于绘制圆。这是 GD 库的一部分。

<?php
 
// 生成图像。
$im = imagecreatetruecolor(255, 255);
 
// 创造一种色彩。
$white = imagecolorallocate($im, 255, 255, 255);
 
// 在图像中间画一个圆。
imageellipse($im, 128, 128, 200, 200, $white);
 
// 将图像保存到文件中。
imagepng($im, 'circle.png');
 
// 销毁图像处理程序。
imagedestroy($im);

此代码生成以下图像。

该imageellipse()函数需要相当多的参数,但可根据提供的参数用于绘制圆或椭圆。要绘制实心圆,您可以使用该 imagefilledellipse()函数,该函数采用相同的参数但绘制实心圆。

如何在不使用内置 PHP 函数的情况下绘制圆?最简单的方法是通过从中心点绘制一系列线来绘制实心圆。对于圆中的每个度数,我们使用以下公式:

x = radius * cos(angle)
y = radius * sin(angle)

我们在这里做的是计算圆心点所对的点。这是从最远点开始的直线连接在一起时特定点的角度。所以本质上,这是一条从圆心到外边缘的线。

有了这个,我们需要做的就是遍历一个圆(即 360)中的所有角度,并从中心点到由上述方程计算出的坐标画一条线。

<?php
 
// 生成图像。
$im = imagecreatetruecolor(255, 255);
 
// 创造一种色彩。
$white = imagecolorallocate($im, 255, 255, 255);
 
// 将线条粗细设置为 4。
imagesetthickness($im, 4);
 
// 设置圆的中心点。
$centerX = 128;
$centerY = 128;
 
// 设置圆的半径。
$radius = 100;
 
// 每个循环将增加的角度。
$theta = 0;
 
while ($theta <= 360) {
  // 计算新的 x,y 坐标。
  $x = $centerX + $radius * cos($theta);
  $y = $centerY + $radius * sin($theta);
 
  // 增加θ。
  $theta += 1;
 
  // 画一条从中心点到 x,y 坐标的线。
  imageline($im, $centerX, $centerY, $x, $y, $white);
}
 
// 将图像保存到文件中。
imagepng($im, 'circle-filled.png');
 
// 销毁图像处理程序。
imagedestroy($im);

请注意,此代码还包括对 的调用imagesetthickness(),我们将其增加到 4,这是为了创建一个完全填充的圆。如果没有这个调用,圆的外边缘就不能正确地啮合在一起,圆也没有完全填满。这也可以通过增加 $theta 时增加步数来排序,但这也意味着增加我们需要运行循环的次数。增加画线的粗细是解决这个问题的简单方法。

此代码产生以下输出。

要绘制一个未填充的圆,我们只需imageline()调用imagesetpixel(). 这实质上是在投影线的末端绘制一个像素。

imagesetpixel($im, $x, $y, $white);

通过该更改,创建了以下圆圈。

如果这看起来有点笨拙,那是因为许多点没有连接。这种 cos/sin 方法适用于小圆圈,但圆圈越大,点之间的距离就越远,因此我们需要增加循环的迭代次数来缩小这个差距。

绘制圆的更好算法是 Bresenham 的圆绘制算法。这与 Bresenham 的画线算法有关,但在这里我们遍历圆周上的每个点,并决定应该绘制哪个像素以最好地拟合圆。

第一步是将圆分成 8 段,每段 45 度。因为圆是对称的,所以我们需要做的就是计算出单个线段,然后我们可以将该线段转换为其他 7 个线段。这意味着我们不必一直绕着圆圈循环,只需通过其中一个段即可。

使用圆心的坐标,我们可以计算出每个线段中的第一个点。

centerX + x, centerY + y
centerX + y, centerY + x
centerX - y, centerY + x
centerX - x, centerY + y
centerX - x, centerY - y
centerX - y, centerY - x
centerX + y, centerY - x
centerX + x, centerY - y

然后我们可以开始计算第一段的每个点所在的位置。第一个决策是根据公式 $decision = 3 - (2 * $radius) 计算出来的。

在何处绘制下一个像素的决定使用以下方法计算:

  • 如果决策小于 0,则下一个像素将在 (x+1, y) 处绘制。然后我们根据公式 $decision = $decision + (4 * $x) + 6 计算出决策。我们还增加了 x。

  • 如果决定大于 0,则下一个像素将在 (x+1, y-1) 处绘制。然后我们根据公式 $decision + 4 * ($x - $y) + 10 计算出决策。我们也增加 x 并减少 y。

一旦 x 和 y 收敛,我们就可以停止,因为我们将计算出圆中的所有点。这里的数学是从圆的周长方程推导出来的,即 x 2 + y 2 - r 2。我发现这个页面很好地介绍了这个数学(虽然它确实需要一段时间才能理解发生了什么)。

以下代码将所有这些付诸实践。

<?php
 
// 生成图像。
$im = imagecreatetruecolor(255, 255);
 
// 创造一种色彩。
$white = imagecolorallocate($im, 255, 255, 255);
 
// 设置圆的中心点。
$centerX = 128;
$centerY = 128;
 
$radius = 100;
 
$x = 0;
$y = $radius;
 
// 计算初始决策 
$decision = 3 - (2 * $radius);
 
while ($x <= $y) {
  // 在圆的 8 个部分中的每一个中放置一个像素。
  imagesetpixel($im, $centerX + $x, $centerY + $y, $white);
  imagesetpixel($im, $centerX + $y, $centerY + $x, $white);
  imagesetpixel($im, $centerX - $y, $centerY + $x, $white);
  imagesetpixel($im, $centerX - $x, $centerY + $y, $white);
  imagesetpixel($im, $centerX - $x, $centerY - $y, $white);
  imagesetpixel($im, $centerX - $y, $centerY - $x, $white);
  imagesetpixel($im, $centerX + $y, $centerY - $x, $white);
  imagesetpixel($im, $centerX + $x, $centerY - $y, $white);
 
  // x 的增量值。
  $x++;
 
  if ($decision < 0) {
    // 下一个像素将在 (x + 1, y) 处绘制。
    $decision = $decision + (4 * $x) + 6;
  }
  else {
    // 下一个像素将在 (x + 1, y - 1) 处绘制。
    $decision = $decision + 4 * ($x - $y) + 10;
    // 减少 y 的值。
    $y--;
  }
}
 
// 将图像保存到文件中。
imagepng($im, 'circle.png');
 
// 销毁图像处理程序。
imagedestroy($im);

这就是代码产生的结果。

如您所见,这是一个更加精致的圆圈,不包含任何间隙或其他错误。这个算法可以改进,我什至见过只使用位移的例子,这使得它更有效率。