Java Graphics & Animations

In this lesson we introduce graphics in Java. Obviously GUIs are graphics, so what are we doing now that's different? Well, in the GUI programming we've done so far, the GUI components (e.g. JButton, JLabel, JFrame, ...) have drawn themselves. Now our code will do the drawing. However, the mechanism for drawing is actually the same, whether it's done by API code or by our own code.

In Java's Swing library, all drawing is done by callback methods. In other words, a JComponent-extending class that wants itself drawn overrides the JComponent method:

protected void paintComponent(Graphics g);
and the Event Dispatch Thread will call that method whenever it deems that the component needs to be drawn. When would that happen?

The Graphics g parameter to paintComponent is the thing you use to do the actual drawing. It has methods for drawing rectangles, ellipses, ... , text, and images.

Overriding paintComponent

According to the above, all we have to do to draw on a component is to override its paintComponent method. So ... let's do that!

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

public class Ex0 {
  public static void main(String[] args) {
    JButton b = new GraffitiButton("Attention Please: This is a button");
    b.setPreferredSize(new Dimension(320, 40));

    JFrame f = new JFrame();
    f.add(b);
    f.pack();
    f.setVisible(true);
  }
}
GraffitiButton.java
import java.awt.*;
import javax.swing.*;

public class GraffitiButton extends JButton {
  public GraffitiButton(String s) {
    super(s);
  }

  protected void paintComponent(Graphics g) {
    // Draw the button as normal.
    super.paintComponent(g);

    // Make a white rectangle on top.
    g.setColor(Color.WHITE);
    g.fillRect(75, 5, 140, 15);

    // Write a String in red.
    g.setColor(Color.RED);
    g.drawString("this button stinks!", 80, 18);
  }
}

In this example, the only thing that's new is that we've overridden this "paintComponent" method that we were previously unaware of. Notice that we call super.paintComponent(g) first. That paints the button in the usual fashion, then we go and paint our stuff on top of it.

Graphics2D and the awfulness of backwards compatibility

At some point Java improved its graphics API, but it didn't want to get rid of the earlier stuff (for backwards compatibility reasons), so ... they derived a new class Graphics2D from Graphics (polymorphism in the works), and paintComponent's argument is actually of that new type. However, you have to cast g to Graphics2D explicitly when inside paintComponent if you want to use the new(er) Graphics2D stuff. For us, the nice feature of Graphics2D is that there are methods:

void draw(Shape s);
void fill(Shape s);
that give you outlined or filled-in versions of any object implementing the Interface Shape. And, of course, the API has Shape-derived objects for rectangles and ellipses and lines and more exotic things as well. Here's the exact same program as before, but done with Graphics2D calls.

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

public class Ex1 {
  public static void main(String[] args) {
    JButton b = new GraffitiButton("Attention Please: This is a button");
    b.setPreferredSize(new Dimension(320, 40));

    JFrame f = new JFrame();
    f.add(b);
    f.pack();
    f.setVisible(true);
  }
}
GraffitiButton.java
import java.awt.geom.*;
import java.awt.*;
import javax.swing.*;

public class GraffitiButton extends JButton {
  public GraffitiButton(String s) {
    super(s);
  }

  protected void paintComponent(Graphics g) {
    // Draw the button as normal.
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D)g;

    // Make a white rectangle on top.
    g2.setColor(Color.WHITE);
    g2.fill(new Rectangle2D.Double(75, 5, 140, 15));

    // Write a String in red.
    g2.setColor(Color.RED);
    g2.drawString("this button stinks!", 80, 18);
  }
}

Some of the many different graphics operations

Of course you wouldn't normally draw over a JButton, as we did in the previous examples. You can draw on any JComponent. If you just want a blank space to draw on, you can just extend the base JComponent class. In the example below, we define a DrawArea in this way, and we show off some of the many things you can do with Java graphics. (Note: we use the file catr.png)

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

public class Ex2 {
  public static void main(String[] args) {
    JComponent c = new DrawArea();

    JFrame f = new JFrame();
    f.add(c);
    f.pack();
    f.setVisible(true);
  }
}
DrawArea.java
import java.awt.geom.*;
import java.awt.*;
import javax.swing.*;
import javax.imageio.*;
import java.awt.image.*;
import java.io.*;

public class DrawArea extends JComponent {
  public DrawArea() {
    super();
    setPreferredSize(new Dimension(400, 400));
  }

  protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D)g;

    // This voodoo makes the output prettier
    g2.setRenderingHint(
      RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setRenderingHint(
      RenderingHints.KEY_RENDERING,
      RenderingHints.VALUE_RENDER_QUALITY);

    // Drawing lines
    g2.setColor(Color.BLUE);
    g2.draw(new Line2D.Double(5, 100, 30, 220));
    g2.setColor(Color.ORANGE);
    g2.draw(new Line2D.Double(15, 100, 40, 220));

    // Drawing rectangles, defining colors, overlap
    g2.setColor(Color.RED);
    g2.draw(new Rectangle2D.Double(300, 100, 40, 40));
    g2.setColor(new Color(61, 153, 122));
    g2.fill(new Rectangle2D.Double(250, 100, 40, 40));
    g2.setColor(new Color(255, 51, 0, 255));
    g2.fill(new Rectangle2D.Double(275, 125, 40, 40));
    g2.setColor(new Color(255, 51, 0, 127));
    g2.fill(new Rectangle2D.Double(275, 75, 40, 40));

    // Drawing Ellipses, changing the "stroke"
    g2.setColor(new Color(255, 51, 255, 255));
    g2.fill(new Ellipse2D.Double(20, 320, 60, 60));
    g2.setColor(new Color(155, 51, 255, 255));
    g2.draw(new Ellipse2D.Double(60, 270, 100, 60));
    g2.setColor(new Color(105, 51, 255, 255));
    g2.setStroke(new BasicStroke(5));
    g2.draw(new Ellipse2D.Double(80, 250, 60, 100));

    // Drawing strings, changing fonts
    g2.drawString("This is some text", 20, 20);
    g2.setFont(new Font("Serif", Font.BOLD, 18));
    g2.drawString("Try a serif font", 20, 40);
    g2.setFont(new Font("Monospaced", Font.BOLD, 18));
    g2.drawString("Try a monospace font", 20, 60);

    // Drawing an image
    BufferedImage img = null;
    try {
      img = ImageIO.read(new File("catr.png"));
    } catch (IOException e) {}
    g2.drawImage(img, 80, 120, null);

    // Playing with transforms
    g2.setStroke(new BasicStroke(1));
    AffineTransform savedTf = g2.getTransform();
    g2.translate(300, 300);

    for (int i = 0; i < 24; i++) {
      g2.rotate(Math.PI / 24);
      g2.draw(new Rectangle2D.Double(0, 0, 60, 60));
    }
    g2.setTransform(savedTf);
  }
}

Animation and Mouse Events

As you develop programs with custom graphics, you will likely want to start tracking how the user interacts with your program. We have covered WindowListeners (actions on windows) and ActionListeners (actions on objects), but we can do even more by tracking how the mouse is actually used with a MouseListener. This wonderful interface can be used on individual GUI components, and has methods like mouseEntered which fire when the user's mouse touches a specific component. You can imagine the possibilities here.

We close this topic with an example of how to animate your custom graphics. This code is a slowly moving ball across the screen. Try clicking anywhere on the window and see what happens. You should be able to follow this code and understand it. The gist is this: animation is nothing more than having a thread that changes something about your scene, and calls repaint() periodically to draw the new version of the scene.

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

public class Ex3 {
  public static class AniThread extends Thread {
    private DrawArea da;

    public AniThread(DrawArea d) {
      da = d;
    }

    public void run() {
      while( true ) {
        try {
          Thread.sleep(20);
        } catch (Exception e) {}
        da.step();
        da.repaint();
      }
    }
  }

  public static void main(String[] args) {
    JFrame   f = new JFrame();
    DrawArea d = new DrawArea();

    f.add(d);
    f.pack();
    f.setVisible(true);
    Thread t = new AniThread(d);
    t.start();
  }
}
DrawArea.java
import java.awt.geom.*;
import java.awt.*;
import javax.swing.*;

public class DrawArea extends JComponent {
  private Ball ball;

  public DrawArea() {
    super();
    setPreferredSize(new Dimension(400, 400));
    ball = new Ball(200, 200);
    ball.setGoal(10, 20);
    addMouseListener(ball);
  }

  public void step() {
    ball.step();
  }

  protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D)g;

    // This voodoo makes the output prettier
    g2.setRenderingHint(
      RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setRenderingHint(
      RenderingHints.KEY_RENDERING,
      RenderingHints.VALUE_RENDER_QUALITY);

    // Here is the code for our ball.
    ball.paint(g2);

    // This is an unfortunate necessity
    // that forces the underlying OS
    // windowing system to actually
    // show the updates we've made
    Toolkit.getDefaultToolkit().sync();
  }
}
Ball.java
import java.awt.geom.*;
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;

public class Ball implements MouseListener {
  private double goalx, goaly;
  private double x, y, r, delta;

  public Ball(double x, double y) {
    this.x = x;
    this.y = y;
    r = 4;
    delta = 2;
  }

  public void setGoal(double gx, double gy) {
    goalx = gx;
    goaly = gy;
  }

  public void step() {
    if (Math.sqrt(Math.pow(goaly - y, 2) + Math.pow(goalx - x, 2)) < delta){
      x = goalx;
      y = goaly;
    } else{
      double a = Math.atan2(goaly - y, goalx - x);
      x += delta * Math.cos(a);
      y += delta * Math.sin(a);
    }
  }

  public void paint(Graphics2D g) {
    g.fill(new Ellipse2D.Double(x - r, y - r, 2 * r, 2 * r));
  }

  public void mouseClicked(MouseEvent e) {
    setGoal(e.getX(), e.getY());
  }

  public void mouseEntered(MouseEvent e)  {}
  public void mouseExited(MouseEvent e)   {}
  public void mousePressed(MouseEvent e)  {}
  public void mouseReleased(MouseEvent e) {}
}

The below version is an equivalent but alternate version that implements Runnable instead of using a static inner class that extends Thread.
Ex3.java
import java.awt.*;
import javax.swing.*;

public class Ex3 {

  public static void main(String[] args) {
    JFrame   f = new JFrame();
    DrawArea d = new DrawArea();

    f.add(d);
    f.pack();
    f.setVisible(true);
    Thread t = new Thread(d);
    t.start();
  }
}
DrawArea.java
import java.awt.geom.*;
import java.awt.*;
import javax.swing.*;

public class DrawArea extends JComponent implements Runnable {
  private Ball ball;

  public DrawArea() {
    super();
    setPreferredSize(new Dimension(400, 400));
    ball = new Ball(200, 200);
    ball.setGoal(10, 20);
    addMouseListener(ball);
  }

  public void step() {
    ball.step();
  }

  public void run() {
    while( true ) {
      try {
        Thread.sleep(20);
      } catch (Exception e) {}
      step();
      repaint();
    }
  }

  protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D)g;

    // This voodoo makes the output prettier
    g2.setRenderingHint(
      RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setRenderingHint(
      RenderingHints.KEY_RENDERING,
      RenderingHints.VALUE_RENDER_QUALITY);

    // Here is the code for our ball.
    ball.paint(g2);

    // This is an unfortunate necessity
    // that forces the underlying OS
    // windowing system to actually
    // show the updates we've made
    Toolkit.getDefaultToolkit().sync();
  }
}
Ball.java
import java.awt.geom.*;
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;

public class Ball implements MouseListener {
  private double goalx, goaly;
  private double x, y, r, delta;

  public Ball(double x, double y) {
    this.x = x;
    this.y = y;
    r = 4;
    delta = 2;
  }

  public void setGoal(double gx, double gy) {
    goalx = gx;
    goaly = gy;
  }

  public void step() {
    if (Math.sqrt(Math.pow(goaly - y, 2) + Math.pow(goalx - x, 2)) < delta){
      x = goalx;
      y = goaly;
    } else{
      double a = Math.atan2(goaly - y, goalx - x);
      x += delta * Math.cos(a);
      y += delta * Math.sin(a);
    }
  }

  public void paint(Graphics2D g) {
    g.fill(new Ellipse2D.Double(x - r, y - r, 2 * r, 2 * r));
  }

  public void mouseClicked(MouseEvent e) {
    setGoal(e.getX(), e.getY());
  }

  public void mouseEntered(MouseEvent e)  {}
  public void mouseExited(MouseEvent e)   {}
  public void mousePressed(MouseEvent e)  {}
  public void mouseReleased(MouseEvent e) {}
}