跳转至

第2节: 形状

Shapes

我们一直在谈论像素坐标这样的低级图形概念,但幸运的是,我们通常不必在最低级别上工作。大多数图形系统允许您使用更高级的形状,如三角形和圆形,而不是单个像素。并且,大部分关于坐标的艰难工作都是使用变换来完成,而不是直接使用坐标。在本节和下一节中,我们将介绍一些通常由2D图形API提供的更高级别的功能。

We have been talking about low-level graphics concepts like pixels and coordinates, but fortunately we don't usually have to work on the lowest levels. Most graphics systems let you work with higher-level shapes, such as triangles and circles, rather than individual pixels. And a lot of the hard work with coordinates is done using transforms rather than by working with coordinates directly. In this section and the next, we will look at some of the higher-level capabilities that are typically provided by 2D graphics APIs.

2.2.1 基本形状

Basic Shapes

在图形API中,可以用一个命令绘制某些基本形状,而更复杂的形状则需要多个命令。什么样的形状被视为基本形状在不同的API中可能会有所不同。例如,在WebGL API中,唯一的基本形状是点、线和三角形。在本小节中,我将线条、矩形和椭圆视为基本形状。

所谓“线条”,实际上是指连接平面上两个给定点的直线段。一个简单的一像素宽的线段,没有反锯齿(antialiasing),是最基本的形状。可以通过给位于无限细几何线段上的像素上色来绘制它。绘制线段的算法必须决定要上色的确切像素。布雷森汉姆线段绘制算法(Bresenham's algorithm)是最早的计算机图形算法之一,实现了一种非常高效的过程。我不会在这里讨论这些低级细节,但如果您想开始学习图形硬件在低级别上实际需要做什么,值得查阅一下。无论如何,线条通常更复杂。反锯齿是一种复杂性。线宽是另一个复杂性。宽线可能实际上会被绘制成一个矩形。

线条可以具有其他影响其外观的属性(attributes)。一个问题是,宽线的末端应该发生什么?通过在线的末端添加一个圆角的“帽子”,可以改善外观。也可以通过将线延长线宽的一半来使用方形帽子。另一个问题是,当两条线作为较大形状的一部分相交时,线应该如何连接?许多图形系统支持由短划线和点组成的线条。这个示例展示了一些可能性:

pixel-coordinates

左边是三条宽线,分别没有帽子、圆帽和方帽。几何线段显示为虚线。(没有帽子的样式称为“平头(butt)”)右边是四条具有不同点划线样式的线条。中间是三种不同的线段连接样式:尖角、圆角和斜角。


基本矩形形状的边是垂直和水平的。(倾斜的矩形通常需要应用旋转(rotation)来制作。)这样的矩形可以通过两个点(x1,y1)和(x2,y2)来指定,这些点给出了矩形的对角线的端点之一。或者,可以给出宽度和高度,以及一个基准点(x,y)。在这种情况下,宽度和高度必须是正数,否则矩形为空。如果y从上到下增加,基准点(x,y)将是矩形的左上角,如果y从下到上增加,它将是矩形的左下角。

pixel-coordinates

假设您已经给定了点(x1,y1)和(x2,y2),并且您想要绘制由它们确定的矩形。假设您可以使用的唯一绘制矩形的命令是需要一个点(x,y)、一个宽度和一个高度的命令。对于该命令,x必须是x1和x2中较小的值,宽度可以计算为x1减去x2的绝对值。y和高度的计算方法类似。伪代码如下:

DrawRectangle from points (x1,y1) and (x2,y2):
    x = min( x1, x2 )
    y = min( y1, y2 )
    width = abs( x1 - x2 )
    height = abs( y1 - y2 )
    DrawRectangle( x, y, width, height )

矩形的一个常见变体是允许圆角。对于一个"round rect",角被替换为椭圆弧。圆角的程度可以通过给出椭圆的水平半径和垂直半径来指定。下面是一些圆角矩形的例子。对于右边的形状,椭圆的两个半径被显示出来:

pixel-coordinates

我的最后一个基本形状是椭圆(也称为椭圆形)。椭圆是一个有两个半径的闭合曲线。对于一个基本的椭圆,我们假设半径是垂直和水平的。可以通过给出刚好包含它的矩形来指定这样的椭圆。或者可以通过给出它的中心点和垂直半径以及水平半径的长度来指定。在这个示例中,左边的椭圆显示了它的包含矩形以及它的中心点和半径:

pixel-coordinates

右边的椭圆是一个圆。圆只是两个半径长度相等的椭圆。

如果椭圆不可用作基本形状,可以通过绘制大量的线段来近似。所需线段的数量取决于椭圆的大小。了解如何做到这一点是有用的。假设一个椭圆具有中心点(x,y),水平半径r1和垂直半径r2。数学上,椭圆上的点由以下公式给出:

( x + r1*cos(angle), y + r2*sin(angle) )

其中angle的值从0到360(如果角度以度为单位测量)或从0到2π(如果以弧度为单位测量)。这里的sin和cos是标准的正弦和余弦函数。为了得到一个椭圆的近似,我们可以使用这个公式生成一些点,然后用线段连接这些点。假设角度以弧度为单位测量,并且pi表示数学常数π,伪代码如下:

Draw Oval with center (x,y), horizontal radius r1, and vertical radius r2:
    for i = 0 to numberOfLines:
        angle1 = i * (2*pi/numberOfLines)
        angle2 = (i+1) * (2*pi/numberOfLines)
        a1 = x + r1*cos(angle1)
        b1 = y + r2*sin(angle1)
        a2 = x + r1*cos(angle2)
        b2 = y + r2*sin(angle2)
        Draw Line from (a1,b1) to (a2,b2)

对于一个圆,当然,r1 = r2。这是我们第一次使用正弦和余弦函数,但不会是最后一次。这些函数在计算机图形学中扮演重要角色,因为它们与圆、圆周运动和旋转有关。当我们在下一节中讨论变换时,我们将再次遇到它们。

这里有一个小的演示,您可以用它来尝试使用线段近似椭圆:

In a graphics API, there will be certain basic shapes that can be drawn with one command, whereas more complex shapes will require multiple commands. Exactly what qualifies as a basic shape varies from one API to another. In the WebGL API, for example, the only basic shapes are points, lines, and triangles. In this subsection, I consider lines, rectangles, and ovals to be basic.

By "line," I really mean line segment, that is a straight line segment connecting two given points in the plane. A simple one-pixel-wide line segment, without antialiasing, is the most basic shape. It can be drawn by coloring pixels that lie along the infinitely thin geometric line segment. An algorithm for drawing the line has to decide exactly which pixels to color. One of the first computer graphics algorithms, Bresenham's algorithm for line drawing, implements a very efficient procedure for doing so. I won't discuss such low-level details here, but it's worth looking them up if you want to start learning about what graphics hardware actually has to do on a low level. In any case, lines are typically more complicated. Antialiasing is one complication. Line width is another. A wide line might actually be drawn as a rectangle.

Lines can have other attributes, or properties, that affect their appearance. One question is, what should happen at the end of a wide line? Appearance might be improved by adding a rounded "cap" on the ends of the line. A square cap—that is, extending the line by half of the line width—might also make sense. Another question is, when two lines meet as part of a larger shape, how should the lines be joined? And many graphics systems support lines that are patterns of dashes and dots. This illustration shows some of the possibilities:

pixel-coordinates

On the left are three wide lines with no cap, a round cap, and a square cap. The geometric line segment is shown as a dotted line. (The no-cap style is called "butt.") To the right are four lines with different patterns of dots and dashes. In the middle are three different styles of line joins: mitered, rounded, and beveled.


The basic rectangular shape has sides that are vertical and horizontal. (A tilted rectangle generally has to be made by applying a rotation.) Such a rectangle can be specified with two points, (x1,y1) and (x2,y2), that give the endpoints of one of the diagonals of the rectangle. Alternatively, the width and the height can be given, along with a single base point, (x,y). In that case, the width and height have to be positive, or the rectangle is empty. The base point (x,y) will be the upper left corner of the rectangle if y increases from top to bottom, and it will be the lower left corner of the rectangle if y increases from bottom to top.

pixel-coordinates

Suppose that you are given points (x1,y1) and (x2,y2), and that you want to draw the rectangle that they determine. And suppose that the only rectangle-drawing command that you have available is one that requires a point (x,y), a width, and a height. For that command, x must be the smaller of x1 and x2, and the width can be computed as the absolute value of x1 minus x2. And similarly for y and the height. In pseudocode,

DrawRectangle from points (x1,y1) and (x2,y2):
    x = min( x1, x2 )
    y = min( y1, y2 )
    width = abs( x1 - x2 )
    height = abs( y1 - y2 )
    DrawRectangle( x, y, width, height )

A common variation on rectangles is to allow rounded corners. For a "round rect," the corners are replaced by elliptical arcs. The degree of rounding can be specified by giving the horizontal radius and vertical radius of the ellipse. Here are some examples of round rects. For the shape at the right, the two radii of the ellipse are shown:

pixel-coordinates

My final basic shape is the oval. (An oval is also called an ellipse.) An oval is a closed curve that has two radii. For a basic oval, we assume that the radii are vertical and horizontal. An oval with this property can be specified by giving the rectangle that just contains it. Or it can be specified by giving its center point and the lengths of its vertical radius and its horizontal radius. In this illustration, the oval on the left is shown with its containing rectangle and with its center point and radii:

pixel-coordinates

The oval on the right is a circle. A circle is just an oval in which the two radii have the same length.

If ovals are not available as basic shapes, they can be approximated by drawing a large number of line segments. The number of lines that is needed for a good approximation depends on the size of the oval. It's useful to know how to do this. Suppose that an oval has center point (x,y), horizontal radius r1, and vertical radius r2. Mathematically, the points on the oval are given by

( x + r1*cos(angle), y + r2*sin(angle) )

where angle takes on values from 0 to 360 if angles are measured in degrees or from 0 to 2π if they are measured in radians. Here sin and cos are the standard sine and cosine functions. To get an approximation for an oval, we can use this formula to generate some number of points and then connect those points with line segments. In pseudocode, assuming that angles are measured in radians and that pi represents the mathematical constant π,

Draw Oval with center (x,y), horizontal radius r1, and vertical radius r2:
    for i = 0 to numberOfLines:
        angle1 = i * (2*pi/numberOfLines)
        angle2 = (i+1) * (2*pi/numberOfLines)
        a1 = x + r1*cos(angle1)
        b1 = y + r2*sin(angle1)
        a2 = x + r1*cos(angle2)
        b2 = y + r2*sin(angle2)
        Draw Line from (a1,b1) to (a2,b2)

For a circle, of course, you would just have r1 = r2. This is the first time we have used the sine and cosine functions, but it won't be the last. These functions play an important role in computer graphics because of their association with circles, circular motion, and rotation. We will meet them again when we talk about transforms in the next section.

Here's a little demo that you can use to experiment with using line segments to approximate ovals:

2.2.2 描边和填充

Stroke and Fill

在绘图中,有两种方式可以使形状可见。您可以描边(stroke)它,或者如果它是一个封闭的形状,比如矩形或椭圆,您可以填充(fill)它。描边一条线就像沿着线条拖动一支笔。描边一个矩形或椭圆就像沿着它的边界拖动一支笔。填充一个形状意味着给包含在该形状内的所有点上色。可以同时描边和填充同一个形状;在这种情况下,形状的内部和外轮廓可以有不同的外观。

当一个形状与自身相交时,就像下面插图中的两个形状一样,不太清楚应该如何定义形状的内部。事实上,至少有两个不同的规则可以用来填充这样的形状。这两个规则都基于一个叫做“绕数(winding number)”的东西。关于一个点的绕数大致是指形状以正方向绕该点旋转的次数,这里我认为正方向是逆时针方向。当绕数为负数时,表示绕数方向相反。在插图中,左边的形状按照所示方向进行描绘,并且每个区域的绕数在区域内显示为一个数字。

pixel-coordinates

这些形状也用两种填充规则进行了填充。对于中间的形状,填充规则是对具有非零绕数的任何区域进行着色。对于右边显示的形状,规则是对绕数为奇数的任何区域进行着色;绕数为偶数的区域不填充。

仍然有一个问题,即形状应该用什么来填充。当然,可以用颜色来填充,但也可以使用其他类型的填充,包括图案(patterns)渐变(gradients)。图案是一个图像,通常是一个小图像。当用于填充形状时,图案可以根据需要水平和垂直重复,以覆盖整个形状。渐变类似,它是一种让颜色从一个点到另一个点变化的方式,但不是从图像中获取颜色,而是计算得出。基本思想有很多变化,但总是有一条线段沿着它的颜色变化。颜色在线段的端点处指定,可能还在其他点处指定;在这些点之间,颜色进行插值(interpolated)。颜色也可以外推到包含线段的线上的其他点,但位于线段之外;这可以通过从线段重复图案或者简单地从最近的端点延伸颜色来完成。对于线性渐变(linear gradient),颜色沿着与基本线段垂直的线保持恒定,因此您会得到以该方向的实色线条。在径向渐变(radial gradient)中,颜色沿着以线段的一个端点为中心的圆保持恒定。这还没有穷尽所有可能性。为了让您了解图案和渐变的外观,这里有一个形状,用两种渐变和两种图案填充:

pixel-coordinates

第一个形状使用仅由两种颜色定义的简单线性渐变进行填充,而第二个形状使用径向渐变进行填充。

图案和渐变不一定局限于填充形状。毕竟,描边一个形状就是填充沿着形状边界的像素带,可以用渐变或图案来实现,而不是用纯色填充。

最后,我要提到,为了绘制文本,可以将文本视为一个形状。形状的边界是字符的轮廓。文本通过填充该形状来绘制。在某些图形系统中,还可以描绘定义文本的形状的轮廓。在下面的插图中,顶部显示了字符串"Graphics",使用图案进行填充,下方则使用渐变进行填充,并用纯黑色描边:

pixel-coordinates

There are two ways to make a shape visible in a drawing. You can stroke it. Or, if it is a closed shape such as a rectangle or an oval, you can fill it. Stroking a line is like dragging a pen along the line. Stroking a rectangle or oval is like dragging a pen along its boundary. Filling a shape means coloring all the points that are contained inside that shape. It's possible to both stroke and fill the same shape; in that case, the interior of the shape and the outline of the shape can have a different appearance.

When a shape intersects itself, like the two shapes in the illustration below, it's not entirely clear what should count as the interior of the shape. In fact, there are at least two different rules for filling such a shape. Both are based on something called the winding number. The winding number of a shape about a point is, roughly, how many times the shape winds around the point in the positive direction, which I take here to be counterclockwise. Winding number can be negative when the winding is in the opposite direction. In the illustration, the shapes on the left are traced in the direction shown, and the winding number about each region is shown as a number inside the region.

pixel-coordinates

The shapes are also shown filled using the two fill rules. For the shapes in the center, the fill rule is to color any region that has a non-zero winding number. For the shapes shown on the right, the rule is to color any region whose winding number is odd; regions with even winding number are not filled.

There is still the question of what a shape should be filled with. Of course, it can be filled with a color, but other types of fill are possible, including patterns and gradients. A pattern is an image, usually a small image. When used to fill a shape, a pattern can be repeated horizontally and vertically as necessary to cover the entire shape. A gradient is similar in that it is a way for color to vary from point to point, but instead of taking the colors from an image, they are computed. There are a lot of variations to the basic idea, but there is always a line segment along which the color varies. The color is specified at the endpoints of the line segment, and possibly at additional points; between those points, the color is interpolated. The color can also be extrapolated to other points on the line that contains the line segment but lying outside the line segment; this can be done either by repeating the pattern from the line segment or by simply extending the color from the nearest endpoint. For a linear gradient, the color is constant along lines perpendicular to the basic line segment, so you get lines of solid color going in that direction. In a radial gradient, the color is constant along circles centered at one of the endpoints of the line segment. And that doesn't exhaust the possibilities. To give you an idea what patterns and gradients can look like, here is a shape, filled with two gradients and two patterns:

pixel-coordinates

The first shape is filled with a simple linear gradient defined by just two colors, while the second shape uses a radial gradient.

Patterns and gradients are not necessarily restricted to filling shapes. Stroking a shape is, after all, the same as filling a band of pixels along the boundary of the shape, and that can be done with a gradient or a pattern, instead of with a solid color.

Finally, I will mention that a string of text can be considered to be a shape for the purpose of drawing it. The boundary of the shape is the outline of the characters. The text is drawn by filling that shape. In some graphics systems, it is also possible to stroke the outline of the shape that defines the text. In the following illustration, the string "Graphics" is shown, on top, filled with a pattern and, below that, filled with a gradient and stroked with solid black:

pixel-coordinates

2.2.3 多边形、曲线和路径

Polygons, Curves, and Paths

对于一个图形API来说,包含每种可能的形状作为基本形状是不可能的,但通常可以通过某种方式创建更复杂的形状。例如,考虑多边形(polygons)。多边形是由一系列线段组成的封闭形状。每个线段的端点与下一个线段的端点连接,最后一个线段连接回第一个线段。端点被称为多边形的顶点,可以通过列出顶点来定义一个多边形。

在一个正多边形(regular polygon)中,所有的边长相等,所有边之间的角度也相等。正方形和等边三角形是正多边形的例子。凸多边形(convex polygon)具有这样的属性:无论两个点是否在多边形内部或边上,连接这些点的整条线段也在多边形内部或边上。直观地说,凸多边形在边界上没有"凹陷"。(凹陷是任何形状的属性,不仅仅是多边形的属性。)

pixel-coordinates

有时候,多边形需要是"简单"的,这意味着多边形没有自相交。也就是说,所有的顶点都是不同的,一条边只能在其端点处与另一条边相交。而且通常要求多边形是"平面"的,也就是说所有的顶点都位于同一个平面上。(当然,在2D图形中,一切都位于同一个平面上,所以这不是一个问题。但在3D中就成为一个问题。)

那么我们应该如何绘制多边形呢?也就是说,在绘图API中,我们希望具备哪些功能来绘制多边形。一种可能性是具备绘制线段和填充多边形的命令,其中多边形的顶点可以作为点的数组或者作为x坐标数组加上y坐标数组来给出。事实上,有时候确实是这样做的;例如,Java图形API就包含了这样的命令。另一种更灵活的方法是引入"路径"的概念。Java、SVG和HTML画布API都支持这个概念。路径是一个通用的形状,可以包含线段和曲线段。线段可以连接到其他线段的端点,也可以不连接。通过给出一系列命令来创建路径,这些命令基本上告诉了如何移动画笔来绘制路径。在创建路径时,有一个表示画笔当前位置的点。有一个命令可以移动画笔而不绘制,还有用于绘制各种类型线段的命令。对于绘制多边形,我们需要的命令包括:

  • createPath() — 开始一个新的空路径
  • moveTo(x,y) — 将画笔移动到点(x,y),而不添加线段到路径中;也就是说,不绘制任何东西
  • lineTo(x,y) — 添加一个线段到路径中,该线段从当前画笔位置开始,到点(x,y)结束,并将画笔移动到(x,y)
  • closePath() — 添加一条线段从当前画笔位置返回到起始点,除非画笔已经在起始点,这样就形成了一个封闭的路径。

(对于closePath(),我需要定义"起始点"。一个路径可以由多个"子路径"组成。一个子路径由一系列连接的线段组成。moveTo()总是开始一个新的子路径。closePath()结束当前线段并隐式地开始一个新的线段。所以"起始点"指的是在最近的moveTo()closePath()之后画笔的位置。)

假设我们想要一个表示三角形的路径,其顶点分别为(100,100)、(300,100)和(200,200)。我们可以使用以下命令实现:

createPath()
moveTo(100, 100)
lineTo(300, 100)
lineTo(200, 200)
closePath()

最后的closePath()命令也可以替换为lineTo(100,100),将画笔移回到第一个顶点。

路径表示一个抽象的几何对象。创建路径并不会使其在屏幕上可见。一旦我们有了路径,为了使其可见,我们需要额外的命令来描边和填充路径。

在本节的前面部分,我们看到了如何通过绘制一个具有大量边的多边形来近似椭圆。在那个例子中,我将每条边绘制为一个单独的线段,所以实际上我们得到的是一堆单独的线段而不是一个多边形。这样的东西无法填充。最好的方法是用多边形路径来近似椭圆。对于一个以中心点(x,y)和半径r1和r2的椭圆来说:

createPath()
moveTo(x + r1, y)
for i = 1 to numberOfPoints-1
    angle = i * (2*pi/numberOfLines)
    lineTo(x + r1*cos(angle), y + r2*sin(angle))
closePath()

使用这个路径,我们既可以绘制填充的椭圆,也可以绘制描边。即使我们只想绘制多边形的轮廓,将多边形创建为路径而不是绘制单独的线段也是更好的选择。通过路径,计算机知道这些边是单个形状的一部分。这使得可以控制相邻边之间的"连接"的外观,正如本节前面所提到的。


我之前提到路径可以包含除了线段之外的其他类型的段。例如,可能可以将圆弧作为一个段包含进来。另一种类型的曲线是贝塞尔曲线(Bezier curve)。贝塞尔曲线可以用来创建非常通用的曲线形状。它们相对直观,所以常常在允许用户交互式设计曲线的程序中使用。数学上,贝塞尔曲线由参数多项式方程定义,但你不需要理解这意味着什么就能使用它们。常见的贝塞尔曲线有两种类型,分别是三次贝塞尔曲线和二次贝塞尔曲线;它们分别由三次和二次多项式定义。当一般术语"贝塞尔曲线"被使用时,通常指的是三次贝塞尔曲线。

一个三次贝塞尔曲线段由两个端点和两个控制点(control points)定义。要理解它是如何工作的,最好想象一下画笔如何绘制曲线段。画笔从第一个端点开始,朝着第一个控制点的方向。控制点与端点的距离控制了画笔开始绘制曲线的速度。第二个控制点控制了画笔在到达曲线的第二个端点时的方向和速度。满足这些条件的三次曲线是唯一的。

pixel-coordinates

上图显示了三个三次贝塞尔曲线段。右侧的两个曲线段在一个端点处连接起来形成一个更长的曲线。曲线以粗黑线绘制。端点显示为黑色点,控制点显示为蓝色方块,每个控制点与相应的端点之间用细红线连接。(通常,只会绘制曲线,除非在允许用户手动编辑曲线的界面中。)请注意,在一个端点处,曲线段与连接端点和控制点的线相切。请注意,两个曲线段相交处可能会有一个尖锐的点或拐角。然而,如果选择了适当的控制点,一个段会平滑地过渡到下一个段。

通过一些实际操作经验,这一切都会更容易理解。这个交互式演示允许您通过拖动端点和控制点来编辑三次贝塞尔曲线段:

当将一个三次贝塞尔曲线段添加到路径中时,路径的当前画笔位置充当段的第一个端点。添加段到路径的命令必须指定两个控制点和第二个端点。一个典型的命令可能是

cubicCurveTo( cx1, cy1, cx2, cy2, x, y )

这将从当前位置到点(x,y)添加一条曲线,使用(cx1,cy1)和(cx2,cy2)作为控制点。也就是说,画笔离开当前位置朝向(cx1,cy1),并以(cx2,cy2)的方向到达点(x,y)。

二次贝塞尔曲线段与三次版本类似,但在二次情况下,段只有一个控制点。曲线离开第一个端点朝着控制点的方向,然后从控制点的方向到达第二个端点。这种情况下的曲线将是一个抛物线的一部分。

同样,通过一些实际操作经验,这将更容易理解。请尝试这个交互式演示:

It is impossible for a graphics API to include every possible shape as a basic shape, but there is usually some way to create more complex shapes. For example, consider polygons. A polygon is a closed shape consisting of a sequence of line segments. Each line segment is joined to the next at its endpoint, and the last line segment connects back to the first. The endpoints are called the vertices of the polygon, and a polygon can be defined by listing its vertices.

In a regular polygon, all the sides are the same length and all the angles between sides are equal. Squares and equilateral triangles are examples of regular polygons. A convex polygon has the property that whenever two points are inside or on the polygon, then the entire line segment between those points is also inside or on the polygon. Intuitively, a convex polygon has no "indentations" along its boundary. (Concavity can be a property of any shape, not just of polygons.)

pixel-coordinates

Sometimes, polygons are required to be "simple," meaning that the polygon has no self-intersections. That is, all the vertices are different, and a side can only intersect another side at its endpoints. And polygons are usually required to be "planar," meaning that all the vertices lie in the same plane. (Of course, in 2D graphics, everything lies in the same plane, so this is not an issue. However, it does become an issue in 3D.)

How then should we draw polygons? That is, what capabilities would we like to have in a graphics API for drawing them. One possibility is to have commands for stroking and for filling polygons, where the vertices of the polygon are given as an array of points or as an array of x-coordinates plus an array of y-coordinates. In fact, that is sometimes done; for example, the Java graphics API includes such commands. Another, more flexible, approach is to introduce the idea of a "path." Java, SVG, and the HTML canvas API all support this idea. A path is a general shape that can include both line segments and curved segments. Segments can, but don't have to be, connected to other segments at their endpoints. A path is created by giving a series of commands that tell, essentially, how a pen would be moved to draw the path. While a path is being created, there is a point that represents the pen's current location. There will be a command for moving the pen without drawing, and commands for drawing various kinds of segments. For drawing polygons, we need commands such as

  • createPath() — start a new, empty path
  • moveTo(x,y) — move the pen to the point (x,y), without adding a segment to the path; that is, without drawing anything
  • lineTo(x,y) — add a line segment to the path that starts at the current pen location and ends at the point (x,y), and move the pen to (x,y)
  • closePath() — add a line segment from the current pen location back to the starting point, unless the pen is already there, producing a closed path.

(For closePath, I need to define "starting point." A path can be made up of "subpaths" A subpath consists of a series of connected segments. A moveTo always starts a new subpath. A closePath ends the current segment and implicitly starts a new one. So "starting point" means the position of the pen after the most recent moveTo or closePath.)

Suppose that we want a path that represents the triangle with vertices at (100,100), (300,100), and (200, 200). We can do that with the commands

createPath()
moveTo( 100, 100 )
lineTo( 300, 100 )
lineTo( 200, 200 )
closePath()

The closePath command at the end could be replaced by lineTo(100,100), to move the pen back to the first vertex.

A path represents an abstract geometric object. Creating one does not make it visible on the screen. Once we have a path, to make it visible we need additional commands for stroking and filling the path.

Earlier in this section, we saw how to approximate an oval by drawing, in effect, a polygon with a large number of sides. In that example, I drew each side as a separate line segment, so we really had a bunch of separate lines rather than a polygon. There is no way to fill such a thing. It would be better to approximate the oval with a polygonal path. For an oval with center (x,y) and radii r1 and r2:

createPath()
moveTo( x + r1, y )
for i = 1 to numberOfPoints-1
    angle = i * (2*pi/numberOfLines)
    lineTo( x + r1*cos(angle), y + r2*sin(angle) )
closePath()

Using this path, we could draw a filled oval as well as stroke it. Even if we just want to draw the outline of a polygon, it's still better to create the polygon as a path rather than to draw the line segments as separate sides. With a path, the computer knows that the sides are part of single shape. This makes it possible to control the appearance of the "join" between consecutive sides, as noted earlier in this section.


I noted above that a path can contain other kinds of segments besides lines. For example, it might be possible to include an arc of a circle as a segment. Another type of curve is a Bezier curve. Bezier curves can be used to create very general curved shapes. They are fairly intuitive, so that they are often used in programs that allow users to design curves interactively. Mathematically, Bezier curves are defined by parametric polynomial equations, but you don't need to understand what that means to use them. There are two kinds of Bezier curve in common use, cubic Bezier curves and quadratic Bezier curves; they are defined by cubic and quadratic polynomials respectively. When the general term "Bezier curve" is used, it usually refers to cubic Bezier curves.

A cubic Bezier curve segment is defined by the two endpoints of the segment together with two control points. To understand how it works, it's best to think about how a pen would draw the curve segment. The pen starts at the first endpoint, headed in the direction of the first control point. The distance of the control point from the endpoint controls the speed of the pen as it starts drawing the curve. The second control point controls the direction and speed of the pen as it gets to the second endpoint of the curve. There is a unique cubic curve that satisfies these conditions.

pixel-coordinates

The illustration above shows three cubic Bezier curve segments. The two curve segments on the right are connected at an endpoint to form a longer curve. The curves are drawn as thick black lines. The endpoints are shown as black dots and the control points as blue squares, with a thin red line connecting each control point to the corresponding endpoint. (Ordinarily, only the curve would be drawn, except in an interface that lets the user edit the curve by hand.) Note that at an endpoint, the curve segment is tangent to the line that connects the endpoint to the control point. Note also that there can be a sharp point or corner where two curve segments meet. However, one segment will merge smoothly into the next if control points are properly chosen.

This will all be easier to understand with some hands-on experience. This interactive demo lets you edit cubic Bezier curve segments by dragging their endpoints and control points:

When a cubic Bezier curve segment is added to a path, the path's current pen location acts as the first endpoint of the segment. The command for adding the segment to the path must specify the two control points and the second endpoint. A typical command might look like

cubicCurveTo( cx1, cy1, cx2, cy2, x, y )

This would add a curve from the current location to point (x,y), using (cx1,cy1) and (cx2,cy2) as the control points. That is, the pen leaves the current location heading towards (cx1,cy1), and it ends at the point (x,y), arriving there from the direction of (cx2,cy2).

Quadratic Bezier curve segments are similar to the cubic version, but in the quadratic case, there is only one control point for the segment. The curve leaves the first endpoint heading in the direction of the control point, and it arrives at the second endpoint coming from the direction of the control point. The curve in this case will be an arc of a parabola.

Again, this is easier to understand this with some hands-on experience. Try this interactive demo: