Drawing Shapes with a Graphics Context
A computer animation is a sequence of individual pictures, each called a frame, shown quickly one after another.
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?
Reveal 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.
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.
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.
- 1First call
frameNumberis 0. The animation has only just begun, soelapsedSecondsis close to 0. - 2Second call
frameNumberis 1. The count went up by one becausedrawFramewas called again. Time has moved on slightly, soelapsedSecondsis a little larger. - 3Third and fourth calls
frameNumberis 2, then 3. Each call adds one to the count, andelapsedSecondskeeps growing. - 4Connecting 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.
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.
void drawFrame(Graphics g, int width, int height) {
int inset = frameNumber % 50;
g.drawRect(inset, inset, width - 1 - 2 * inset, height - 1 - 2 * inset);
}- 1Line 2 reads the counter. On each call, inset grows by one until it reaches 49, then restarts at 0.
- 2Line 3 draws one outline. Its upper left corner moves inward by inset on both axes, and its width and height shrink by twice inset.
- 3Because 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 repeatdrawRectplaces 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.
| Situation | Change per frame | Likely appearance |
|---|---|---|
| High frame rate, small change | Small | Smooth, continuous motion |
| Low frame rate, small change | Small | Motion that appears to jump between positions |
| High frame rate, large change | Large | Sequence 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.
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.
| Method | Parameters | What each parameter controls |
|---|---|---|
| drawString | x, y | x is the left end of the text; y is the baseline, the line on which the characters rest |
| drawLine | x1, y1, x2, y2 | The line runs from (x1, y1) to (x2, y2); the pen extends one pixel right and one pixel down |
| drawRect | x, y, width, height | (x, y) is the upper left corner; width and height are sizes, drawn one pixel larger in each direction |
| drawOval | not detailed in source | Named 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.
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?
- 1Find 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.
- 2Find 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.
- 3Pick 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.
- 4Use 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.
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 likeg.drawRect(0, 0, 100, 50) in a 100 by 50 componentWhy it failsThe 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 insteadExpect 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 likeg.drawRect(0, 0, getWidth(), getHeight())Why it failsThe extra pixel falls at
getWidth()andgetHeight(), which are outside the component, so the right and bottom edges disappear.Do this insteadUse g.drawRect(0, 0, getWidth() - 1, getHeight() - 1).Common mistake Expecting drawString's y value to be the top of the text
Why it failsThe y value marks the baseline, and descending characters extend below it.
Do this insteadThink 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 failsThe animation is a collection of separate pictures. Each frame is produced fresh by
drawFrame, and motion comes from differences between frames.Do this insteadCompute each frame's contents from frameNumber and elapsedSeconds rather than trying to move one persistent shape.
Practice: Predict Before You Compute
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.
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
frameNumberwith 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.
drawFramedraws exactly one frame per call, andframeNumberandelapsedSecondslet 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.