The Brachistichrone Applet

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);
}
}

Explanation

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:

The final line in the paint method prints the offscreen buffer to the screen.

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():


After calculating these quantites, the quantity tdiff is calculated, which represents the amount of time it will take the ball to reach the end of the segment. If the time (tdiff) is less than than the defined time step (dt=0.01), the position and velocity of the ball are updated using this smaller time step. This prevents the ball from overshooting the line segment. If tdiff is greater than dt, the ball's position gets updated using dt. The x position of the ball gets updated by multiplying the x-component of the velocity by the time step, and the y position gets updated by using the slope-intercept formula.

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.



Please send comments and suggestions to jdecarolis@clarku.edu
Last updated 8 December 1997