19.绘制一条直线:在绘制直线时决定去填充哪些像素

19.Drawing in a Straight Line: Deciding which pixels to fill when drawing a line

On paper, drawing a straight line between two points is easy: Put your ruler down on the page, and run your pencil across. In contrast, drawing a straight line on a computer screen is a matter of deciding which pixels to colour in (known as rasterization). It can be thought of as drawing a pencil line on squared graph paper, and colouring in any squares which the line passes through:

如果在纸上,在两点之间绘制一条直线是很容易的:把尺子放在纸上,然后用铅笔划过去。相反地,在电脑屏幕上绘制一条直线是决定哪些像素需要着色的问题(被称为光栅化)。它可以被想象为在方格绘图纸上用铅笔绘制一条直线,并且给直线穿过的格子涂上颜色:

But how do you practically do this? One way is to move along the line in small increments, and colour the square you are in after each movement. But if your increment isn’t small enough you can miss some squares which the line barely touches — and if your increment is too small, it can be quite an inefficient exercise.

但是你怎样实际操作呢?一种方法是以很小的增量沿着直线移动,在每一步移动之后为你到达的方格着色。但是如你的果增量不是足够小的话你会漏掉一些直线刚刚触到的方格——而且如果你的增量太小的话,那将是十分没有效率的工作。

This post will explain how to draw a line on a computer screen. There are several fast line-drawing algorithms:Bresenham is quite famous, andWu can do anti-aliasing. I’m going to explain an equivalent to Wu’s algorithm, but without the anti-aliasing.

这篇帖子将解释如何在电脑屏幕上绘制直线。这儿有许多高速的直线绘制算法:Bresenham是非常有名的,而Wu能够进行反混淆。我将解释等同于Wu的算法,但是不使用反混淆。

The Algorithm

算法

Every two-dimensional line can be put into one of three categories:

  1. It’s longer in the Y dimension than in X.
  2. It’s the same length in X and Y (and thus is at 45 degrees).
  3. It’s longer in the X dimension than in Y.

每个二维直线可以归为以下三种情况:

   1. Y维长度大于X

   2. X和Y的长度相等(因而呈45度角)

     3. X维长度大于Y

Thus we can always pick the longest dimension and call it the major axis, with the other axis being the minor axis. (In the middle case, you can pick X or Y for the major axis, it won’t matter.) Here’s the first part of our key insight for our line-drawing algorithm: by definition, if you move 1 pixel on the major axis, you’ll move at most 1 pixel in the minor axis. After all, if you moved further in the minor axis than major, you have labelled them wrong. Here’s some diagrams to illustrate:

因此我们可以始终选取最长的维度并称其为长轴,而另一个轴称为短轴。(在中间的情况下,你可以选取X或者Y作为长轴,这没有关系)下面是我们直线绘制算法的第一个关键要点:根据定义,如果你在长轴上移动一个像素距离,你将最多在短轴上移动一个像素距离。毕竟,如果你在短轴上的移动距离超过了长轴,你便已经犯错了。以下用图表来演示:

The major axis is red, the minor axis is blue. In the bottom-right picture, we could have chosen either X or Y to be the major axis.

长轴是红色,短轴是蓝色。在右下方的图片里,我们可以选择X或Y作为长轴。

And here’s the second part of our insight: if you move 1 pixel in the major axis, and thus at most 1 pixel in the minor axis, you will always touch at most two pixels on the minor axis. You can see this in the diagram above. Trace one pixel in the major axis. To touch three pixels, you would need to begin in pixel A, then move into pixel B, out of pixel B and into pixel C. That would require moving more than one pixel in the minor axis, which we’ve already seen would mean you’ve labelled them wrong.

下面是第二个要点:如果你在长轴上移动一个像素距离,因而最多在短轴上移动一个像素距离,那么你将始终在短轴上最多接触到两个像素。你可以在上图中观察这个特点。在长轴上追踪一个像素。为了接触到三个像素,你可能需要从像素A出发,接着移动到像素B,然后离开像素B到达像素C。那将可能需要在短轴上移动超过一个像素的距离,我们已经知道这意味这你已经犯错了。

So we can write our line-drawing algorithm so that it advances 1 pixel in the major axis, and then just works out which 1 or 2 pixels it touched on the minor axis, then go again.

于是我们可以写出直线绘制算法,它在长轴上前进一个像素距离,接着只要算出它在短轴上接触到了哪一个或两个像素,如此往复。

Specific Case

特殊情况

To begin with, here’s the code for the case when the X axis is the major axis, and the start point is to the left of the end point. We fill the start and end pixels as a special case, because we know straight away that the line must pass through them:

作为开始,以下代码适合于当X轴是长轴的情况,并且起点在终点的左边。我们将起点和终点像素作为特殊的情况来填充,因为我们很清楚直线必定通过它们:

        fillPixel(lineStartX, lineStartY);
        
        if (lineStartX == lineEndX && lineStartY == lineEndY)
            return;
        
        double slope = (double)(lineEndY - lineStartY) / (double)(lineEndX - lineStartX);
        fillPixelsLine(lineStartX, lineEndX, lineStartY, slope);
        
        fillPixel(lineEndX, lineEndY);

All the other pixels in the line are filled by the fillPixelsLine function:

直线上的所有其他像素使用fillPixelsLine 方法来填充:

    private void fillPixelsLine(int startX, int endX, int startY, double slope)
    {
        double curY = startY + 0.5 + (0.5 * slope);
        for (int curX = startX + 1; curX != endX; curX++)
        {
            fillPixel(curX, (int)Math.floor(curY));

            double newY = curY + slope;            
            if (Math.floor(newY) != Math.floor(curY))
            {
                fillPixel(curX, (int)Math.floor(newY));
            }
            curY = newY;
        }
    }

The above function is passed the coordinates of the starting pixel, but actually begins on the next pixel. This is effectively the top-left corner of a pixel, but the line starts in the middle. So the “+ 0.5 + 0.5 * slope” for curY starts the Y coordinate in the middle of the pixel (the “+ 0.5″), then works out how much it would be at the end of the pixel:

上面的方法传入了起始像素的两个坐标值,但事实上是从下一个像素开始工作的。传入的实际上是像素左上角的坐标值,但直线是从中心开始绘制的。于是curY变量的“+ 0.5 + 0.5 * slope”表示从像素中心的Y坐标(“+ 0.5″ )开始算起,当直线到达像素末尾时值是多少:

The loop begins by filling a pixel (using the Y at the start, or left-hand edge, of the current column). Then it advances the Y to its next value (the Y at the end, or right-hand edge of the current column) — if this is a different pixel from the earlier one, it is also filled:

循环从填充一个像素开始(使用起点,或是当前列左边缘的Y坐标值)。接着将Y坐标增加到它的下一个值(终点,或时当前列右边缘的Y坐标值)——如果这是与前一个像素不同的像素,则填充之:

We tell if the pixel is the same by looking at the integer part using Math.floor: In the above diagram, Math.floor(10.7) is 10, and Math.floor(11.3) is 11, so because those are not equal, we can tell that we have crossed the pixel boundary (located at 11.00).

我们可通过观察其值的整数部分来判断是否为同一个像素,这可使用Math.floor方法:在上图中,Math.floor(10.7) 是10,而Math.floor(11.3) 是11,于是因为它们不相等,所以我们可以说直线跨过了像素的边界(位于11.00处)。

Generalising

推广

The above code is specialised for the case that the X axis is the major axis, and the line heads in the positive X direction. To generalise the code to handle all cases, we need to add a bit of extra parameterisation to the code:

以上代码适用于当X轴为长轴,而直线朝向X正方向的特殊情况。为了推广代码去处理所有情况,我们为代码需要添加一些额外的参数:

    private void drawLine(int lineStartX, int lineStartY, int lineEndX, int lineEndY)
    {
        fillPixel(lineStartX, lineStartY, true);
        
        if (lineStartX == lineEndX && lineStartY == lineEndY)
            return;
            
        fillPixel(lineEndX, lineEndY, true);
        
        if (Math.abs(lineEndX - lineStartX) >= Math.abs(lineEndY - lineStartY))
            fillPixels(lineStartX, lineEndX, lineStartY, (double)(lineEndY - lineStartY) / (double)(lineEndX - lineStartX), true);
        else
            fillPixels(lineStartY, lineEndY, lineStartX, (double)(lineEndX - lineStartX) / (double)(lineEndY - lineStartY), false);
    }
    
    private void fillPixels(int start, int end, int startMinor, double slope, boolean horizontal)
    {
        int advance = end > start ? 1 : -1;
        double curMinor = startMinor + 0.5 + (0.5 * advance * slope);
        for (int curMajor = start + advance; curMajor != end; curMajor += advance)
        {
            fillPixel(curMajor, (int)Math.floor(curMinor), horizontal);

            double newMinor = curMinor + (advance * slope);            
            if (Math.floor(newMinor) != Math.floor(curMinor))
                fillPixel(curMajor, (int)Math.floor(newMinor), horizontal);
            curMinor = newMinor;
        }
    }
    
    private void fillPixel(int major, int minor, boolean horizontal)
    {
        if (horizontal) // X is major
            addObject(new Box(), major, minor);
        else // Y is major
            addObject(new Box(), minor, major);
    }

The two added parameters are the horizontal boolean, which indicates whether major is X or Y, and the advance constant, which allows you to go in a negative direction along the major axis. The principle of the code is exactly the same.

所添加的两个参数是布尔变量horizontal ,它表示长轴是否为X或者Y,以及常量advance,它允许你沿着长轴向负方向前进。代码的原理是完全相同的。

Summary

小结

You can see the line-drawing in action — left-click to set the start point, and right-click to set the end point.

Our line-drawing algorithm is perhaps a bit unusual, because it colours inany pixel the line touches, which Bresenham does not. (Wu does, but draws in different transparencies, to implement anti-aliasing.) However, in the next few posts I’m going to show you other uses of this algorithm, besides line-drawing, which require this behaviour.

你可以看看直线绘制的执行情况——点击鼠标左键去设置起点,点击右键去设置终点。

我们的直线绘制算法可能有一点不常见,因为它给直线接触到的所有像素进行着色,而Bresenham 算法则没有。(Wu算法做了,但是用不同的透明度绘制的,以便进行反混淆。)然而,除了进行直线绘制,在接下来的几篇帖子里我打算去展示这个算法的其它功用,它们需要借助这个原理。

你可能感兴趣的:(Joy,of,Greenfoot,algorithm,function,graph,integer,parameters)