[ Exercises | Chapter Index | Main Index ]

Solution for Programming Exercise 3.9


This page contains a sample solution to one of the exercises from Introduction to Programming Using Java.


Exercise 3.9:

Often, some element of an animation repeats over and over, every so many frames. Sometimes, the repetition is "cyclic," meaning that at the end it jumps back to the start. Sometimes the repetition is "oscillating," like a back-and-forth motion where the second half is the same as the first half played in reverse.

Write an animation that demonstrates both cyclic and oscillating motions at various speeds. For cyclic motion, you can use a square that moves across the drawing area, then jumps back to the start, and then repeats the same motion over and over. For oscillating motion, you can do something similar, but the square should move back and forth between the two edges of the drawing area; that is, it moves left-to-right during the first half of the animation and then backwards from right-to-left during the second half. To write the program, you can start with a copy of the sample program SimpleAnimationStarter.java, as in the previous exercise.

A cyclic motion has to repeat every N frames for some value of N. What you draw in some frame of the animation depends on the frameNumber. The frameNumber just keeps increasing forever. To implement cyclic motion, what you really want is a "cyclic frame number" that takes on the values 0, 1, 2, ..., (N-1), 0, 1, 2, ..., (N-1), 0, 1, 2, .... You can derive the value that you need from frameNumber simply by saying

cyclicFrameNumber = frameNumber % N;

Then, you just have to base what you draw on cyclicFrameNumber instead of on frameNumber. Similarly, for an oscillating animation, you need an "oscillation frame number" that takes on the values 0, 1, 2, ... (N-1), N, (N-1), (N-2), ... 2, 1, 0, 1, 2, and so on, repeating the back and forth motion forever. You can compute the value that you need with

oscilationFrameNumber = frameNumber % (2*N);
if (oscillationFrameNumber > N)
   oscillationFrameNumber = (2*N) - oscillationFrameNumber;

Here is a screen shot from my version of the program. I use six squares. The top three do cyclic motion at various speeds, while the bottom three do oscillating motion. I drew black lines across the drawing area to separate the squares and to give them "channels" to move in.

screenshot from CyclicAndOscillatingMotionDemo


Discussion

This exercise exists mostly to introduce you to cyclic and oscillating motion and the formulas that are used to implement them. But it's also a good exercise in designing an animation and working with coordinates.

Some decisions have to be made before you can start writing code. I decided to use rather small squares. (Actually, I tried it with big squares and didn't like the appearance as much.) The size of my squares is 20-by-20 pixels. For my first cyclic motion, I decided to make the length of the frame 300 frames, or about 6 seconds. This means that cyclicFrameNumber can be computed as frameNumber % 300. And I decided to move the square at one pixel per frame; that lets me use cyclicFrameNumber as the x-coordinate for the square. The top square moves along the top edge of the window, so its y-coordinate is 0. The code for the first square is:

cyclicFrameNum = frameNumber % 300;  // Repeats every 300 frames
g.setColor(Color.RED);
g.fillRect( cyclicFrameNum, 0, 20, 20 );

The exercise asks for cyclic motion at different speeds. If the second square is to be faster than the first, it should cross the drawing area in a smaller number of frames. If it is twice as fast, it will only need half as many frames to make it across the drawing area. For the second square, I use a cycle length of 150 instead of 300. To cross the entire area, the x-coordinate of the square must get to 300 after just 150 frames. We can accomplish that by using 2*cyclicFrameNumber as the x-coord. (Another way to think about it: A square with x-coord 2*cyclicFrameNumber moves twice as fast as one with x-coord cyclicFrameNumber. And it will finish the journey after 150 frames instead of 300, so the length of the cycle should be 150 frames.) The code for the second square is

cyclicFrameNum = frameNumber % 150;  // Repeats every 150 frames
g.setColor(Color.GREEN);
g.fillRect( 2*cyclicFrameNum, 20, 20, 20 );

The y-coord is 20, since the top square occupies 20 pixels at top of the window. The third square moves three times as fast as the first, and its code is similar.

For the fourth square, I want an oscillating motion. Like the first square, it takes 300 frames to move across the window from left to right. But then it takes another 300 frames to move back from right to left. The total length of the motion is 600. (In the formula given in the exercise, N is 300 and 2*N is 600.) The formula for the oscilation frame number is

oscilationFrameNum = frameNumber % 600;
if (oscilationFrameNum > 300)
    oscilationFrameNum = 600 - oscilationFrameNum;

You should study how this works. When oscilationFrameNumber is between 300 and 600, the formula 600-oscilationFrameNumber gives an answer between 0 and 300. And as oscilationFrameNumber gets bigger, 600-oscilationFrameNumber gets smaller (which will mean that the x-coord of the square is decreasing). From there, you should be able to figure out the rest of the program.

One more small issue is the size of the drawing area. Since there are 6 squares, and each is 20 pixels high, the height of the drawing area must be 120. You might think at first that the width should be 300, since the x-coordinate of the square goes up to 300. But that x-coordinate is actually where the left edge of the square is. When the left edge is at 300, the right edge is at 320. So for the entire square to be visible, we have to make the width 320.


The Solution

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/**
 * This program demonstrates cyclic and oscillating animations.  For cyclic animation,
 * it repeats itself every N frames, for some value of N.  An oscillating animation also
 * repeats, but the repetition is "back-and-forth."  That is, the second half
 * of the repeated animation is the same as the first half played backwards.
 */
public class CyclicAndOscillatingMotionDemo extends JPanel implements ActionListener {

    public void drawFrame(Graphics g, int frameNumber, int width, int height) {

        
        /* Show cyclic motion at three speeds.  In each case, a square 
         * moves across the drawing area from left to right, then jumps
         * back to the start.
         */

        int cyclicFrameNum;
        
        cyclicFrameNum = frameNumber % 300;  // Repeats every 300 frames
        g.setColor(Color.RED);
        g.fillRect( cyclicFrameNum, 0, 20, 20 );
        
        cyclicFrameNum = frameNumber % 150;  // Repeats every 150 frames
        g.setColor(Color.GREEN);
        g.fillRect( 2*cyclicFrameNum, 20, 20, 20 );
        
        cyclicFrameNum = frameNumber % 100;  // Repeats every 100 frames
        g.setColor(Color.BLUE);
        g.fillRect( 3*cyclicFrameNum, 40, 20, 20 );
        

        /* Show oscillating motion at three speeds.  In each case, a square 
         * moves across the drawing area from left to right, then reverses
         * direction to move from right to left back to its starting point.
         */
        
        int oscillationFrameNum;
        
        oscillationFrameNum = frameNumber % 600;  // repeats every 600 frames
        if (oscillationFrameNum > 300)
            oscillationFrameNum = 600 - oscillationFrameNum; // after 300, the values go backwards back to 0
        g.setColor(Color.CYAN);
        g.fillRect( oscillationFrameNum, 60, 20, 20 );
        
        oscillationFrameNum = frameNumber % 300; // repeats every 300 frames
        if (oscillationFrameNum > 150)
            oscillationFrameNum = 300 - oscillationFrameNum; // after 150, the values go backwards back to 0
        g.setColor(Color.MAGENTA);
        g.fillRect( 2*oscillationFrameNum, 80, 20, 20 );
        
        oscillationFrameNum = frameNumber % 200; // repeats every 200 frames
        if (oscillationFrameNum > 100)
            oscillationFrameNum = 200 - oscillationFrameNum; // after 100, the values go backwards back to 0
        g.setColor(Color.YELLOW);
        g.fillRect( 3*oscillationFrameNum, 100, 20, 20 );
        
        
        /* Draw horizontal black lines across the window to separate the
         * regions used by the six squares.  Also draw a box around the outside,
         * mostly for the picture that I need for the web page!
         */
        
        int y;
        g.setColor(Color.BLACK);
        for ( y = 20; y < 120; y = y + 20 )
            g.drawLine(0,y,320,y);
        g.drawRect(0,0,319,119);  // Why not (0,0,320,120)? This is a technicality.
                                  // If you use 320 and 120, the right and bottom edges
                                  // of the rect will actually be outside the drawing area.        
    }
    
    //------ Implementation details: DO NOT EXPECT TO UNDERSTAND THIS ------
    
    
    public static void main(String[] args) {
        
        JFrame window = new JFrame("Cyclic and Oscillating Motion");
        CyclicAndOscillatingMotionDemo drawingArea = new CyclicAndOscillatingMotionDemo();
        drawingArea.setBackground(Color.WHITE);
        window.setContentPane(drawingArea);
        drawingArea.setPreferredSize(new Dimension(320,120));  // size is 320 by 120
        window.pack();
        window.setLocation(100,50);
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        window.setResizable(false); // The user can't change the size.
        Timer frameTimer = new Timer(20,drawingArea);
        window.setVisible(true);
        frameTimer.start();

    } // end main

    private int frameNum;
    
    public void actionPerformed(ActionEvent evt) {
        frameNum++;
        repaint();
    }
    
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        drawFrame(g, frameNum, getWidth(), getHeight());
    }

}

[ Exercises | Chapter Index | Main Index ]