To skip to the explanation, click here.
Here's the Java source code:
import java.awt.*;
import java.applet.Applet;
public class Brach extends Applet implements Runnable{
Thread displayer;
boolean doSuspend = false;
Panel controlPanel;
Button controlButton;
Image offscreenImage;
Graphics offscreenGraphics;
int w, h;
int drag_point = -1; // point that is being dragged, or -1 if none are
int drag_point_last = -1; // last point that was dragged
double x[] = new double[20];
double y[] = new double[20];
double dx, dy;
int n;
double xball, yball, total_time;
double delx,dely,hyp,accel;
double g = 9.8;
double a,b;
double dt = 0.01;
int segi;
double vel;
double m,yIntercept,velball,velx,tball;
public void init() {
setBackground(Color.white);
controlPanel = new Panel();
controlPanel.setLayout(new GridLayout(1,1));
controlButton = new Button("Start");
controlPanel.add(controlButton);
setLayout(new FlowLayout());
add("East",controlPanel);
w = size().width-2; //get dimensions in pixels
h = size().height-2;
n = 2;
x[0] = 0.0; y[0] = 1.0;
x[1] = 1.0; y[1] = 0.0;
xball = 0.0; yball = 1.0;
offscreenImage = createImage(this.size().width,this.size().height);
offscreenGraphics = offscreenImage.getGraphics();
}
public void run() {
while (true) {
segi = -1;
tball = 0; // time kept for ball animation
velball = 0;
while (segi < n-1) {
animate();
repaint();
try {displayer.sleep(100);}
catch (InterruptedException e) {};
if (doSuspend) {
doSuspend = false;
controlButton.setLabel("Start");
System.out.println("Displayer suspended");
displayer.suspend();
}
}
controlButton.setLabel("Start");
System.out.println("Displayer suspended");
displayer.suspend();
}
}
public void stop() {
if (displayer != null) {
displayer.stop();
}
}
public boolean action (Event evt, Object arg) {
if (evt.target instanceof Button) {
System.out.println("Button"+arg+"pressed");
if ("Start".equals(arg)) {
if (displayer == null) {
displayer = new Thread(this);
displayer.start();
}
else displayer.resume();
controlButton.setLabel("Stop ");
compute_time();
}
else if ("Stop ".equals(arg)) {
doSuspend = true;
}
return true;
}
return false;
}
public boolean mouseDown (Event e, int c, int d) {
a = (c + 0.0)/w;
b = 1.0 - (d+0.0)/h;
int i;
// see if any dot is selected so to drag the dot
for (i=1; i<n; ++i) {
dx = a - x[i];
dy = b - y[i];
if (Math.sqrt(dx*dx+dy*dy) < 0.01) //radius around each point is 0.01 in real coordinates
break;
}
if (i < n) { // select the point for dragging
x[i] = a; y[i] = b;
drag_point = i;
System.out.println("Selected old point "+i);
repaint();
return true;
}
// see if any line is selected and then add a dot
for (i=1; i<n; ++i)
if (Math.sqrt((a-x[i])*(a-x[i])+(b-y[i])*(b-y[i]))
+ Math.sqrt((a-x[i-1])*(a-x[i-1])+(b-y[i-1])*(b-y[i-1]))
< Math.sqrt((x[i-1]-x[i])*(x[i-1]-x[i])+(y[i-1]-y[i])*(y[i-1]-y[i])) + 0.025)
break;
if (i < n) { // add a point on that line and select that point for dragging
for (int j=n-1; j>=i; --j) {
x[j+1] = x[j]; y[j+1] = y[j];
}
x[i] = a; y[i] = b;
n++;
drag_point = i;
System.out.println("Selected new point "+i);
repaint();
}
return true;
}
public boolean mouseDrag (Event e, int c, int d) {
if (drag_point >= 0) {
a = (c+0.0)/w;
if (a < 0.0) a = 0.0;
if (a > 1.0) a = 1.0;
b = 1.0 - (d+0.0)/h;
if (b < 0.0) b = 0.0;
if (b > 1.0) b = 1.0;
x[drag_point] = a; y[drag_point] = b;
repaint();
}
return true;
}
public boolean mouseUp (Event e, int c, int d) {
if (drag_point >= 0) {
double a = (c+0.0)/w;
if (a < 0.0) a = 0.0;
if (a > 1.0) a = 1.0;
double b = 1.0 - (d+0.0)/h;
if (b < 0.0) b = 0.0;
if (b > 1.0) b = 1.0;
x[drag_point] = a; y[drag_point] = b;
drag_point_last = drag_point;
drag_point = -1;
repaint();
}
return true;
}
public boolean keyDown(Event evt, int key) {
if (key=='d' || key=='D') {
if (drag_point_last>=0 && drag_point==-1) {
for (int j=drag_point_last; j<n-1; ++j) {
x[j] = x[j+1]; y[j] = y[j+1];
}
n--;
drag_point_last = -1;
repaint();
return true;
}
}
return false;
}
public void compute_time() {
int i;
double t;
vel = total_time = 0.0;
for (i=0; i<n-1; i++) {
delx = x[i+1]-x[i];
dely = y[i+1]-y[i];
hyp = Math.sqrt(delx*delx+dely*dely);
accel = -g*(dely/hyp);
if (accel == 0.0)
t = hyp/vel;
else {
t = (Math.sqrt(vel*vel+2.0*accel*hyp)-vel)/accel;
vel += accel*t;
total_time +=t;
}
}
}
public void animate() {
double tdiff;
if (segi == -1) {
segi = 0;
xball = x[0]; // x-coordinate of ball in pixels
delx = x[1]-x[0];
dely = y[1]-y[0];
m = (dely/delx);
yIntercept = y[0] - m*x[0];
yball = m*xball + yIntercept; // y-coordinate of ball in pixels
hyp = Math.sqrt(delx*delx+dely*dely);
accel = -g*(dely/hyp);
}
if (xball >= w) return;
velball += accel*dt; //velocity of the ball
double velx = velball*(delx/hyp);
tdiff = (x[segi+1] - xball)/velx;
if (tdiff < dt) {
velball += accel*tdiff;
velx = velball*(delx/hyp);
xball += velx*tdiff;
yball = m*xball + yIntercept;
++segi;
delx = x[segi+1]-x[segi];
dely = y[segi+1]-y[segi];
m = (dely/delx);
hyp = Math.sqrt(delx*delx+dely*dely);
accel = -g*(dely/hyp);
yIntercept = y[segi] - m*x[segi];
}
else {
xball += velx*dt; //ball's new x-position
yball = m*xball + yIntercept; //ball's new y-position
tball += dt; //increment ball's time
}
}
public void paint( Graphics g ) {
offscreenGraphics.drawRect(0,w+3,h+3,0); //in pixel coordinates
for (double k=0.0; k<1.0; k+=0.1) {
offscreenGraphics.setColor(Color.white);
offscreenGraphics.drawLine(0,(int)(k*h),w,(int)(k*h));
offscreenGraphics.drawLine((int)(k*w),0,(int)(k*w),h);
}
//draw dots and lines
for (int i=0; i<n; ++i) {
if (i == drag_point)
offscreenGraphics.setColor(Color.red);
else
offscreenGraphics.setColor(Color.blue);
offscreenGraphics.drawOval((int)(x[i]*w),(int)((1.0-y[i])*h),3,3); //convert x[i] and y[i] to pixel coordinates
}
offscreenGraphics.setColor(Color.blue);
for (int i=1; i<n; ++i) {
offscreenGraphics.drawLine((int)(x[i-1]*w),(int)((1.0-y[i-1])*h),(int)(x[i]*w),(int)((1.0-y[i])*h));
}
offscreenGraphics.setColor(Color.red);
offscreenGraphics.fillOval((int)(xball*w)-5,(int)((1.0-yball)*h)-5,10,10);
offscreenGraphics.drawString("total time = "+total_time,250,50);
offscreenGraphics.drawString("final velocity = "+vel,250,60);
offscreenGraphics.drawString("current x = "+a,250,70);
offscreenGraphics.drawString("current y = "+b,250,80);
g.drawImage(offscreenImage,0,0,this);
}
public void update(Graphics g) {
offscreenGraphics.setColor(Color.white);
offscreenGraphics.fillRect(0,0,w,h);
for (double k=0.0; k<1.0; k+=0.1) {
offscreenGraphics.setColor(Color.lightGray);
offscreenGraphics.drawLine(0,(int)(k*h),w,(int)(k*h));
offscreenGraphics.drawLine((int)(k*w),0,(int)(k*w),h);
}
paint(g);
}
}
I give an explanation of each method as it is invoked throughout the lifecycle of the applet. If the reader prefers to skip ahead to read about a specific method, see the table below.
| init() | paint() | update() |
| mouseDown() | mouseUp() | mouseDrag() |
| keyDown() | action() | start() |
| compute_time() | run() | animate() |
| stop() |   |   |
When the applet is first loaded, the init() method is called by the browser or applet viewer. It performs the initialization tasks necessary for the applet to run properly. In this applet, I do the following in init():

Now the browser or applet viewer searches for the paint() method. As I mentioned before, I use double-buffered animation in this applet to reduce flicker. With double-buffering, you create a second surface, do all the painting to the off-screen surface, and then draw the whole surface at once to the actual applet after the image has been drawn. In the paint method I draw the following items, in pixel coordinates, to the off-screen buffer:
Closely associated with the paint method is update(). This method is called by default whenever the applet needs to be repainted. Because of the double-buffered animation, update() paints the background of the applet to offscreenGraphics and calls paint() to finish the updating, which includes repainting the ball's position during animation.

At this point, the applet goes into a waiting mode until the user gives input. When the user clicks the mouse down on the screen in order to drag a point, the method mouseDown() is called. Two variables, c and d, are initialized in the mouseDown() argument to represent the pixel coordinates of the mouse. These pixel coordinates are converted into real coordiantes, a and b, by dividing the mouse's pixel location by the pixel dimensions of the applet.
The first step is to see if where the mouse button was pushed down is close enough to a predefined point to select it. A predefined point is selected if the point where the mouse went down is within a 0.01 radius (in real coordinates) of the predefined point. If a predefined point is selected, then the ith elements in the x and y arrays of points are assigned the values a and b respectively.
If a predefined point is not selected, mouseDown checks to see whether the coordinate of the mouse is within a long narrow ellipse surrounding a line segment whose minor axis is 0.025 in real coordinates. If so, a new point is created on the line segment and is automatically selected for dragging. To add this new point to the x and y arrays of points, a space is created in the array by shifting all the points with a higher array index than the new point up a space, and inserting the newly created point into the space just created. The paint method is called again to update the picture.
If the user doesn't click anywhere near a preexisting point or line segment, nothing happens.

From here, the mouseDrag() method is called. The pixel coordinates of the mouse are passed to this method. The first seven lines of MouseDrag prevent a selected point from being dragged off the screen. Also, the pixel coordinates c and d are again converted into real coordinates by dividing by the pixel dimensions of the applet. The mouse coordinates are assigned to the particular elements x[drag_point] and y[drag_point] within the x and y arrays. Finally, the screen is repainted, allowing the user to see the point being dragged.

When the user raises the mouse button, the method mouseUp() is called. As in mouseDrag, the first seven lines prevent the selected point from being dragged off the screen. The coordinates x[drag_point] and y[drag_point] within the x and y arrays are permanently assigned unless the same point is selected for dragging again. The variable drag_point_last is set equal to drag_point, which in turn is reinitialized to -1 so that the applet can reassign the value of drag_point if another point is selected.

There is a final event handler method for users who wish to delete a selected point. By clicking on the point and then pressing "d" or "D" on keyboard, the keyDown method is called. In keyDown, the x and y coordinates of the point selected for deleting are erased from the x[] and y[] arrays. Then each element in the x[] and y[] arrays having a higher index than the selected point are moved down a position in the array to replace the deleted point. Again, the variable drag_point_last is reinintialized to 0, meaning that no point is currently selected. Finally, the total number of points, n, is decreased by 1 and applet is repainted.

When the user has finished setting up the ball's path, he/she hits the start button to begin the animation and calculate the total time and final velocity. When the start button is pushed, the event is intercepted by the action() method. The only user interface component in this applet is the start button, but the label changes to "Stop" during the animation in case the user whiches to stop the animation. Buttons create actions when they are selected, and a button's argument is the label of the button. When the start button is pushed, it checks the label of the button. If the button's label is start, the method checks to see whether the thread is running. If displayer is null, it spins a new thread, named displayer, and calls the start() method which starts the thread running. If displayer is already running but has been paused, the default method resume() is called to restart the thread.
After action() either calls methods to start or restart a thread, it calls the method compute_time() to calculate the time it takes to follow the designated path.
If the button label is "Stop", a boolean variable called doSuspend is set to false, which in turn pauses the thread in the run() method.

The method compute_time() is called from action() to calculate the total time it takes the ball to follow the designated path. The result is printed to the screen just as the animation begins. In compute_time(), there is a loop over all the line segments in which the x and y components of the line segment are calculated. Using the x and y components, the length of the path (the hyponetuse) is calculated. Then using trigonometry, the acceleration of the ball rolling along the particular line segment is calculated. If there is no acceleration, then the total time is just equal to the length of the segment divided by the velocity. Otherwise, the time it takes to travel along the line segment is calculated and added to the total time it takes the ball to reach the end of all the line segments. The velocity of the ball as it reaches the end of the segment is also calculated, which becomes the starting velocity of the ball on the next line segment.

The action() method also calls start(), which in turn calls run() by default. Assuming the start button has been pushed for the first time, the default method start() is called to start the thread running. In this case we do not need to change the default start() method, so it isn't overidden. The start() method, in turn, calls run(), which calls other methods as long as displayer is running.

The run() method basically controls the thread as long as it is running. All the statements are nested within an infinite loop that continues execution until the thread is stopped. There is a while loop which calls the methods animate() and repaint() until the ball reaches the end of the last segment. The run() method also checks to see if the boolean variable doSuspend is true, and if so it suspends the thread and resets doSuspend to false. When the thread is restarted, the default method resume() is called which itself calls run() again.

The method animate() is called from run() and controls the animation of the ball. The following quantities are calculated in animate():

The stop() method is called when the user leaves the page containing the applet. In this case, we stop the thread when the user leaves the page so that it does not continue to use the computer's resources.