CSCI A202 - Introduction to Programming (II)

Lecture 8: Getting Mouse Input in Graphical Applications. Basic animations

The last function we defined last time describes what we do when the mouse is pressed. We know that it is invoked by the run time system when the mouse button is pressed and at invocation time it receives the x and y coordinates of the mouse pointer in its two parameters.

public void mousePressed(int x, int y) {
  for (int i = 0; i < radii.length; i++) 
    if (circles[i].inside(x, y)) {
      selectedCircle = circles[i]; 
      break; 
    } 
}
This function tries to find a circle that has the mouse pointer inside. To find the circle we loop through the array of references and ask each circle if the two coordinates are inside it.

So we need to define an inside() method:

boolean inside(int xMouse, int yMouse) {
  if (Math.sqrt((x + radius - xMouse) * (x + radius - xMouse) +
                (y + radius - yMouse) * (y + radius - yMouse)) 
      <= radius)
    return true;
  else 
    return false; 
}
This will be an instance method part of the Circle class, and it will return either true or false depending on whether the mouse is inside or outside of the circle.

We also need to define and use a Circle variable

Circle selectedCircle;
in which we will keep a reference to the first circle that replies with true when asked if the mouse pointer is inside it. Note that this should be null if no Circle is currently selected.

Notice that if more than one circle qualifies we select the one that has the smallest index in the array of circles that we maintain.

We also need to point out that we loop for radii.length number of times, and we do that relying on the fact that the arrays of radii, xCoords, yCoords and colors are parallel arrays with the same length.

For this example the paint() method of the application (Step3) will be the same as the one for the application we developed at the previous stage:

public void paint(Graphics g) {
  for (int i = 0; i < radii.length; i++) 
    circles[i].draw(g);   
}

For the drawing routine we will use a different approach though, in that we will make use of the XOR painting mode. In this mode the circle may appear in a slightly different color but when drawn twice the background is restored, so it's a very cheap (fast) way of simulating the movement of an object against a certain background.

void draw(Graphics g) {
  g.setColor(color); 
  g.setXORMode(Color.black); 
  g.fillOval(x, y, 2 * radius, 2 * radius);     
}
We will have more to say about this later.

Now we need to define the other two methods that handle mouse input.

If we drag the mouse and a circle is selected we need to have it follow the mouse pointer.

public void mouseDragged(int x, int y) {
  if (selectedCircle != null) 
    selectedCircle.moveTo(getGraphics(), x, y); 
}
So we need to define a moveTo method as an instance method in the Circle class. I think the lab notes call this method jumpTo but a little change of notation never hurt anyone.
void moveTo(Graphics g, int newX, int newY) {
  draw(g); 
  x = newX - radius; 
  y = newY - radius; 
  draw(g); 
}
You notice that we first erase the circle by redrawing it at the same exact location and then drawing it fresh at the current location of the mouse pointer. (We remember that drawing in XOR mode twice in a row has the effect of restoring the background to what it was before we drew there for the first time).

You should also note that we need to subtract the radius from the current mouse pointer's position because we want the circle to move along with the mouse as if we would be holding it by its center, and the x and y that we store as instance variables are in fact the coordinates of the lower left corner of the circle's bounding rectangle (top left corner because the coordinates on the screen are reversed along the y axis).

When the mouse is released, and if there is a circle being moved at the time (that is, if the selectedCircle variable is not null) then we need to place the circle at that location and forget about carrying the circle around any more.

public void mouseReleased(int x, int y) {
  if (selectedCircle != null) { 
    selectedCircle.moveTo(getGraphics(), x, y); 
    selectedCircle = null; 
  } 
}
And we're done. Here's the framework of Step3:
import java.awt.*;
import BreezyGUI.*;

public class Step3 extends GBFrame {

  Color[] colors = ... ; 

  int[] xCoords = ... ; 
  int[] yCoords = ... ; 

  int[] radii   = ... ; 
  
  public void paint(Graphics g) {
    for (int i = 0; i < radii.length; i++) 
      circles[i].draw(g);   
  }

  int numberOfCircles = 6; 

  Circle[] circles = new Circle[numberOfCircles]; 

  Step3() {
    for (int i = 0; i < numberOfCircles; i++) {
      circles[i] = new Circle(...); // same as in Step2
    } 
  } 

  Circle selectedCircle; 

  public void mousePressed(int x, int y) {
    ... 
  } 

  public void mouseDragged(int x, int y) {
    ... 
  } 

  public void mouseReleased(int x, int y) {
    ... 
  } 

  public static void main(String[] args) {
    Frame f = new Step3(); 
    f.setSize(500, 300); 
    f.setVisible(true); 
  }
}

class Circle {
  private int x, y, radius; 
  private Color color;
  Circle( int xInitially, int yInitially, 
          int radius, Color color) { 
    x      = xInitially; 
    y      = yInitially; 
    this.radius = radius; 
    this.color  = color; 
  } 
Since the coordinates change but the radius and the color don't (at least in our examples here) we decide to make this distinction in the names of the parameters passed to the constructor. So we need to make use of the this variable to initialize the last two.
  void draw(Graphics g) {
    ... 
  } 
  void moveTo(Graphics g, int newX, int newY) {
    ... 
  } 
  boolean inside(int xMouse, int yMouse) {
    ... 
  } 
}

Threads A thread is a single sequential flow of control within a process.

A single process can have multiple concurrently executing threads. Java provides a Thread class for threads just as it provides a String class for strings, etc., in the standard Java class libraries.

Here's a short example:

tucotuco.cs.indiana.edu% vi ThreadsTest.java
tucotuco.cs.indiana.edu% cat ThreadsTest.java
public class ThreadsTest {
  public static void main(String[] args) {
    PingPong a = new PingPong("Ping", 300); 
    PingPong b = new PingPong("Pong", 700); 
    b.start(); 
    a.start(); 
  } 
} 
 
class PingPong extends Thread {
  private String word;
  private int delay; 
  PingPong (String w, int d) { 
    word = w; 
    delay = d; 
  } 
  public void run() {
    while (true) { 
      try { 
        System.out.println(word + "..."); 
        sleep(delay); 
      } catch (InterruptedException e) {
        return;  
      }
    }  
  } 
} 
tucotuco.cs.indiana.edu% javac ThreadsTest.java
tucotuco.cs.indiana.edu% java ThreadsTest
Pong...
Ping...
Ping...
Ping...
Pong...
Ping...
Ping...
Pong...
Ping...
Ping...
Pong...
Ping...
Ping...
^Ctucotuco.cs.indiana.edu% 
Enough about threads, let's get back to Step 4.

We need to write a function that describes a simulation step:

public void run() {
  Graphics g = getGraphics();     
  for (int i = 0; i < numberOfCircles; i++) 
    circles[i].advance(g); 
  repaint(); 
}
This is one simulation step only. Its name is probably unfortunate. We should have called it simulationStep() or something of that kind, to avoid any confusion with any terminology for threads.

We'll call this repeatedly, every 300 milliseconds, from a driver thread.

class Driver extends Thread {
  Step4 myFrame; 
  Driver (Step4 passedFrame) {
    myFrame = passedFrame; 
  } 
  public void run() {
    while (true) {
      try {
        sleep(300); 
        myFrame.run();         
      } catch (InterruptedException e) { } 
    } 
  } 
}
Step 4's main will initialize a Driver and start() it.
public static void main(String[] args) {
  Step4 f = new Step4(); 
  f.setSize(500, 300); 
  f.setVisible(true); 
  Driver d = new Driver(f);  
  d.start(); 
}
We need to devine advance() for the simulation.

Each circle should know how to advance() so it's an instance method.

void advance(Graphics g) {
  int newX, newY; 
 
  newX = currentX + deltaX; 

  if (newX < Step4.LOW_X) { 
    newX = Step4.LOW_X + (Step4.LOW_X - newX);
    deltaX = -deltaX; } 

  if (newX + radius * 2 > Step4.HIGH_X) { 
    newX = Step4.HIGH_X - 2 * radius - 
           (newX + radius * 2 - Step4.HIGH_X); 
    deltaX = -deltaX; 
  } 

  newY = currentY + deltaY; 
 
  if (newY < Step4.LOW_Y) { 
    newY = Step4.LOW_Y + (Step4.LOW_Y - newY);
    deltaY = -deltaY; 
  }

  if (newY + radius * 2 > Step4.HIGH_Y) { 
    newY = Step4.HIGH_Y - 2 * radius - 
           (newY + radius * 2 - Step4.HIGH_Y); 
    deltaY = -deltaY; 
  }  

  moveTo(g, newX, newY); 
}
The part in blue controls the bouncing and will be explained in class.

We need to define the constants in Step4 for the bounding rectangle of our simulation.

public final static int LOW_X = 30, 
                        LOW_Y = 30, 
                        HIGH_X = 400,  
                        HIGH_Y = 300;
The paint() changes only slightly:
public void paint(Graphics g) {
  g.setColor(Color.black); 
  g.drawRect(LOW_X - 1,        
             LOW_Y - 1, 
             HIGH_X+1 - LOW_X, 
             HIGH_Y+1-LOW_Y); 
  for (int i = 0; i < numberOfCircles; i++) 
    circles[i].draw(g); 
}
The only thing that's left is to describe how the Circle class is representing movement.

In the snippet below we use currentX, and currentY for the location variables and deltaX and deltaY for the direction of movement.

private int currentX, currentY, radius; 
private int deltaX, deltaY; 
There are eight directions of movement and they correspond to adding 1 or subtracting 1 from the current values of the coordinates. The 9th case is the one when the circle doesn't move, so the coordinates are changed with 0 each one of them (they, in fact, remain unchanged).

Here's the initialization part:

Circle( int xInitially, int yInitially, 
        int radiusInitially, Color colorInitially) {
  currentX      = xInitially; 
  currentY      = yInitially; 
  radius        = radiusInitially; 
  color         = colorInitially; 

  deltaX = (int)(Math.random() * 3) - 1; // { -1, 0, 1} 
  deltaY = (int)(Math.random() * 3) - 1; // { -1, 0, 1}

  if (deltaX == 0 && deltaY == 0) { // everybody should be moving 
    switch (((int)Math.random() * 8 + 1)) {
      case  1: deltaX = -1; deltaY = -1; break;
      case  2: deltaX = -1; deltaY =  0; break;
      case  3: deltaX = -1; deltaY =  1; break;
      case  4: deltaX =  1; deltaY = -1; break;
      case  5: deltaX =  1; deltaY =  0; break;
      case  6: deltaX =  1; deltaY =  1; break;
      case  7: deltaX =  0; deltaY = -1; break;
      case  8: deltaX =  0; deltaY =  1; break;
      default: break; 
    }
  } 
}
Since we no longer use XOR mode of painting
void draw(Graphics g) {
  g.setColor(color); 
  g.fillOval(currentX, currentY, 2 * radius, 2 * radius); 
}
the image will have a bit of flickering. We can fix this in several ways (for example doing double buffering, but that's outside of the scope of what we want to do now) but at least the moveTo method is shorter

void moveTo(Graphics g, int newX, int newY) {
  currentX = newX; 
  currentY = newY; 
}
since all drawing happens in paint().

You now have everything you need to put together Step 3 and Step 4 to finish your assignment.