Concepts / Drawing Shapes with a Graphics Context

Drawing Shapes with a Graphics Context

A computer animation is a sequence of individual pictures, each called a frame, shown quickly one after another.

  • Programming

Predicting an Edge You Cannot See

Suppose you want a border around a window's drawing area. The obvious approach is to ask for a rectangle exactly as large as the area itself. Before you read on, consider what that choice does to the right and bottom edges. This article traces what the Graphics class actually draws, one step at a time, so you can check your prediction against the model rather than against a memorized rule.

What do you think happens?

Inside a paint method, you call g.drawRect(0, 0, getWidth(), getHeight()). Which edges of the component show the rectangle?

  • All four edges, because the rectangle matches the component size

  • Only the top and left edges; the right and bottom outline pixels fall outside the component

  • No edges, because drawing a rectangle of full size is an error

Reveal answer
Answer:

Only the top and left edges show. The outline is drawn one pixel wider and one pixel taller than the width and height passed in, so its right column sits at x = getWidth() and its bottom row at y = getHeight(). A component's valid pixel positions run from 0 to getWidth() minus 1 horizontally and 0 to getHeight() minus 1 vertically, so those right and bottom pixels lie outside the component and are not shown.

The same two facts explain the whole article: the outline is one pixel larger than the numbers you pass, and anything beyond the component's boundary is ignored.

A Animation Is a Sequence of Frames

A computer animation is a sequence of individual pictures, each called a frame, shown quickly one after another. Think about a flipbook. Each page holds a slightly different drawing, and when you flip through the pages quickly, the drawings seem to slide, grow, or turn. No single page moves. The motion exists only in the change from one page to the next.

Frame-based animation in a computer works on exactly this principle. The animation is not one object that the program carries around the screen. It is a collection of separate pictures, and each picture is complete in itself. The illusion of motion comes from the relationship between neighboring pictures, which differ only by a small amount. If the change from one picture to the next is small, the viewer perceives one continuous movement. If the change is large, the sequence breaks apart into unrelated images.

Key point

Motion is not drawn in any one frame. It is produced by small differences between consecutive frames, shown at a steady rate.

The drawFrame Subroutine

A frame-based animation program is organized around a subroutine named drawFrame. The sample program SimpleAnimationStarter.java provides this subroutine as a starting point, and a programmer creates an animation by filling in the body of drawFrame. Each call to drawFrame draws exactly one frame, so drawFrame is the place where each picture is produced.

Besides the graphics context and the width and height of the drawing area, the code inside drawFrame can read two further values. The first is frameNumber, which counts the calls. The second is elapsedSeconds, which records how many seconds the animation has been running. The source pack does not specify how precisely elapsedSeconds is stored, so you should treat it as a value that grows steadily as time passes.

Tracing frameNumber and elapsedSeconds

Trace the counter first. frameNumber takes the values 0, 1, 2, 3, and so on in successive calls, increasing by one each time drawFrame runs. Read the diagram as a timeline: each box is one invocation of drawFrame, and the number inside it is the value the program sees on that invocation. Because the counter goes up on every call, the program can use it to decide where a shape should be, how large it should be, or how it should be colored in that particular frame.

drawFrame call 0frameNumber 0drawFrame call 1frameNumber 1drawFrame call 2frameNumber 2drawFrame call 3frameNumber 3frameNumber + 1frameNumber + 1frameNumber + 1
What value does frameNumber hold on each successive drawFrame call, and how does elapsedSeconds advance alongside it?
Worked example

Following the two counters through four calls

The animation has just started. Follow frameNumber and elapsedSeconds through the first four calls to drawFrame, which the source describes as running about 60 times per second.

  1. 1
    First call

    frameNumber is 0. The animation has only just begun, so elapsedSeconds is close to 0.

  2. 2
    Second call

    frameNumber is 1. The count went up by one because drawFrame was called again. Time has moved on slightly, so elapsedSeconds is a little larger.

  3. 3
    Third and fourth calls

    frameNumber is 2, then 3. Each call adds one to the count, and elapsedSeconds keeps growing.

  4. 4
    Connecting the two

    At about 60 calls per second, one call happens roughly every sixtieth of a second. Each frame therefore sees a slightly different counter pair, and that difference is what lets it differ from the one before.

Result

frameNumber goes 0, 1, 2, 3 while elapsedSeconds keeps increasing. Together they give every frame its own identity, so the drawFrame code can make each picture different from the last.

Writing drawFrame That Uses the Counter

The source describes an animation of nested rectangles that shrink toward the center of the drawing. Each frame shows the rectangles slightly smaller than the frame before, so the shapes keep receding inward. The code below is an original illustration of that idea. It is not the source's code, and the exact method signature in the starter program may differ, so check SimpleAnimationStarter.java for the real form. The point is that frameNumber controls how far the rectangle is inset on each call.

Code walkthrough
void drawFrame(Graphics g, int width, int height) {
    int inset = frameNumber % 50;
    g.drawRect(inset, inset, width - 1 - 2 * inset, height - 1 - 2 * inset);
}
  1. 1
    Line 2 reads the counter. On each call, inset grows by one until it reaches 49, then restarts at 0.
  2. 2
    Line 3 draws one outline. Its upper left corner moves inward by inset on both axes, and its width and height shrink by twice inset.
  3. 3
    Because each call draws one frame, the sequence of insets across calls produces the receding motion.
  • frameNumber % 50 keeps the inset cycling between 0 and 49 so the shapes repeat
  • drawRect places the upper left corner at (inset, inset)
  • width - 1 - 2 * inset shrinks the rectangle as inset grows, leaving room for the one-pixel outline

How Frame Rate Shapes Smoothness

The drawFrame subroutine is called about 60 times per second. That fixed rate turns a list of pictures into a moving scene. Because the calls arrive so rapidly, the viewer does not see the individual pictures as separate events. The eye merges them into a single motion.

Frame rate and the size of each change work together. A high frame rate gives many chances per second to show a new picture, while a low frame rate spreads those chances thinly. If the picture changes only slightly between steps, the high-rate version looks smooth. The low-rate version of the same change can appear to jump from one position to the next.

SituationChange per frameLikely appearance
High frame rate, small changeSmallSmooth, continuous motion
Low frame rate, small changeSmallMotion that appears to jump between positions
High frame rate, large changeLargeSequence may break apart into unrelated images

The relationship described in the source: perceived smoothness depends on how small each change is and how often a new picture appears. The source gives no measured threshold, so these are qualitative relationships.

Why a Rectangle Covers One Extra Pixel

The rectangle method draws the outline of a rectangle. Its upper left corner sits at the point (x, y), and its width and height are the values you pass in. That description is only half the story. The outline is drawn with the same pen used for lines, and that pen colors pixels one pixel to the right of the geometric line and one pixel below it.

The consequence is that the outline's real width is width plus one, and its real height is height plus one. The extra pixel column lies past the right edge, and the extra pixel row lies past the bottom edge. The pixels you see extend one beyond the numbers you typed.

column 0left edgecolumn 1outline pixelcolumn 2outline pixelcolumn 3outline pixelcolumn 4extra pixel past width 4
Which pixel columns does drawRect fill when the width passed in is 4, and why does the last filled column fall one beyond that width?

Parameters of the Four Drawing Methods

All of the methods discussed here belong to the Graphics class, so each one is called through an object of type Graphics. The examples refer to that object as g, but the programmer chooses the actual name. Each of the four methods takes integer coordinates or sizes, and each parameter has a specific job. The table summarizes what each one controls, following the source's descriptions. The source pack describes drawOval only by name and does not give its parameter meanings in detail, so that row is left to the caption rather than explained further here.

MethodParametersWhat each parameter controls
drawStringx, yx is the left end of the text; y is the baseline, the line on which the characters rest
drawLinex1, y1, x2, y2The line runs from (x1, y1) to (x2, y2); the pen extends one pixel right and one pixel down
drawRectx, y, width, height(x, y) is the upper left corner; width and height are sizes, drawn one pixel larger in each direction
drawOvalnot detailed in sourceNamed in the source's method group; its parameters are not specified in the source pack

Parameter roles as stated in the source. drawString also uses the current color and font of the graphics context.

Two details are easy to miss. First, drawString's y value is not the top of the text: some characters, such as the tail of a lowercase y or g, extend below the baseline. Second, a line's coordinates refer to the lines between pixels, and the pen extends one pixel right and down from the point, which is why a single point is not invisible.

Choosing Sizes That Keep Edges Visible

Combine the two rules. The rectangle's right column is at x plus width, and the source's one-pixel extra means that column is the one that could fall outside the component. For the right edge to stay visible, that column must be no larger than getWidth() minus 1. The same reasoning gives the bottom edge: y plus height must be no larger than getHeight() minus 1. A rectangle that meets both conditions has its right and bottom edges inside the component.

Worked example

Checking a rectangle against a 200 by 100 component

A component's getWidth() returns 200 and getHeight() returns 100. Is g.drawRect(10, 10, 200, 100) fully visible, and if not, what size fixes it?

  1. 1
    Find the right column

    x plus width is 10 plus 200, which is 210. The valid horizontal positions run from 0 to 199, so 210 is far outside.

  2. 2
    Find the bottom row

    y plus height is 10 plus 100, which is 110. The valid vertical positions run from 0 to 99, so 110 is also outside.

  3. 3
    Pick a fitting size

    For the right edge to be visible, width must satisfy 10 plus width at most 199, so width can be at most 189. For the bottom edge, height can be at most 89.

  4. 4
    Use the border idiom for the full area

    To outline the whole component from the corner, use (0, 0, getWidth() minus 1, getHeight() minus 1), which gives 199 and 99.

Result

g.drawRect(10, 10, 189, 89) keeps both edges visible. For a full-component border, g.drawRect(0, 0, getWidth() - 1, getHeight() - 1) is the correct form.

What Happens Outside the Component

All of the drawing commands in this group respect the boundaries of the component. Any drawing done outside those boundaries is ignored. This is why the getWidth() minus 1 adjustment matters: pixels placed past the component's right or bottom edge are simply not shown. A shape that fits entirely inside is shown whole, while a shape that extends past an edge shows only the part within the boundary, with the remainder missing.

Mistakes Beginners Make with Borders and Sizes

  • Common mistake

    Assuming a rectangle with width 100 and height 50 covers exactly 100 by 50 pixels

    What it looks like
    g.drawRect(0, 0, 100, 50) in a 100 by 50 component
    Why it fails

    The outline is drawn one pixel wider and one pixel taller than the values passed in, so the right and bottom pixels land at 100 and 50.

    Do this instead

    Expect the outline to extend one pixel past the stated size, and subtract one when you want it to end on the last pixel inside.

  • Common mistake

    Drawing a full-size border with the component's own dimensions

    What it looks like
    g.drawRect(0, 0, getWidth(), getHeight())
    Why it fails

    The extra pixel falls at getWidth() and getHeight(), which are outside the component, so the right and bottom edges disappear.

    Do this instead
    Use g.drawRect(0, 0, getWidth() - 1, getHeight() - 1).
  • Common mistake

    Expecting drawString's y value to be the top of the text

    Why it fails

    The y value marks the baseline, and descending characters extend below it.

    Do this instead

    Think of y as the line the text sits on, and leave room below it for descenders.

  • Common mistake

    Believing that a frame changes because the program moves an object

    Why it fails

    The animation is a collection of separate pictures. Each frame is produced fresh by drawFrame, and motion comes from differences between frames.

    Do this instead

    Compute each frame's contents from frameNumber and elapsedSeconds rather than trying to move one persistent shape.

Practice: Predict Before You Compute

MEDIUM

A component returns getWidth() equal to 300 and getHeight() equal to 150. Choose a call to drawRect that starts at (20, 20) and keeps both its right and bottom edges visible. Then explain in one sentence why your width and height are one less than the maximum that the component's dimensions would seem to allow.

Hints
  • The right column of the outline is at x plus width, and it must be no larger than getWidth() minus 1.
  • Work out the largest valid right column (299) and bottom row (149) first, then subtract the starting corner from each.
  • The one-pixel extra in the outline is the reason the subtraction is needed.
HARD

Modify the nested-rectangle drawFrame example so that the shapes stop shrinking at a minimum inset and then restart. Predict which frameNumber value first produces the restart before you run it.

Hints
  • Compare frameNumber with a limit and reset the counter-driven value when it passes that limit.
  • Make sure the width and height arguments never become negative.

Key Takeaways

  • A computer animation is a sequence of separate frames, and motion comes from small differences between neighboring frames shown at a steady rate.
  • drawFrame draws exactly one frame per call, and frameNumber and elapsedSeconds let each call produce a different picture.
  • A rectangle's outline is drawn one pixel wider and one pixel taller than its width and height, so g.drawRect(0, 0, getWidth() - 1, getHeight() - 1) is the correct full-component border.
  • Drawing outside a component's boundaries is ignored, and a negative width or height draws nothing.