Beginning graphics: an introduction to Java2D


A simple illustration
Java2D
Graphics Primitives
Working with text
Playing back a path
Graphics2D's internal state
Affine transforms
Graphing a function
Stroking lines
Rendering hints

Based on notes Beginning graphics and AffineTransforms and stroking

A simple illustration

DrawRectangle.java

How it works

  • Two objects can be seen: a JFrame that holds the picture and a JPanel on which the picture is drawn.

  • Think of the JFrame as a frame that holds a picture and the JPanel is the canvas on which we paint and place in the frame.

  • When the JFrame is made visible, the paintComponent of any component contained within it is invoked. DrawRectangle's method paintComponent overrides that of its superclass JPanel to give the drawing instructions.

  • The Graphics object passed to the paintComponent method is a rendering engine, an object that, in this case, translates drawing instructions into the actual picture we see.

  • Always include super.paintComponent(Graphics g) as the first line when overriding the paintComponent method of a component. Without that instruction, some very weird behavior can result.

  • This example uses Java's Swing components.

Java's coordinate system

The units are given in pixels.

This is a left-handed coordinate system.


Java2D

Distance.java

Java2D features

  • Java2D provides a more powerful rendering engine, Graphics2D, a subclass of Graphics.

  • Several new classes are provided in java.awt.geom: points, lines, ellipses and others.

  • Coordinates may be specified as either floats or doubles.

  • Line2D and Ellipse2D are subclasses (actually, implementations) of Shape. A Shape can be either filled or drawn.

  • A Point2D is not a Shape but instead gives a convenient way to pass coordinates around to various methods.

  • Colors can be chosen from among the class fields of Color or by specifying the red, green and blue values as float between 0 and 1.

  • Generally speaking, Java paint is opaque so the order in which elements are drawn is important when they overlap.


Some more graphics primitives

Quadratic curves

A quadratic curve has the form

where

Subdivision

Quadratic curves may be drawn elegantly by a subdivision process

The curve is broken into two halves, each of which is itself a quadratic curve whose control points are obtained by averaging the original control points.

Instantiation

Quadratic curves may be instantiated using either

    QuadCurve2D.Double quad = new QuadCurve2D.Double(double x0, double y0,
        double x1, double y1, double x2, double y2);

or

    QuadCurve2D.Double quad = new QuadCurve2D.Double();
    quad.setCurve(Point2D p0, Point2D p1, Point2D p2);

Cubic curves

A cubic curve has the form

where

Instantiation

Cubic curves may be instantiated using either

    CubicCurve2D.Double quad = new CubicCurve2D.Double(double x0, double y0,
        double x1, double y1, double x2, double y2, double x3, double y3);

or

    CubicCurve2D.Double quad = new CubicCurve2D.Double();
    quad.setCurve(Point2D p0, Point2D p1, Point2D p2, Point2D p3);

Rectangular shapes

Rectangular shapes, including rectangles and ellipses, are generally defined by stating the upper left corner of the smallest rectangle containing the shape along with its width and height.

    Rectangle2D.Double rectangle = new Rectangle2D.Double(double ulx, double uly,
        double width, double height);

    Ellipse2D.Double ellipse = new Ellipse2D.Double(double ulx, double uly,
        double width, double height);

Arcs

    Arc2D.Double arc = new Arc2D.Double(double ulx, double uly, double width,
        double height, double angleStart, double angleEnd, int closure);

  • The angles are measured in degrees.

  • The angles are given with respect to a parametrization of the ellipse:

  • Remember that Java gives us a left-handed coordinate system.

  • The constant closure has one of the values Arc2D.OPEN, Arc2D.PIE or Arc2D.CHORD

General paths

The class GeneralPath lets us build paths like we might in PostScript.

GeneralPath path = new GeneralPath();
path.moveTo(50, 50);
path.lineTo(150, 150);
path.quadTo(200, 200, 250, 150);
path.curveTo(250, 250, 150, 250, 150, 200);
path.closePath();
g.draw(path);


Working with text

Font font = new Font("sanserif", Font.BOLD, 80);
g.setFont(font);
g.drawString("graphics", 50, 100);

Measuring text

A FontMetrics object allows us to measure the size of a String.

    FontMetrics fm = g.getFontMetrics();
    Rectangle2D rect = fm.getStringBounds("graphics", g);

This produces the smallest rectangle containing the String were it to be drawn at the point (0, 0).


Playing back a path

Any Shape object can produce a PathIterator, a description of the outline of the Shape in terms of line segments and quadratic and cubic curves.

Here is how Java2D draws a circle:

PathIteratedCircle.java

Text is handled similarly

PathIteratedText.java

The character in red is the image of the character in blue under the complex function f(z)=1/z.

CharacterImage.java


Graphic2D's internal state

A Graphics2D object uses some adjustable parameters when rendering a Shape. These include

  • Paint: at this stage, think of this as the current color.

  • Font

  • Transform: converts the coordinates used in defining Shapes into pixels

  • Stroke: controls the thickness and other attributes of curves drawn

  • Clipping shape: restricts the visible region in which Shape's are rendered

  • Compositing rule: controls how colors are blended when placed on top of one another

  • Rendering hints: allows us to fine tune some parameters to favor either performance or quality

Affine transforms

Affine transforms of the plane have the form

A Graphics2D maintains an internal affine transform to convert the coordinates of a Shape into pixels on the computer screen using an AffineTransform object.

A Shape is defined in a coordinate system called user space. Pixels live in a coordinate system called device space.

The AffineTransform is a map from user space to device space.

Creating AffineTransforms

AffineTransforms are ubiquitous in Java2D and there are consequently many ways to instantiate them.

    AffineTransform transform = new AffineTransform();

gives an object representing the identity transform. Instructions such as

    transform.translate(100, 200);
    transform.scale(2, -1);
    transform.shear(1, 0);

modify transform by composing it, on the right, with the indicated transform.

Using AffineTransforms

There are two ways to use AffineTransforms. First, one may modify a Graphics2D's internal transform. Here is an example along with the result:

AffineTransform at = new AffineTransform();
at.translate(150, 200);
at.rotate(-Math.PI/3);
at.scale(2, 0.5);
g.transform(at);
Ellipse2D.Float circle = 
    new Ellipse2D.Float(-50, -50, 100, 100);
g.draw(circle);

Transforming shapes

Alternatively, we may transform shapes.

AffineTransform at = new AffineTransform();
at.translate(150, 200);
at.rotate(-Math.PI/3);
at.scale(2, 0.5);
Ellipse2D.Float circle = 
    new Ellipse2D.Float(-50, -50, 100, 100);
Shape shape = at.createTransformedShape(circle);
g.draw(shape);

Non-uniform scaling

Reasons to transform Shapes

I generally transform Shapes rather than alter the Graphics2D's underlying AffineTransform.

This is to avoid the problem we've just seen with non-uniform scaling.

Also, the thickness of lines and the size of points drawn are most conveniently expressed in terms of pixels.

Finally, when we work with events, some important information will be given to us in pixels.

Inverses

The inverse of an AffineTransform may be found like this:

    AffineTransform inverse;
    try {
        inverse = transform.createInverse();
    } catch(NoninvertibleTransformException ex) {
        do something here if transform is not invertible
    }

Graphing a function

Let's look at an example. In a 301 by 301 pixel frame, we will draw the graph of the function y = x3 - x in the viewing rectangle along with a 1 by 1 grid.

Setting up the transform

It would be most convenient if user space agreed with our mathematical coordinate system. We will therefore set up an AffineTransform that converts mathematical coordinates into the coordinates of the JPanel.

    AffineTransform transform = new AffineTransform();
    transform.translate(150, 150);
    transform.scale(1, -1);
    transform.scale(75, 50);

The origin of the coordinate system is moved to the center of the JPanel, the vertical coordinate is scaled so that it increases as we move upward and finally, both coordinates are scaled so that the viewing rectangle will fill up the JPanel.

Constructing the grid

    g.setPaint(Color.lightGray);
    GeneralPath path = new GeneralPath();
    for (int i = -2; i <= 2; i++) {
        path.moveTo(i, -3);
        path.lineTo(i, 3);
    }
    for (int i = -3; i <= 3; i++) {
        path.moveTo(-2, i);
        path.lineTo(2, i);
    }
    g.draw(transform.createTransformedShape(path));

Constructing the graph

    int steps = 50;
    float dx = 4.0f/steps;
    g.setPaint(Color.black);
    path = new GeneralPath();
    path.moveTo(-2, valueAt(-2));
    for (float x = -2+dx;  x <= 2; x += dx)
        path.lineTo(x, valueAt(x));
    g.draw(transform.createTransformedShape(path));

Here the method valueAt(double x) returns the value of the function evaluated at x.


Stroking lines

Here are some examples illustrating how a Graphics2D's stroke attribute may be modified.

BasicStroke stroke = new BasicStroke(50);
g.setStroke(stroke);
g.draw(new Line2D.Float(50, 50, 250, 150));

This example gives some insight into the why a non-uniform scaling of a Graphics2D's AffineTransform results in a curve of varying thickness: the outline of the stroked curve is transformed.

BasicStroke stroke = new BasicStroke(25f, BasicStroke.CAP_BUTT,
                                     BasicStroke.JOIN_MITER);

BasicStroke stroke = new BasicStroke(25f, BasicStroke.CAP_ROUND,
                                     BasicStroke.JOIN_MITER);

BasicStroke stroke = new BasicStroke(25f, BasicStroke.CAP_SQUARE,
                                     BasicStroke.JOIN_MITER);

BasicStroke stroke = new BasicStroke(5f, BasicStroke.CAP_BUTT,
                                     BasicStroke.JOIN_MITER, 1f,
                                     new float[] {10, 5, 5, 5}, 2);


Rendering hints

Rendering hints control some finer points in how a Shape is rendered. One of the most useful controls anti-aliasing. In a black and white image, anti-aliasing allows some pixels to be colored in shades of gray to capture the fact that, say, a line passes nearby. This produces images that appear smoother.

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

Anti-aliasing

Compare the following images: on the left, anti-aliasing is turned off and on the right, it is turned on.