跳转至

第5节: Java 绘制2D

Java Graphics2D

在本章的其余部分,我们将看一些二维图形的具体实现。这里有一些新的想法,但大多数情况下,您将看到我们已经介绍的一般概念如何在几个实际图形系统中使用。

在本节中,我们的重点是Java编程语言。Java仍然是最受欢迎的编程语言之一。其标准桌面版本包括一个复杂的2D图形API,这是我们在这里讨论的主题。在阅读本节之前,您应该已经了解Java编程的基础知识。但即使您不了解,您也应该能够理解大部分关于图形API本身的讨论。(在附录A中的Section A.1中可以找到Java的基本介绍。)

这里讨论的图形API是Swing的一部分,Swing是用于图形用户界面编程的API,包含在Java的标准发行版中。现在许多Java程序都是使用名为JavaFX的另一种API编写的,它不是标准发行版的一部分。本教材不讨论JavaFX。实际上,JavaFX的图形API与HTML画布图形的API非常相似,这在Section 2.6中讨论过。

Java的原始版本具有更小的图形API。它严格侧重于像素,并且仅使用整数坐标。该API有用于描绘和填充各种基本形状(包括线条、矩形、椭圆和多边形,尽管Java使用draw而不是stroke这个术语)的子例程。其绘图操作的含义规定在像素级别非常精确。整数坐标被定义为参考像素之间的线条。例如,一个12x8像素网格的x坐标从0到12,y坐标从0到8,如下所示。编号的是像素之间的线条,而不是像素本身。

命令fillRect(3,2,5,3)会填充左上角位于(3,2)、宽度为5、高度为3的矩形,如上图左侧所示。命令drawRect(3,2,5,3)在概念上围绕该矩形的轮廓绘制一个“笔”。但是,这支笔是一个1像素的正方形,而沿轮廓移动的是笔的左上角。当笔沿矩形的右边缘移动时,该边缘右侧的像素被着色;当笔沿底边移动时,底边下方的像素被着色。结果如上图右侧所示。我在这里的重点不是为了纠结细节,而是要指出,对绘图操作的含义有精确规定可以让您在像素级别上有非常精细的控制。

Java的原始图形不支持实数坐标、变换、抗锯齿或渐变等功能。在Java首次引入几年后,添加了一个支持所有这些功能的新图形API。我们将在这里看一下这个更高级的API。

In the rest of this chapter, we look at specific implementations of two-dimensional graphics. There are a few new ideas here, but mostly you will see how the general concepts that we have covered are used in several real graphics systems.

In this section, our focus is on the Java programming language. Java remains one of the most popular programming languages. Its standard desktop version includes a sophisticated 2D graphics API, which is our topic here. Before reading this section, you should already know the basics of Java programming. But even if you don't, you should be able to follow most of the discussion of the graphics API itself. (See Section A.1 in Appendix A for a very basic introduction to Java.)

The graphics API that is discussed here is part of Swing, an API for graphical user interface programming that is included as part of the standard distribution of Java. Many Java programs are now written using an alternative API called JavaFX, which is not part of the standard distribution. JavaFX is not discussed in this textbook. Its graphics API is, in fact, quite similar to the API for HTML canvas graphics, which is discussed in Section 2.6.

The original version of Java had a much smaller graphics API. It was tightly focused on pixels, and it used only integer coordinates. The API had subroutines for stroking and filling a variety of basic shapes, including lines, rectangles, ovals, and polygons (although Java uses the term draw instead of stroke). Its specification of the meaning of drawing operations was very precise on the pixel level. Integer coordinates are defined to refer to the lines between pixels. For example, a 12-by-8 pixel grid has x-coordinates from 0 to 12 and y-coordinates from 0 to 8, as shown below. The lines between pixels are numbered, not the pixels.

pixel-coordinates

The command fillRect(3,2,5,3) fills the rectangle with upper left corner at (3,2), with width 5, and with height 3, as shown on the left above. The command drawRect(3,2,5,3) conceptually drags a "pen" around the outline of this rectangle. However, the pen is a 1-pixel square, and it is the upper left corner of the pen that moves along the outline. As the pen moves along the right edge of the rectangle, the pixels to the right of that edge are colored; as the pen moves along the bottom edge, the pixels below the edge are colored. The result is as shown on the right above. My point here is not to belabor the details, but to point out that having a precise specification of the meaning of graphical operations gives you very fine control over what happens on the pixel level.

Java's original graphics did not support things like real-number coordinates, transforms, antialiasing, or gradients. Just a few years after Java was first introduced, a new graphics API was added that does support all of these. It is that more advanced API that we will look at here.

2.5.1 绘制2D

Graphics2D

Java是一种面向对象的语言。其API被定义为一个大型的类集合,原始图形API中的实际绘图操作大多包含在名为Graphics的类中。在更新的Swing API中,绘图操作是位于名为Graphics2D的类中的方法,它是Graphics的子类,因此所有原始的绘图操作仍然可用。(在Java中,一个类包含在称为“包”的类集合中。例如,GraphicsGraphics2D位于名为java.awt的包中。定义形状和变换的类位于名为java.awt.geom的包中。)

图形系统需要一个绘制的位置。在Java中,绘图表面通常是JPanel类的对象,它代表屏幕上的一个矩形区域。JPanel类有一个名为paintComponent()的方法来绘制其内容。要创建一个绘图表面,您可以创建JPanel的子类并为其paintComponent()方法提供定义。所有绘图都应该在paintComponent()内完成;当需要更改绘图的内容时,您可以调用面板的repaint()方法来触发对paintComponent()的调用。paintComponent()方法有一个类型为Graphics的参数,但实际传递给方法的参数是Graphics2D类型的对象,它可以被类型转换为Graphics2D以获取对更高级别图形功能的访问。因此,paintComponent()方法的定义通常看起来像这样:

protected void paintComponent( Graphics g ) {
    Graphics2D g2;
    g2 = (Graphics2D)g;  // 将参数转换为Graphics2D类型。
    .
    .  // 使用g2绘图。
    .
}

在本节的其余部分,我将假设g2是一个类型为Graphics2D的变量,并讨论您可以使用它做的一些事情。作为第一个示例,我注意到Graphics2D支持抗锯齿,但默认情况下未启用。可以在图形上下文g2中使用以下相当令人生畏的命令启用它:

g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                            RenderingHints.VALUE_ANTIALIAS_ON);

对于在完整的Java程序中进行简单图形绘制的示例,您可以查看样本程序java2d/GraphicsStarter.javajava2d/AnimationStarter.java。它们分别提供了使用Graphics2D绘制静态和动画图像的非常简单的框架。程序java2d/EventsStarter.java是一个类似的框架,用于处理图形程序中的鼠标和键事件。如果您想探索Java图形,您可以将这些程序作为一些实验的基础。

Java is an object-oriented language. Its API is defined as a large set of classes, The actual drawing operations in the original graphics API were mostly contained in the class named Graphics. In the newer Swing API, drawing operations are methods in a class named Graphics2D, which is a subclass of Graphics, so that all the original drawing operations are still available. (A class in Java is contained in a collection of classes known as a "package." Graphics and Graphics2D, for example, are in the package named java.awt. Classes that define shapes and transforms are in a package named java.awt.geom.)

A graphics system needs a place to draw. In Java, the drawing surface is often an object of the class JPanel, which represents a rectangular area on the screen. The JPanel class has a method named paintComponent() to draw its content. To create a drawing surface, you can create a subclass of JPanel and provide a definition for its paintComponent() method. All drawing should be done inside paintComponent(); when it is necessary to change the contents of the drawing, you can call the panel's repaint() method to trigger a call to paintComponent(). The paintComponent() method has a parameter of type Graphics, but the parameter that is passed to the method is actually an object of type Graphics2D, and it can be type-cast to Graphics2D to obtain access to the more advanced graphics capabilities. So, the definition of the paintComponent() method usually looks something like this:

protected void paintComponent( Graphics g ) {
    Graphics2D g2;
    g2 = (Graphics2D)g;  // Type-cast the parameter to Graphics2D.
    .
    .  // Draw using g2.
    .
}

In the rest of this section, I will assume that g2 is a variable of type Graphics2D, and I will discuss some of the things that you can do with it. As a first example, I note that Graphics2D supports antialiasing, but it is not turned on by default. It can be enabled in a graphics context g2 with the rather intimidating command

g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                            RenderingHints.VALUE_ANTIALIAS_ON);

For simple examples of graphics in complete Java programs, you can look at the sample programs java2d/GraphicsStarter.java and java2d/AnimationStarter.java. They provide very minimal frameworks for drawing static and animated images, respectively, using Graphics2D. The program java2d/EventsStarter.java is a similar framework for working with mouse and key events in a graphics program. You can use these programs as the basis for some experimentation if you want to explore Java graphics.

2.5.2 形状

Shapes

使用原始的Graphics类进行绘制时,使用整数坐标,单位为像素。这在标准坐标系中效果很好,但在使用实数坐标时不适用,因为在这样的坐标系中,度量单位将不等于一个像素。我们需要能够使用实数来指定形状。Java包java.awt.geom提供了支持使用实数坐标定义的形状的功能。例如,该包中的Line2D类表示以一对实数为端点的线段。

现在,Java有两种实数类型:doublefloatdouble类型可以表示比float更大范围的数字,并且具有更多的有效位数,double是更常用的类型。实际上,doubles在Java中更容易使用。然而,float值通常在图形应用中具有足够的精度,并且它们具有在内存中占用更少空间的优势。此外,计算机图形硬件通常在内部使用float值。

因此,考虑到这些因素,java.awt.geom包实际上为每个形状提供了两个版本,一个使用float类型的坐标,另一个使用double类型的坐标。这是以一种相当奇怪的方式实现的。以Line2D为例,Line2D类本身是一个抽象类。它有两个子类,一个表示使用float坐标的线,另一个使用double坐标。最奇怪的部分是,这些子类被定义为Line2D的嵌套类:Line2D.Float和Line2D.Double。这意味着您可以声明一个类型为Line2D的变量,但要创建一个对象,您需要使用Line2D.DoubleLine2D.Float

Line2D line1, line2;
line1 = new Line2D.Double(1,2,5,7); // 从 (1.0,2.0) 到 (5.0,7.0) 的线段
line2 = new Line2D.Float(2.7F,3.1F,1.5F,7.1F); // 从 (2.7,3.1) 到 (1.5,7.1) 的线段

注意,在Java中使用float类型的常量时,您必须将"F"作为后缀添加到值后面。这是为什么doubles在Java中更容易的一个原因。为简单起见,您可能希望坚持使用Line2D.Double。然而,Line2D.Float可能会提供稍微更好的性能。


让我们来看看java.awt.geom中的一些其他类。抽象类Point2D—以及它的具体子类Point2D.DoublePoint2D.Float—表示二维空间中的一个点,由两个实数坐标指定。点不是一个形状;您无法对其进行填充或描边。可以用两个实数构造一个点("new Point2D.Double(1.2,3.7)")。如果p是类型为Point2D的变量,您可以使用p.getX()和p.getY()来检索其坐标,并且您可以使用p.setX(x)、p.setY(y)或p.setLocation(x,y)来设置其坐标。如果pd是类型为Point2D.Double的变量,您还可以直接引用坐标,如pd.x和pd.y(对于Point2D.Float也是如此)。java.awt.geom中的其他类提供了类似的多种方式来操纵其属性,我不会在这里尝试列出它们所有。

有各种各样的类表示几何形状,包括Line2D、Rectangle2D、RoundRectangle2D、Ellipse2D、Arc2D和Path2D。所有这些都是抽象类,每个类包含一对子类,例如Rectangle2D.Double和Rectangle2D.Float。一些形状,比如矩形,具有可以填充的内部;这样的形状也有可以描边的轮廓。一些形状,比如线段,纯粹是一维的,只能描边。

除了线段,矩形可能是最简单的形状。Rectangle2D有一个角点(x,y),一个宽度和一个高度,并且可以根据这些数据构造("new Rectangle2D.Double(x,y,w,h)")。角点(x,y)指定了矩形中的最小x值和y值。对于通常的像素坐标系,(x,y)是左上角。然而,在最小y值在底部的坐标系中,(x,y)将是左下角。矩形的边平行于坐标轴。类型为Rectangle2D.Double的变量r具有公共实例变量r.x、r.y、r.width和r.height。如果宽度或高度小于或等于零,当矩形被填充或描边时将不会绘制任何内容。一个常见的任务是从两个角点(x1,y1)和(x2,y2)定义一个矩形。这可以通过创建一个高度和宽度均为零的矩形,然后将第二个点添加到矩形中来完成。将一个点添加到矩形会使矩形增长足够以包括该点:

Rectangle2D.Double r = new Rectangle2D.Double(x1,y1,0,0);
r.add(x2,y2);

Line2DEllipse2DRoundRectangle2DArc2D创建其他基本形状,并且工作原理类似于Rectangle2D。您可以查看Java API文档以获取详细信息。

Path2D类更有趣。它表示由线段和贝塞尔曲线组成的一般路径。路径是使用类似于在子节2.2.3中讨论过的moveTo和lineTo子例程创建的。要创建路径,首先构造一个类型为Path2D.Double(或Path2D.Float)的对象:

Path2D.Double p = new Path2D.Double();

当首次创建路径p时,它是空的。通过沿着要创建的路径移动一个想象的“笔”来构造路径。方法p.moveTo(x,y)将笔移动到点(x,y)而不绘制任何内容。它用于指定路径的初始点或路径的新部分的起始点。方法p.lineTo(x,y)绘制一条从当前笔位置到(x,y)的直线,将笔留在(x,y)处。方法p.close()可用于通过绘制一条线返回到其起始点来关闭路径(或路径的当前部分)。例如,以下代码创建了一个顶点分别位于(0,5)、(2,-3)和(-4,1)的三角形:

Path2D.Double p = new Path2D.Double();
p.moveTo(0,5);
p.lineTo(2,-3);
p.lineTo(-4,1);
p.close();

您还可以向Path2D添加贝塞尔曲线段。贝塞尔曲线在子节2.2.3中已经讨论过了。您可以使用方法将三次贝塞尔曲线添加到路径Path2D p中

p.curveTo( cx1, cy1, cx2, cy2, x, y );

这将添加一个曲线段,从当前笔位置开始,到(x,y)结束,并使用(cx1,cy1)和(cx2,cy2)作为曲线的两个控制点。添加二次贝塞尔曲线段到路径的方法是quadTo。它只需要一个控制点:

p.quadTo( cx, cy, x, y );

当路径与自身相交时,其内部是通过查看缠绕数确定的,如子节2.2.2中所讨论的。确定点是否在内部有两种可能的规则:询问围绕该点的曲线的缠绕数是否为非零,或者询问是否为奇数。您可以使用以下方法设置Path2D p使用的缠绕规则:

p.setWindingRule( Path2D.WIND_NON_ZERO );
p.setWindingRule( Path2D.WIND_EVEN_ODD );

默认是WIND_NON_ZERO。

最后,我要注意的是可以在图形上下文中绘制图像的副本。图像可以从文件加载或由程序创建。我稍后在本节中讨论第二种可能性。图像由类型为Image的对象表示。实际上,我在这里假设对象是BufferedImage类型,它是Image的子类。如果img是这样的对象,则

g2.drawImage( img, x, y, null );

将在点(x,y)处绘制图像的左上角。(第四个参数很难解释,但对于BufferedImage,应将其指定为null。)这将以其自然宽度和高度绘制图像,但可以在方法中指定不同的宽度和高度:

g2.drawImage( img, x, y, width, height, null );

还有一个绘制文本字符串的方法。该方法指定了字符串和字符串的基点。(基点是字符串的左下角,忽略了像字母"g"的尾巴之类的“下沉”部分。)例如,

g2.drawString( "Hello World", 100, 50 );

图像和字符串与其他形状一样受到变换的影响。变换是获得旋转文本和图像的唯一方法。例如,当对一些文本和图像应用旋转时,可能会发生以下情况:

pixel-coordinates

Drawing with the original Graphics class is done using integer coordinates, with the measurement given in pixels. This works well in the standard coordinate system, but is not appropriate when real-number coordinates are used, since the unit of measure in such a coordinate system will not be equal to a pixel. We need to be able to specify shapes using real numbers. The Java package java.awt.geom provides support for shapes defined using real number coordinates. For example, the class Line2D in that package represents line segments whose endpoints are given as pairs of real numbers.

Now, Java has two real number types: double and float. The double type can represent a larger range of numbers than float, with a greater number of significant digits, and double is the more commonly used type. In fact, doubles are simply easier to use in Java. However, float values generally have enough accuracy for graphics applications, and they have the advantage of taking up less space in memory. Furthermore, computer graphics hardware often uses float values internally.

So, given these considerations, the java.awt.geom package actually provides two versions of each shape, one using coordinates of type float and one using coordinates of type double. This is done in a rather strange way. Taking Line2D as an example, the class Line2D itself is an abstract class. It has two subclasses, one that represents lines using float coordinates and one using double coordinates. The strangest part is that these subclasses are defined as nested classes inside Line2D: Line2D.Float and Line2D.Double. This means that you can declare a variable of type Line2D, but to create an object, you need to use Line2D.Double or Line2D.Float:

Line2D line1, line2;
line1 = new Line2D.Double(1,2,5,7); // Line from (1.0,2.0) to (5.0,7.0)
line2 = new Line2D.Float(2.7F,3.1F,1.5F,7.1F); // (2.7,3.1) to (1.5,7.1)

Note that when using constants of type float in Java, you have to add "F" as a suffix to the value. This is one reason why doubles are easier in Java. For simplicity, you might want to stick to using Line2D.Double. However, Line2D.Float might give slightly better performance.


Let's take a look at some of the other classes from java.awt.geom. The abstract class Point2D—with its concrete subclasses Point2D.Double and Point2D.Float—represents a point in two dimensions, specified by two real number coordinates. A point is not a shape; you can't fill or stroke it. A point can be constructed from two real numbers ("new Point2D.Double(1.2,3.7)"). If p is a variable of type Point2D, you can use p.getX() and p.getY() to retrieve its coordinates, and you can use p.setX(x), p.setY(y), or p.setLocation(x,y) to set its coordinates. If pd is a variable of type Point2D.Double, you can also refer directly to the coordinates as pd.x and pd.y (and similarly for Point2D.Float). Other classes in java.awt.geom offer a similar variety of ways to manipulate their properties, and I won't try to list them all here.

There is a variety of classes that represent geometric shapes, including Line2D, Rectangle2D, RoundRectangle2D, Ellipse2D, Arc2D, and Path2D. All of these are abstract classes, and each of them contains a pair of subclasses such as Rectangle2D.Double and Rectangle2D.Float. Some shapes, such as rectangles, have interiors that can be filled; such shapes also have outlines that can be stroked. Some shapes, such as lines, are purely one-dimensional and can only be stroked.

Aside from lines, rectangles are probably the simplest shapes. A Rectangle2D has a corner point (x,y), a width, and a height, and can be constructed from that data ("new Rectangle2D.Double(x,y,w,h)"). The corner point (x,y) specifies the minimum x- and y-values in the rectangle. For the usual pixel coordinate system, (x,y) is the upper left corner. However, in a coordinate system in which the minimum value of y is at the bottom, (x,y) would be the lower left corner. The sides of the rectangle are parallel to the coordinate axes. A variable r of type Rectangle2D.Double has public instance variables r.x, r.y, r.width, and r.height. If the width or the height is less than or equal to zero, nothing will be drawn when the rectangle is filled or stroked. A common task is to define a rectangle from two corner points (x1,y1) and (x2,y2). This can be accomplished by creating a rectangle with height and width equal to zero and then adding the second point to the rectangle. Adding a point to a rectangle causes the rectangle to grow just enough to include that point:

Rectangle2D.Double r = new Rectangle2D.Double(x1,y1,0,0);
r.add(x2,y2);

The classes Line2D, Ellipse2D, RoundRectangle2D and Arc2D create other basic shapes and work similarly to Rectangle2D. You can check the Java API documentation for details.

The Path2D class is more interesting. It represents general paths made up of segments that can be lines and Bezier curves. Paths are created using methods similar to the moveTo and lineTo subroutines that were discussed in Subsection 2.2.3. To create a path, you start by constructing an object of type Path2D.Double (or Path2D.Float):

Path2D.Double p = new Path2D.Double();

The path p is empty when it is first created. You construct the path by moving an imaginary "pen" along the path that you want to create. The method p.moveTo(x,y) moves the pen to the point (x,y) without drawing anything. It is used to specify the initial point of the path or the starting point of a new piece of the path. The method p.lineTo(x,y) draws a line from the current pen position to (x,y), leaving the pen at (x,y). The method p.close() can be used to close the path (or the current piece of the path) by drawing a line back to its starting point. For example, the following code creates a triangle with vertices at (0,5), (2,-3), and (-4,1):

Path2D.Double p = new Path2D.Double();
p.moveTo(0,5);
p.lineTo(2,-3);
p.lineTo(-4,1);
p.close();

You can also add Bezier curve segments to a Path2D. Bezier curves were discussed in Subsection 2.2.3. You can add a cubic Bezier curve to a Path2D p with the method

p.curveTo( cx1, cy1, cx2, cy2, x, y );

This adds a curve segment that starts at the current pen position and ends at (x,y), using (cx1,cy1) and (cx2,cy2) as the two control points for the curve. The method for adding a quadratic Bezier curve segment to a path is quadTo. It requires only a single control point:

p.quadTo( cx, cy, x, y );

When a path intersects itself, its interior is determined by looking at the winding number, as discussed in Subsection 2.2.2. There are two possible rules for determining whether a point is interior: asking whether the winding number of the curve about that point is non-zero, or asking whether it is odd. You can set the winding rule used by a Path2D p with

p.setWindingRule( Path2D.WIND_NON_ZERO );
p.setWindingRule( Path2D.WIND_EVEN_ODD );

The default is WIND_NON_ZERO.

Finally, I will note that it is possible to draw a copy of an image into a graphics context. The image could be loaded from a file or created by the program. I discuss the second possibility later in this section. An image is represented by an object of type Image. In fact, I will assume here that the object is of type BufferedImage, which is a subclass of Image. If img is such an object, then

g2.drawImage( img, x, y, null );

will draw the image with its upper left corner at the point (x,y). (The fourth parameter is hard to explain, but it should be specified as null for BufferedImages.) This draws the image at its natural width and height, but a different width and height can be specified in the method:

g2.drawImage( img, x, y, width, height, null );

There is also a method for drawing a string of text. The method specifies the string and the basepoint of the string. (The basepoint is the lower left corner of the string, ignoring "descenders" like the tail on the letter "g".) For example,

g2.drawString( "Hello World", 100, 50 );

Images and strings are subject to transforms in the same way as other shapes. Transforms are the only way to get rotated text and images. As an example, here is what can happen when you apply a rotation to some text and an image:

pixel-coordinates

2.5.3 描边和填充

Stroke and Fill

一旦您有一个表示形状的对象,您就可以填充该形状或描边它。Graphics2D类定义了执行此操作的方法。描边形状的方法称为draw:

g2.fill(shape);
g2.draw(shape);

这里,g2是Graphics2D类型,shape可以是Path2DLine2DRectangle2D或任何其他形状类的对象。这些通常用于新创建的对象上,当该对象表示的形状只会被绘制一次时。例如:

g2.draw( new Line2D.Double( -5, -5, 5, 5 ) );

当然,也可以创建形状对象并多次重用它们。

用于描边形状的“笔”通常由BasicStroke类型的对象表示。默认的笔的线宽等于1。这是当前坐标系中的一个单位,而不是一个像素。要获得不同宽度的线条,可以安装一个新的笔:

g2.setStroke( new BasicStroke(width) );

构造函数中的width的类型是float。可以向构造函数添加参数来控制笔在其端点的形状以及两个线段相遇的位置。(见子节2.2.1。)例如:

g2.setStroke( new BasicStroke( 5.0F,
        BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL) );

还可以用虚线和点线制作笔,但我不会在这里讨论如何做。


对形状进行描边或填充意味着设置某些像素的颜色。在Java中,用于对这些像素着色的规则称为“画笔”。画笔可以是纯色、渐变或图案。与Java中的大多数东西一样,画笔由对象表示。如果paint是这样的一个对象,那么

g2.setPaint(paint);

将设置paint用于图形上下文g2的后续绘图操作,直到下次更改画笔为止。(还有一种更旧的方法,g2.setColor(c),它仅适用于颜色,并等价于调用g2.setPaint(c)。)

纯色由Color类型的对象表示。颜色在内部表示为RGBA颜色。可以使用构造函数创建一个不透明颜色,其alpha分量最大:

new Color( r, g, b );

其中r、g和b是介于0到255之间的整数,表示颜色的红、绿和蓝分量。要获得半透明颜色,可以添加alpha分量,也在0到255范围内:

new Color( r, b, g, a );

还有一个函数,Color.getHSBColor(h,s,b),它从HSB(又名HSV)颜色模型的值创建颜色。在这种情况下,色相、饱和度和亮度颜色分量必须作为float类型的值给出。还有常量来表示大约十几种常见的颜色,例如Color.WHITE、Color.RED和Color.YELLOW。例如,这是我可能如何绘制一个带有黑色轮廓和浅蓝色内部的正方形的方法:

Rectangle2D square = new Rectangle2D.Double(-2,-2,4,4);
g2.setPaint( new Color(200,200,255) );
g2.fill( square );
g2.setStroke( new BasicStroke(0.1F) );
g2.setPaint( Color.BLACK );
g2.draw( square );

除了纯色外,Java还有GradientPaint类,用于表示简单的线性渐变,以及TexturePaint类,用于表示图案填充。在三维图形中使用的图像模式称为纹理。渐变和图案在子节2.2.2中已经讨论过了。对于这些画笔,应用于像素的颜色取决于像素的坐标。

要创建一个TexturePaint,您需要一个BufferedImage对象来指定它将用作图案的图像。您还必须说明图像中的坐标如何映射到显示中的绘图坐标。您可以通过指定一个矩形来实现这一点,该矩形将容纳图像的一个副本。因此,构造函数采用以下形式:

new TexturePaint( image, rect );

其中image是BufferedImage,rect是Rectangle2D。在指定的矩形外部,图像在水平和垂直方向上重复。GradientPaint的构造函数采用以下形式:

new GradientPaint( x1, y1, color1, x2, y2, color2, cyclic )

这里,x1、y1、x2和y2是float类型的值;color1和color2是Color类型;cyclic是布尔值。渐变颜色将沿着从点(x1,y1)到点(x2,y2)的线段变化。在第一个端点处,颜色是color1,在第二个端点处是color2。颜色沿着与该线段垂直的线段是恒定的。布尔参数cyclic指定颜色模式是否重复。例如,以下命令将在图形上下文中安装一个GradientPaint:

g2.setPaint( new GradientPaint( 0,0, Color.BLACK, 200,100, Color.RED, true ) );

顺便说一句,当前画笔用于描边和填充。

示例Java程序java2d/PaintDemo.java显示了一个填充有GradientPaintTexturePaint的多边形,并允许您调整其属性。图像文件java2d/QueenOfHearts.pngjava2d/TinySmiley.png是该程序的一部分,在运行该程序时,它们必须与构成该程序的编译后的类文件位于同一位置。

Once you have an object that represents a shape, you can fill the shape or stroke it. The Graphics2D class defines methods for doing this. The method for stroking a shape is called draw:

g2.fill(shape);
g2.draw(shape);

Here, g2 is of type Graphics2D, and shape can be of type Path2D, Line2D, Rectangle2D or any of the other shape classes. These are often used on a newly created object, when that object represents a shape that will only be drawn once. For example

g2.draw( new Line2D.Double( -5, -5, 5, 5 ) );

Of course, it is also possible to create shape objects and reuse them many times.

The "pen" that is used for stroking a shape is usually represented by an object of type BasicStroke. The default stroke has line width equal to 1. That's one unit in the current coordinate system, not one pixel. To get a line with a different width, you can install a new stroke with

g2.setStroke( new BasicStroke(width) );

The width in the constructor is of type float. It is possible to add parameters to the constructor to control the shape of a stroke at its endpoints and where two segments meet. (See Subsection 2.2.1.) For example,

g2.setStroke( new BasicStroke( 5.0F,
        BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL) );

It is also possible to make strokes out of dashes and dots, but I won't discuss how to do it here.


Stroking or filling a shape means setting the colors of certain pixels. In Java, the rule that is used for coloring those pixels is called a "paint." Paints can be solid colors, gradients, or patterns. Like most things in Java, paints are represented by objects. If paint is such an object, then

g2.setPaint(paint);

will set paint to be used in the graphics context g2 for subsequent drawing operations, until the next time the paint is changed. (There is also an older method, g2.setColor(c), that works only for colors and is equivalent to calling g2.setPaint(c).)

Solid colors are represented by objects of type Color. A color is represented internally as an RGBA color. An opaque color, with maximal alpha component, can be created using the constructor

new Color( r, g, b );

where r, g, and b are integers in the range 0 to 255 that give the red, green, and blue components of the color. To get a translucent color, you can add an alpha component, also in the range 0 to 255:

new Color( r, b, g, a );

There is also a function, Color.getHSBColor(h,s,b), that creates a color from values in the HSB color model (which is another name for HSV). In this case, the hue, saturation, and brightness color components must be given as values of type float. And there are constants to represent about a dozen common colors, such as Color.WHITE, Color.RED, and Color.YELLOW. For example, here is how I might draw a square with a black outline and a light blue interior:

Rectangle2D square = new Rectangle2D.Double(-2,-2,4,4);
g2.setPaint( new Color(200,200,255) );
g2.fill( square );
g2.setStroke( new BasicStroke(0.1F) );
g2.setPaint( Color.BLACK );
g2.draw( square );

Beyond solid colors, Java has the class GradientPaint, to represent simple linear gradients, and TexturePaint to represent pattern fills. (Image patterns used in a similar way in 3D graphics are called textures.) Gradients and patterns were discussed in Subsection 2.2.2. For these paints, the color that is applied to a pixel depends on the coordinates of the pixel.

To create a TexturePaint, you need a BufferedImage object to specify the image that it will use as a pattern. You also have to say how coordinates in the image will map to drawing coordinates in the display. You do this by specifying a rectangle that will hold one copy of the image. So the constructor takes the form:

new TexturePaint( image, rect );

where image is the BufferedImage and rect is a Rectangle2D. Outside that specified rectangle, the image is repeated horizontally and vertically. The constructor for a GradientPaint takes the form

new GradientPaint( x1, y1, color1, x2, y2, color2, cyclic )

Here, x1, y1, x2, and y2 are values of type float; color1 and color2 are of type Color; and cyclic is boolean. The gradient color will vary along the line segment from the point (x1,y1) to the point (x2,y2). The color is color1 at the first endpoint and is color2 at the second endpoint. Color is constant along lines perpendicular to that line segment. The boolean parameter cyclic says whether or not the color pattern repeats. As an example, here is a command that will install a GradientPaint into a graphics context:

g2.setPaint( new GradientPaint( 0,0, Color.BLACK, 200,100, Color.RED, true ) );

You should, by the way, note that the current paint is used for strokes as well as for fills.

The sample Java program java2d/PaintDemo.java displays a polygon filled with a GradientPaint or a TexturePaint and lets you adjust their properties. The image files java2d/QueenOfHearts.png and java2d/TinySmiley.png are part of that program, and they must be in the same location as the compiled class files that make up that program when it is run.

2.5.4 变换

Transforms

Java将几何变换实现为Graphics2D类中的方法。例如,如果g2是一个Graphics2D,那么调用g2.translate(1,3)将对在调用该方法之后绘制的对象应用一个(1,3)的平移变换。可用的方法对应于Section 2.3中讨论的变换函数:

  • g2.scale(sx,sy) — 按水平缩放因子sx和垂直缩放因子sy缩放。
  • g2.rotate(r) — 绕原点旋转r弧度角度,其中角度以弧度表示。正角度将正x轴旋转到正y轴的方向。
  • g2.rotate(r,x,y) — 绕点(x,y)旋转r角度。
  • g2.translate(dx,dy) — 水平平移dx和垂直平移dy。
  • g2.shear(sx,sy) — 应用水平剪切量sx和垂直剪切量sy。(通常,剪切量之一为0,产生纯水平或纯垂直的剪切。)

在Java中,变换表示为AffineTransform类的对象。您可以使用构造函数创建一个一般的仿射变换

AffineTransform trns = new AffineTransform(a,b,c,d,e,f);

变换trns将点(x,y)变换为点(x1,y1),公式如下

x1 = a*x + c*y + e
y1 = b*x + d*y + f;

您可以通过调用g2.transform(trns)将变换trns应用于图形上下文g2。

图形上下文g2包括当前的仿射变换,该变换是应用的所有变换的组合。诸如g2.rotate和g2.transform之类的命令修改当前变换。您可以通过调用g2.getTransform()获取当前变换的副本,该方法返回一个AffineTransform对象。您可以使用g2.setTransform(trns)设置当前变换。这将在g2中用AffineTransform trns替换当前变换。(注意,g2.setTransform(trns)与g2.transform(trns)不同;第一个命令替换g2中的当前变换,而第二个命令修改当前变换,将其与trns组合。)

getTransform和setTransform方法可用于实现分层建模。如Section 2.4所讨论的那样,绘制对象之前,您应保存当前变换。绘制对象后,恢复保存的变换。在绘制对象及其子对象时应用的任何额外的建模变换将不会影响对象之外的内容。在Java中,这看起来像是:

AffineTransform savedTransform = g2.getTransform();
drawObject();
g2.setTransform( savedTransform );

对于分层图形,我们实际上需要一个变换堆栈。但是,如果使用子程序实现层次结构,则上述代码将是子程序的一部分,并且局部变量savedTransform的值将存储在子程序调用堆栈上。实际上,我们将使用子程序调用堆栈来实现保存变换的堆栈。

除了建模变换之外,变换还用于设置窗口到视口变换,建立用于绘图的坐标系统。这通常在创建图形上下文之后立即进行,而不是在任何绘图操作之前。它可以使用Subsection 2.3.7中的Java版本的applyWindowToViewportTransformation函数进行。请参见示例程序java2d/GraphicsStarter.java


我还要提一下AffineTransform对象的另一个用途:有时,您确实需要显式地转换坐标。例如,给定对象坐标(x,y),我可能需要知道它们在屏幕上实际会到达哪里,即像素坐标。换句话说,我想通过当前变换来转换(x,y)以获取相应的像素坐标。AffineTransform类有一个方法用于将仿射变换应用于点。它使用Point2D类型的对象。以下是一个示例:

AffineTransform trns = g2.getTransform();
Point2D.Double originalPoint = new Point2D.Double(x,y);
Point2D.Double transformedPoint = new Point2D.Double();
trns.transform( originalPoint, transformedPoint );
// transformedPoint 现在包含与 (x,y) 对应的像素坐标
int pixelX = (int)transformedPoint.x;
int pixelY = (int)transformedPoint.y;

我使用这种方法的一种方式是在处理字符串时。通常,在使用变换坐标系显示字符串时,我希望转换字符串的基点,但不转换字符串本身。也就是说,我希望变换影响字符串的位置但不影响其大小或旋转。为了实现这一点,我使用上述技术获取转换后基点的像素坐标,然后在这些坐标处绘制字符串,使用原始的、未经转换的图形上下文。

反向操作有时也是必要的。也就是说,给定像素坐标(px,py),找到通过给定仿射变换转换为(px,py)的点(x,y)。例如,当实现鼠标交互时,通常会知道鼠标的像素坐标,但您希望找到您自己选择的坐标系中相应的点。为此,您需要一个逆变换。仿射变换T的逆变换是执行相反变换的另一个变换。也就是说,如果T(x,y) = (px,py),并且如果R是逆变换,则R(px,py) = (x,y)。在Java中,可以使用以下方法获得AffineTransform trns的逆变换:

AffineTransform inverse = trns.createInverse();

(最后注意:来自Graphics的旧绘图方法,如drawLine,使用整数坐标。重要的是要注意,使用这些旧方法绘制的任何形状都受到与指定实数坐标的Line2D等形状相同的变换的影响。例如,使用g.drawLine(1,2,5,7)绘制线将具有与绘制具有端点(1.0,2.0)和(5.0,7.0)的Line2D相同的效果。事实上,所有绘图都受到坐标变换的影响。)

Java implements geometric transformations as methods in the Graphics2D class. For example, if g2 is a Graphics2D, then calling g2.translate(1,3) will apply a translation by (1,3) to objects that are drawn after the method is called. The methods that are available correspond to the transform functions discussed in Section 2.3:

  • g2.scale(sx,sy) — scales by a horizontal scale factor sx and a vertical scale factor sy.
  • g2.rotate(r) — rotates by the angle r about the origin, where the angle is measured in radians. A positive angle rotates the positive x-axis in the direction of the positive y-axis.
  • g2.rotate(r,x,y) — rotates by the angle r about the point (x,y).
  • g2.translate(dx,dy) — translates by dx horizontally and dy vertically.
  • g2.shear(sx,sy) — applies a horizontal shear amount sx and a vertical shear amount sy. (Usually, one of the shear amounts is 0, giving a pure horizontal or vertical shear.)

A transform in Java is represented as an object of the class AffineTransform. You can create a general affine transform with the constructor

AffineTransform trns = new AffineTransform(a,b,c,d,e,f);

The transform trns will transform a point (x,y) to the point (x1,y1) given by

x1 = a*x + c*y + e
y1 = b*x + d*y + f;

You can apply the transform trns to a graphics context g2 by calling g2.transform(trns).

The graphics context g2 includes the current affine transform, which is the composition of all the transforms that have been applied. Commands such as g2.rotate and g2.transform modify the current transform. You can get a copy of the current transform by calling g2.getTransform(), which returns an AffineTransform object. You can set the current transform using g2.setTransform(trns). This replaces the current transform in g2 with the AffineTransform trns. (Note that g2.setTransform(trns) is different from g2.transform(trns); the first command replaces the current transform in g2, while the second modifies the current transform by composing it with trns.)

The getTransform and setTransform methods can be used to implement hierarchical modeling. The idea, as discussed in Section 2.4, is that before drawing an object, you should save the current transform. After drawing the object, restore the saved transform. Any additional modeling transformations that are applied while drawing the object and its sub-objects will have no effect outside the object. In Java, this looks like

AffineTransform savedTransform = g2.getTransform();
drawObject();
g2.setTransform( savedTransform );

For hierarchical graphics, we really need a stack of transforms. However, if the hierarchy is implemented using subroutines, then the above code would be part of a subroutine, and the value of the local variable savedTransform would be stored on the subroutine call stack. Effectively, we would be using the subroutine call stack to implement the stack of saved transforms.

In addition to modeling transformations, transforms are used to set up the window-to-viewport transformation that establishes the coordinate system that will be used for drawing. This is usually done in Java just after the graphics context has been created, before any drawing operations. It can be done with a Java version of the applyWindowToViewportTransformation function from Subsection 2.3.7. See the sample program java2d/GraphicsStarter.java for an example.


I will mention one more use for AffineTransform objects: Sometimes, you do need to explicitly transform coordinates. For example, given object coordinates (x,y), I might need to know where they will actually end up on the screen, in pixel coordinates. That is, I would like to transform (x,y) by the current transform to get the corresponding pixel coordinates. The AffineTransform class has a method for applying the affine transform to a point. It works with objects of type Point2D. Here is an example:

AffineTransform trns = g2.getTransform();
Point2D.Double originalPoint = new Point2D.Double(x,y);
Point2D.Double transformedPoint = new Point2D.Double();
trns.transform( originalPoint, transformedPoint );
// transformedPoint now contains the pixel coords corresponding to (x,y)
int pixelX = (int)transformedPoint.x;
int pixelY = (int)transformedPoint.y;

One way I have used this is when working with strings. Often when displaying a string in a transformed coordinate system, I want to transform the basepoint of a string, but not the string itself. That is, I want the transformation to affect the location of the string but not its size or rotation. To accomplish this, I use the above technique to obtain the pixel coordinates for the transformed basepoint, and then draw the string at those coordinates, using an original, untransformed graphics context.

The reverse operation is also sometimes necessary. That is, given pixel coordinates (px,py), find the point (x,y) that is transformed to (px,py) by a given affine transform. For example, when implementing mouse interaction, you will generally know the pixel coordinates of the mouse, but you will want to find the corresponding point in your own chosen coordinate system. For that, you need an inverse transform. The inverse of an affine transform T is another transform that performs the opposite transformation. That is, if T(x,y) = (px,py), and if R is the inverse transform, then R(px,py) = (x,y). In Java, the inverse transform of an AffineTransform trns can be obtained with

AffineTransform inverse = trns.createInverse(); (A final note: The older drawing methods from Graphics, such as drawLine, use integer coordinates. It's important to note that any shapes drawn using these older methods are subject to the same transformation as shapes such as Line2D that are specified with real number coordinates. For example, drawing a line with g.drawLine(1,2,5,7) will have the same effect as drawing a Line2D that has endpoints (1.0,2.0) and (5.0,7.0). In fact, all drawing is affected by the transformation of coordinates.)

AffineTransform inverse = trns.createInverse();

(A final note: The older drawing methods from Graphics, such as drawLine, use integer coordinates. It's important to note that any shapes drawn using these older methods are subject to the same transformation as shapes such as Line2D that are specified with real number coordinates. For example, drawing a line with g.drawLine(1,2,5,7) will have the same effect as drawing a Line2D that has endpoints (1.0,2.0) and (5.0,7.0). In fact, all drawing is affected by the transformation of coordinates.)

2.5.5 BufferedImage 和 Pixels

BufferedImage and Pixels

在一些图形应用程序中,能够使用不可见于屏幕的图像是很有用的。换句话说,您需要我所称的离屏画布。您还需要一种快速将离屏画布复制到屏幕上的方法。例如,将屏幕上的图像副本存储在离屏画布中可能很有用。画布是图像的官方副本。对图像的更改是在画布上进行的,然后复制到屏幕上。这样做的一个原因是,您可以在屏幕图像上绘制额外的内容而不改变官方副本。例如,您可能会在屏幕图像中选择一个区域并绘制一个框。您可以在不损害离屏画布中的官方副本的情况下完成此操作。要从屏幕中删除框,您只需将离屏画布图像复制到屏幕上。

在Java中,可以将离屏图像实现为BufferedImage类型的对象。BufferedImage表示内存中的一个区域,您可以在其中绘制,方式与您可以绘制到屏幕上的方式完全相同。也就是说,您可以获取一个名为g2的Graphics2D类型的图形上下文,用于在图像上绘制。BufferedImage是一个Image,您可以将其绘制到屏幕上或任何其他图形上下文中,就像处理任何其他Image一样,即使用要显示图像的图形上下文的drawImage方法。在典型的设置中,有如下变量:

BufferedImage OSC;  // 离屏画布
Graphics2D OSG;     // 用于在画布上绘制的图形上下文

可以使用以下方式创建对象:

OSC = new BufferedImage( 640, 480, BufferedImage.TYPE_INT_RGB );
OSG = OSC.createGraphics();

BufferedImage的构造函数指定了图像的宽度和高度以及其类型。类型告诉了图像中可以表示什么颜色以及它们如何存储。在这里,类型为TYPE_INT_RGB,这意味着图像使用带有每个颜色分量的8位的常规RGB颜色。每个像素的三个颜色分量被打包到一个整数值中。

在使用BufferedImage存储屏幕上图像的程序中,paintComponent方法通常具有以下形式:

protected void paintComponent(Graphics g) {
    g.drawImage( OSC, 0, 0, null );
    Graphics2D g2 = (Graphics2D)g.create();
    .
    . // 在图像上绘制额外的内容。
    .
}

使用这种技术的示例程序是java2d/JavaPixelManipulation.java。在该程序中,用户可以通过拖动鼠标来绘制线条、矩形和椭圆。当鼠标移动时,形状在鼠标的起始点和当前位置之间绘制。随着鼠标的移动,现有图像的部分可以被重复覆盖和暴露,而不更改现有图像。事实上,图像在一个离屏画布中,用户绘制的形状实际上是由paintComponent在画布的内容上绘制的。直到用户释放鼠标并结束拖动操作,形状才会被绘制到画布中的官方图像上。

但我编写该程序的主要原因是为了说明像素操作,即使用单个像素的颜色分量进行计算。BufferedImage类有用于读取和设置单个像素颜色的方法。图像由像素的行和列组成。如果OSC是BufferedImage,则

int color = OSC.getRGB(x,y)

获取表示x列y行像素颜色的整数。每个颜色分量存储在整数颜色值中的一个8位字段中。可以使用Java的位操作符从整数颜色值中提取出用于处理的单个颜色分量:

int red = (color >> 16) & 255;
int green = (color >> 8) & 255;
int blue = color & 255;

类似地,给定范围为0到255的红色、绿色和蓝色分量值,我们可以将这些分量值组合成一个整数,并使用它来设置图像中像素的颜色:

int color = (red << 16) | (green << 8) | blue;
OSC.setRGB(x,y,color);

还有用于读取和设置矩形区域中所有像素颜色的方法。

像素操作用于实现示例程序的两个功能。首先,有一个“涂抹”工具。当用户使用此工具拖动时,就像涂抹湿漆一样。当用户首次单击鼠标时,从鼠标位置周围的一小块像素中复制颜色分量到数组中。随着用户移动鼠标,颜色从数组中混合到鼠标附近的像素颜色中,同时将这些颜色混合到数组中的颜色中。这是一个已经“涂抹”的小矩形:

![

pixel-coordinates](../../en/c2/smudge.png)

像素操作的第二个用途是实现“滤镜”。在这个程序中,滤镜是一种通过将每个像素的颜色替换为一个3x3像素方块的颜色的加权平均值来修改图像的操作。例如,“模糊”滤镜使用所有像素的平均权重,因此像素的颜色会更改为该像素及其邻居的颜色的简单平均值。使用不同的权重对每个像素进行操作可以产生一些引人注目的效果。

示例程序中的像素操作产生了纯矢量图形无法实现的效果。我鼓励您通过查看源代码来了解更多信息。您还可以查看下一节中使用HTML画布图形实现相同效果的实时演示。

In some graphics applications, it is useful to be able to work with images that are not visible on the screen. That is, you need what I call an off-screen canvas. You also need a way to quickly copy the off-screen canvas onto the screen. For example, it can be useful to store a copy of the on-screen image in an off-screen canvas. The canvas is the official copy of the image. Changes to the image are made to the canvas, then copied to the screen. One reason to do this is that you can then draw extra stuff on top of the screen image without changing the official copy. For example, you might draw a box around a selected region in the on-screen image. You can do this without damaging the official copy in the off-screen canvas. To remove the box from the screen, you just have to copy the off-screen canvas image onto the screen.

In Java, an off-screen image can be implemented as an object of type BufferedImage. A BufferedImage represents a region in memory where you can draw, in exactly the same way that you can draw to the screen. That is, you can obtain a graphics context g2 of type Graphics2D that you can use for drawing on the image. A BufferedImage is an Image, and you can draw it onto the screen—or into any other graphics context—like any other Image, that is, by using the drawImage method of the graphics context where you want to display the image. In a typical setup, there are variables

BufferedImage OSC;  // The off-screen canvas.
Graphics2D OSG;     // graphics context for drawing to the canvas

The objects are created using, for example,

OSC = new BufferedImage( 640, 480, BufferedImage.TYPE_INT_RGB );
OSG = OSC.createGraphics();

The constructor for BufferedImage specifies the width and height of the image along with its type. The type tells what colors can be represented in the image and how they are stored. Here, the type is TYPE_INT_RGB, which means the image uses regular RGB colors with 8 bits for each color component. The three color components for a pixel are packed into a single integer value.

In a program that uses a BufferedImage to store a copy of the on-screen image, the paintComponent method generally has the form

protected void paintComponent(Graphics g) {
    g.drawImage( OSC, 0, 0, null );
    Graphics2D g2 = (Graphics2D)g.create();
    .
    . // Draw extra stuff on top of the image.
    .
}

A sample program that uses this technique is java2d/JavaPixelManipulation.java. In that program, the user can draw lines, rectangles, and ovals by dragging the mouse. As the mouse moves, the shape is drawn between the starting point of the mouse and its current location. As the mouse moves, parts of the existing image can be repeatedly covered and uncovered, without changing the existing image. In fact, the image is in an off-screen canvas, and the shape that the user is drawing is actually drawn by paintComponent over the contents of the canvas. The shape is not drawn to the official image in the canvas until the user releases the mouse and ends the drag operation.

But my main reason for writing the program was to illustrate pixel manipulation, that is, computing with the color components of individual pixels. The BufferedImage class has methods for reading and setting the color of individual pixels. An image consists of rows and columns of pixels. If OSC is a BufferedImage, then

int color = OSC.getRGB(x,y)

gets the integer that represents the color of the pixel in column number x and row number y. Each color component is stored in an 8-bit field in the integer color value. The individual color components can be extracted for processing using Java's bit manipulation operators:

int red = (color >> 16) & 255;
int green = (color >> 8) & 255;
int blue = color & 255;

Similarly, given red, green, and blue color component values in the range 0 to 255, we can combine those component values into a single integer and use it to set the color of a pixel in the image:

int color = (red << 16) | (green << 8) | blue;
OSC.setRGB(x,y,color);

There are also methods for reading and setting the colors of an entire rectangular region of pixels.

Pixel operations are used to implement two features of the sample program. First, there is a "Smudge" tool. When the user drags with this tool, it's like smearing wet paint. When the user first clicks the mouse, the color components from a small square of pixels surrounding the mouse position are copied into arrays. As the user moves the mouse, color from the arrays is blended into the color of the pixels near the mouse position, while those colors are blended into the colors in the arrays. Here is a small rectangle that has been "smudged":

pixel-coordinates

The second use of pixel manipulation is in implementing "filters." A filter, in this program, is an operation that modifies an image by replacing the color of each pixel with a weighted average of the colors of a 3-by-3 square of pixels. A "Blur" filter for example, uses equal weights for all pixels in the average, so the color of a pixel is changed to the simple average of the colors of that pixel and its neighbors. Using different weights for each pixel can produce some striking effects.

The pixel manipulation in the sample program produces effects that can't be achieved with pure vector graphics. I encourage you to learn more by looking at the source code. You might also take a look at the live demos in the next section, which implement the same effects using HTML canvas graphics.