Home
10Animation with Threads
10

Lab Manual X · CSE 202

Animation with Threads

Animate bouncing balls: a worker thread updates positions about 60 times a second, a custom panel draws each frame with double buffering, and Start, Pause and Reset buttons control it.

← → move between slides · F full screen · M slide list

Before we start

Why this lab

A film is just still pictures shown quickly. How can a Java program make a ball look like it is moving?

Draw it, move it a little, draw it again — about 60 times a second. A worker thread runs that loop while Swing keeps the window responsive.

Lab 10 of 10

Lab at a glance

After this lab you can

  • Explain frame-based animation
  • Implement an update–repaint–sleep animation loop in a worker thread
  • Control the frame rate with Thread.sleep()
  • Draw frames with paintComponent and Graphics2D
  • Detect collisions with walls and between balls
  • Control the animation with Start, Pause and Reset buttons

Before you start

  • Lab 7: threads and Runnable
  • Lab 8: synchronized methods
  • Lab 9: JFrame, JPanel, paintComponent, ActionListener

Plan for the session

  1. Theory recap
  2. Walk through the program, section by section, and run it
  3. In-lab tasks, shown to the instructor
  4. Report exercises at home

Theory 1 of 5

Frame-based animation

  • Each frame: update the state, redraw, wait. Repeated quickly, the eye sees smooth motion.
  • About 16 ms per frame gives roughly 60 frames per second.

Frame-based animation

x = x + vx;
y = y + vy;

Theory 2 of 5

The worker thread and the EDT

while (true) {
    panel.updateAnimation();   // move the balls
    panel.repaint();           // ask Swing to draw
    Thread.sleep(16);          // ~60 FPS
}
  • The loop runs in AnimationThread so the Event Dispatch Thread stays free for clicks and painting.
  • Both threads touch the balls, so AnimationPanel’s methods are synchronized.

Theory 3 of 5

Double buffering

BufferedImage buffer = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = buffer.createGraphics();
// ... draw everything on g2d ...
g.drawImage(buffer, 0, 0, null);
  • Draw the whole frame off-screen, then copy it at once: no half-drawn frames, no flicker.
  • Swing already double-buffers; doing it by hand shows the idea.

Theory 4 of 5

Collisions

  • Wall: reverse the velocity component. Ball–ball: centres closer than the sum of radii.

Collisions

if (x - radius < 0 || x + radius > panelWidth) vx = -vx;   // wall

double d = Math.sqrt(dx * dx + dy * dy);
if (d <= r1 + r2) { /* balls touch: swap velocities */ }

Theory 5 of 5

Applets and modern animation

  • Classic textbook animations used applets; browsers dropped them, so this lab animates in a JFrame.
  • The ideas — update state, repaint, control timing — are exactly the same.

Implementation design

Program design

Implementation design

Execution flow

  1. main creates AnimationApp on the EDT
  2. AnimationApp builds the panel and buttons and starts a daemon AnimationThread
  3. The thread loops: updateAnimation(), repaint(), sleep the rest of the 16 ms frame
  4. updateAnimation moves balls only while running and handles collisions
  5. Start / Pause flip the running flag; Reset restores the three balls
  6. Closing the window ends the program (the thread is a daemon)

Ball = model, AnimationPanel = rendering + state, AnimationThread = loop, AnimationApp = controls.

Code walkthrough 1 of 10 · Ball.java

Moving and bouncing

public void update(int panelWidth, int panelHeight) {
    x = x + vx;
    y = y + vy;
    if (x - radius < 0 || x + radius > panelWidth) {
        vx = -vx;
        x = Math.max(radius, Math.min(panelWidth - radius, x));
    }
    if (y - radius < 0 || y + radius > panelHeight) {
        vy = -vy;
        y = Math.max(radius, Math.min(panelHeight - radius, y));
    }
}
  • Add the velocity each frame; reverse it at a wall and keep the ball inside.

Code walkthrough 2 of 10 · Ball.java

Ball-to-ball collisions

public boolean collidesWith(Ball other) {
    double dx = this.x - other.x;
    double dy = this.y - other.y;
    double distance = Math.sqrt(dx * dx + dy * dy);
    return distance <= this.radius + other.radius;
}

public void bounceOff(Ball other) {
    double tempVx = this.vx;

Code walkthrough 2 of 10 · Ball.java

Ball-to-ball collisions (cont.)

    double tempVy = this.vy;
    this.vx = other.vx;
    this.vy = other.vy;
    other.vx = tempVx;
    other.vy = tempVy;
}
  • Touching when the centres are closer than the radii added together.
  • Swapping velocities is a simple bounce.

Code walkthrough 3 of 10 · AnimationPanel.java

Double buffering: draw off screen

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    if (getWidth() <= 0 || getHeight() <= 0) {
        return;
    }
    BufferedImage buffer = new BufferedImage(getWidth(), getHeight(),
            BufferedImage.TYPE_INT_RGB);
    Graphics2D g2d = buffer.createGraphics();
    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
    g2d.setColor(Color.WHITE);
    g2d.fillRect(0, 0, getWidth(), getHeight());
    drawGrid(g2d);
  • Draw the whole frame into a BufferedImage first, so the screen never shows half a frame.

Code walkthrough 4 of 10 · AnimationPanel.java

…then copy it at once

synchronized (this) {
    for (Ball ball : balls) {
        ball.draw(g2d);
    }
    g2d.setColor(Color.BLACK);
    g2d.drawString("Balls: " + balls.size() + " | "
            + (running ? "Running" : "Paused"), 10, getHeight() - 10);
}
g.drawImage(buffer, 0, 0, null);
g2d.dispose();
  • The synchronized block reads the balls safely while the worker thread may update them.
  • One drawImage call shows the finished frame.

Code walkthrough 5 of 10 · AnimationPanel.java

One animation step: move

public synchronized void updateAnimation() {
    if (!running) {
        return;
    }
    int width = getWidth();
    int height = getHeight();
    if (width <= 0 || height <= 0) {
        return;
    }
    for (Ball ball : balls) {
        ball.update(width, height);
    }
  • Nothing happens while paused.
  • Each ball moves and bounces off the walls.

Code walkthrough 6 of 10 · AnimationPanel.java

One animation step: collide

for (int i = 0; i < balls.size(); i++) {
    for (int j = i + 1; j < balls.size(); j++) {
        Ball first = balls.get(i);
        Ball second = balls.get(j);
        if (first.collidesWith(second)) {
            first.bounceOff(second);
        }
    }
}
  • j starts at i + 1, so each pair is tested once.

Code walkthrough 7 of 10 · AnimationThread.java

The animation loop

@Override
public void run() {
    while (true) {
        long frameStartTime = System.currentTimeMillis();

        panel.updateAnimation();
        panel.repaint();
  • An endless loop: one pass per frame.
  • Update the positions, then ask Swing to repaint.

Code walkthrough 8 of 10 · AnimationThread.java

Keeping a steady frame rate

long elapsedTime = System.currentTimeMillis() - frameStartTime;
long sleepTime = FRAME_TIME_MS - elapsedTime;
if (sleepTime > 0) {
    try {
        Thread.sleep(sleepTime);
    } catch (InterruptedException e) {
        System.out.println("Animation thread interrupted.");
        Thread.currentThread().interrupt();
        break;
    }
}
  • Sleep only for what is left of the 16 ms frame (about 60 FPS).
  • An interrupt ends the loop.

Code walkthrough 9 of 10 · AnimationApp.java

Buttons control a flag

private void addActionListeners() {
    startButton.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            animationPanel.startAnimation();
            updateStatus("Running");
        }
    });
  • Start only sets running; the loop keeps going.
  • Pause and Reset are written the same way.

Code walkthrough 10 of 10 · AnimationApp.java

Starting the worker thread

private void startWorkerThread() {
    animationThread = new Thread(new AnimationThread(animationPanel));
    animationThread.setDaemon(true);
    animationThread.start();
}
  • A daemon thread does not keep the program alive after the window closes.

Step through the code · 1 of 2

Trace: one animation frame

Move, bounce off the wall, detect a collision and swap velocities.

Step through the code · 1 of 2

Trace: one animation frame (cont.)

Step through the code · 2 of 2

Trace: the animation thread and the EDT

The Animator moves the balls, the EDT draws them. The panel’s lock makes sure the EDT never paints a half-moved frame.

Step through the code · 2 of 2

Trace: the animation thread and the EDT (cont.)

For reference · 5 files

The complete program

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;

public class Ball {

    private double x;
    private double y;
    private double vx;
    private double vy;
    private int radius;
    private Color colour;

    public Ball(double x, double y, double vx, double vy, int radius, Color colour) {
        this.x = x;
        this.y = y;
        this.vx = vx;
        this.vy = vy;
        this.radius = (radius > 0) ? radius : 15;
        this.colour = colour;
    }

    public void update(int panelWidth, int panelHeight) {
        x = x + vx;
        y = y + vy;
        if (x - radius < 0 || x + radius > panelWidth) {
            vx = -vx;
            x = Math.max(radius, Math.min(panelWidth - radius, x));
        }
        if (y - radius < 0 || y + radius > panelHeight) {
            vy = -vy;
            y = Math.max(radius, Math.min(panelHeight - radius, y));
        }
    }

    public void draw(Graphics2D g) {
        int diameter = 2 * radius;
        g.setColor(colour);
        g.fillOval((int) (x - radius), (int) (y - radius), diameter, diameter);
        g.setColor(Color.BLACK);
        g.setStroke(new BasicStroke(2));
        g.drawOval((int) (x - radius), (int) (y - radius), diameter, diameter);
    }

    public boolean collidesWith(Ball other) {
        double dx = this.x - other.x;
        double dy = this.y - other.y;
        double distance = Math.sqrt(dx * dx + dy * dy);
        return distance <= this.radius + other.radius;
    }

    public void bounceOff(Ball other) {
        double tempVx = this.vx;
        double tempVy = this.vy;
        this.vx = other.vx;
        this.vy = other.vy;
        other.vx = tempVx;
        other.vy = tempVy;
    }

    public double getX() { return x; }
    public double getY() { return y; }
    public int getRadius() { return radius; }
}
import javax.swing.JPanel;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.ArrayList;

public class AnimationPanel extends JPanel {

    private ArrayList<Ball> balls;
    private boolean running;

    public AnimationPanel() {
        balls = new ArrayList<Ball>();
        running = false;
        initializeBalls();
        setBackground(Color.WHITE);
    }

    private void initializeBalls() {
        balls.clear();
        balls.add(new Ball(100, 100, 3.0, 2.0, 15, Color.RED));
        balls.add(new Ball(300, 150, -2.5, 3.5, 15, Color.BLUE));
        balls.add(new Ball(200, 300, 2.0, -2.0, 15, Color.GREEN));
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        if (getWidth() <= 0 || getHeight() <= 0) {
            return;
        }
        BufferedImage buffer = new BufferedImage(getWidth(), getHeight(),
                BufferedImage.TYPE_INT_RGB);
        Graphics2D g2d = buffer.createGraphics();
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setColor(Color.WHITE);
        g2d.fillRect(0, 0, getWidth(), getHeight());
        drawGrid(g2d);

        synchronized (this) {
            for (Ball ball : balls) {
                ball.draw(g2d);
            }
            g2d.setColor(Color.BLACK);
            g2d.drawString("Balls: " + balls.size() + " | "
                    + (running ? "Running" : "Paused"), 10, getHeight() - 10);
        }
        g.drawImage(buffer, 0, 0, null);
        g2d.dispose();
    }

    private void drawGrid(Graphics2D g2d) {
        g2d.setColor(new Color(220, 220, 220));
        for (int x = 0; x < getWidth(); x += 50) {
            g2d.drawLine(x, 0, x, getHeight());
        }
        for (int y = 0; y < getHeight(); y += 50) {
            g2d.drawLine(0, y, getWidth(), y);
        }
    }

    public synchronized void updateAnimation() {
        if (!running) {
            return;
        }
        int width = getWidth();
        int height = getHeight();
        if (width <= 0 || height <= 0) {
            return;
        }
        for (Ball ball : balls) {
            ball.update(width, height);
        }
        for (int i = 0; i < balls.size(); i++) {
            for (int j = i + 1; j < balls.size(); j++) {
                Ball first = balls.get(i);
                Ball second = balls.get(j);
                if (first.collidesWith(second)) {
                    first.bounceOff(second);
                }
            }
        }
    }

    public synchronized void startAnimation() { running = true; }

    public synchronized void pauseAnimation() { running = false; }

    public synchronized void resetAnimation() {
        running = false;
        initializeBalls();
        repaint();
    }

    public synchronized boolean isRunning() { return running; }

    public synchronized int getBallCount() { return balls.size(); }
}
public class AnimationThread implements Runnable {

    private AnimationPanel panel;

    private static final int TARGET_FPS = 60;
    private static final long FRAME_TIME_MS = 1000 / TARGET_FPS;

    public AnimationThread(AnimationPanel panel) {
        this.panel = panel;
    }

    @Override
    public void run() {
        while (true) {
            long frameStartTime = System.currentTimeMillis();

            panel.updateAnimation();
            panel.repaint();

            long elapsedTime = System.currentTimeMillis() - frameStartTime;
            long sleepTime = FRAME_TIME_MS - elapsedTime;
            if (sleepTime > 0) {
                try {
                    Thread.sleep(sleepTime);
                } catch (InterruptedException e) {
                    System.out.println("Animation thread interrupted.");
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }
    }
}
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class AnimationApp extends JFrame {

    private AnimationPanel animationPanel;
    private JButton startButton;
    private JButton pauseButton;
    private JButton resetButton;
    private JLabel statusLabel;
    private Thread animationThread;

    public AnimationApp() {
        setTitle("Bouncing Balls Animation");
        setSize(800, 600);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setupUI();
        addActionListeners();
        startWorkerThread();
        setVisible(true);
    }

    private void setupUI() {
        JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
        mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));

        JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
        JLabel titleLabel = new JLabel("Bouncing Balls Animation");
        titleLabel.setFont(new Font("Arial", Font.BOLD, 16));
        topPanel.add(titleLabel);

        animationPanel = new AnimationPanel();
        animationPanel.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2));

        JPanel controlPanel = new JPanel(new BorderLayout());
        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
        startButton = new JButton("Start");
        pauseButton = new JButton("Pause");
        resetButton = new JButton("Reset");
        buttonPanel.add(startButton);
        buttonPanel.add(pauseButton);
        buttonPanel.add(resetButton);

        statusLabel = new JLabel("Status: Initialized");
        controlPanel.add(buttonPanel, BorderLayout.CENTER);
        controlPanel.add(statusLabel, BorderLayout.SOUTH);

        mainPanel.add(topPanel, BorderLayout.NORTH);
        mainPanel.add(animationPanel, BorderLayout.CENTER);
        mainPanel.add(controlPanel, BorderLayout.SOUTH);
        setContentPane(mainPanel);
    }

    private void addActionListeners() {
        startButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                animationPanel.startAnimation();
                updateStatus("Running");
            }
        });
        pauseButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                animationPanel.pauseAnimation();
                updateStatus("Paused");
            }
        });
        resetButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                animationPanel.resetAnimation();
                updateStatus("Reset");
            }
        });
    }

    private void startWorkerThread() {
        animationThread = new Thread(new AnimationThread(animationPanel));
        animationThread.setDaemon(true);
        animationThread.start();
    }

    private void updateStatus(String status) {
        statusLabel.setText("Status: " + status + " | Balls: " + animationPanel.getBallCount());
    }
}
import javax.swing.SwingUtilities;

public class Lab10Demo {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new AnimationApp();
            }
        });
    }
}

Copy or download each file, save it under the same name, then compile and run: javac *.java → java Lab10Demo

Implementation

Expected output

The program opens a window instead of printing to the console. When it works you should see:

  • A window titled “Bouncing Balls Animation” with a gridded white panel and three coloured balls
  • Start moves the balls; they bounce off the walls and swap directions when they collide
  • Pause freezes them; Reset puts them back and pauses
  • The panel shows the ball count and Running / Paused

Interactive

Try the bouncing balls

In-lab task 1 of 3

Task 1: Add a fourth ball

One more ball in the simulation.
Requirements
Show a hint
Start it away from the other balls so it does not overlap at frame 0.

In-lab task 2 of 3

Task 2: Velocity damping

Make the balls slow down gradually.
Requirements
Show a hint
After damping: if (Math.abs(vx) < 0.05) vx = 0; and the same for vy.

In-lab task 3 of 3

Task 3: Frame-rate display

Show the actual frames per second.
Requirements
Show a hint
Every second: fps = frames counted during that second; then reset the counter.

In-lab tasks

Show your work during the lab

For each task, show the instructor:

  • the source code
  • a successful compilation
  • the running output
  • a short explanation of the logic you used

Lab report · exercise 1 of 2

Physics: gravity and bounce

Add gravity and energy loss.
Requirements

Lab report · exercise 2 of 2

Create balls with the mouse

Click the canvas to add balls.
Requirements

Lab report

Writing the lab report

  • Problem statement and your design (classes, fields, methods)
  • Complete source code, formatted and commented
  • Sample output or screenshots for normal and edge cases
  • A short discussion: what each OOP concept did in your program

Academic integrity

Copying from the internet, classmates, seniors or any other unauthorized source is strictly prohibited. Full marks may be deducted if plagiarism, copied work or academic dishonesty is detected. Complete the tasks, implementation, output analysis and report yourself.

Discussion and analysis

Viva questions (1 of 2)

Why run the animation loop in a worker thread and not in an ActionListener?
Show the answer
A loop on the EDT would block it: the window could not repaint or respond to clicks.
Why are AnimationPanel’s methods synchronized?
Show the answer
The worker thread updates the ball list while the EDT paints it; synchronization stops them seeing it half-updated.

Discussion and analysis

Viva questions (2 of 2)

What problem does double buffering solve?
Show the answer
Flicker: the user only ever sees complete frames.
Why is the thread a daemon?
Show the answer
So it does not keep the JVM alive after the window is closed.

Check yourself

Quiz

Summary

Summary

  • Animation = update state, redraw, wait — repeated about 60 times a second.
  • A worker thread runs the loop; the EDT handles painting and events.
  • Synchronize state shared between the two threads.
  • Double buffering draws complete frames off-screen.
  • Walls reverse a velocity; overlapping balls swap velocities.