/* BETHA Project Gas Law Tutorial Bouncing Molecules Java Applet Written by Midori Kitagawa DeLeon Advanced Computing Center for the Arts and Design The Ohio State University Last revision: July 16, 1997 Interactions: * Dragging the mouse up/down inside the piston changes the volume of the gas in the piston's gas chamber. * Dragging the mouse up/down inside the thermometer changes the temperature of the gas. * Clicking "+" or "-" button or typing a number into textfield between the buttons changes the number of animated molecules. * Clicking on "Stop" and "Start" buttons starts and ends the animation respectively. */ import java.applet.*; import java.awt.*; import java.util.*; class Moles { public final static double dT = 0.01; // dT < 1.0 slows down all the moles public int max; // maximum number of moles public int num; // current number of moles public double speed; // mole's over all speed public int size; // mole image's size public Image image; // mole's image // (x[], y[]) = array of mole coordinates in the unit square space // -0.5 <= x,y <= 0.5 public double x[]; public double y[]; // (vx[], vy[]) = array of mole velocities public double vx[]; public double vy[]; // Constructor public Moles (int max, int num, double speed) { this.max = max; this.num = num; this.speed = speed; this.x = new double[this.max]; this.y = new double[this.max]; this.vx = new double[this.max]; this.vy = new double[this.max]; // Initializes each mole's position and velocity // // The velocity of a mole is sampled from a Gaussian distribution // with a mean of 0.0 and a standard deviation of 1.0 Random r = new Random(); for (int i = 0; i < this.max; i++) { this.x[i] = Math.random() - 0.5; this.y[i] = Math.random() - 0.5; this.vx[i] = r.nextGaussian(); this.vy[i] = r.nextGaussian(); } } } class HotSpot { // wRatio = width of the hot spot / width of the screen space // hRatio = height of hot spot / width of the screen space public double wRatio, hRatio; // left upper corner position of the hot spot // if the screen size were 1.0 X 1.0 public double xPos, yPos; // hot spot's size (pixels) public int w, h; // left upper corner position of the hot spot (pixels) public int x1, y1; // right lower corner position of the hot spot (pixels) public int x2, y2; // current, max, and min y-coordinates of the movable element */ public int y, yMax, yMin; // mid x-coordinate public int cx; // constructor public HotSpot (double wRatio, double hRatio, double xPos, double yPos, int w, int h) { this.wRatio = wRatio; this.hRatio = hRatio; this.xPos = xPos; this.yPos = yPos; this.w = (int)(this.wRatio * (double)w); this.h = (int)(this.hRatio * (double)h); this.x1 = (int)(this.xPos * (double)w); this.y1 = (int)(this.yPos * (double)h); this.x2 = this.x1 + this.w; this.y2 = this.y1 + this.h; this.cx = (this.x1 + this.x2)/2; } } public class GasLaw extends Applet implements Runnable { // global variables in GasLaw class // // flags // boolean stopAnim = true; // flag to stop the animation boolean updateOnce = true; // flag to indicate that // 1. the applet is just started, // 2. a parameter value is changed, // 3. the cursor is moved in or out of a hotspot, or // 4. Start button is clicked, // and a single (non-animated) frame needs to be updated // without moving moles or sounds of moles. // areas // final static int AREA_OTHER = 0; final static int AREA_PISTON = 1; // piston hot spot final static int AREA_THERM = 2; // thermometer hot spot int areaCurr = AREA_OTHER; // current area int areaPrev = AREA_PISTON; // previous area // mouse // double old_mouse_x; double old_mouse_y; final static int SENSITIV = 10; // adjusts sensitivity of mouse movement // buttons // private Button addButton; private Button delButton; private Button stopButton; private Button startButton; // background and button colors // Color bgColor = Color.lightGray; Color onBack = Color.gray; Color onFore = Color.red; Color offBack = Color.lightGray; Color offFore = Color.white; // text field // private TextField moleNum; // thread // Thread bouncing = null; // frame // Object frame; // moles // Moles m; AudioClip audioClip[] = new AudioClip[5]; // audio clips // piston and the thermometer // HotSpot p, t; final static double MAXTEMP = 3.0; final static double MINTEMP = 0.2; double temp = 0.5 * (MINTEMP + MAXTEMP); // sets the initial temp // Gets parameters and initializes graphics elements // public void init() { // // Reads in parameters // String param; // temporary String to store a parameter // imageName = mole image file name // String imageName = getParameter("imageName"); if (imageName == null) { imageName = "mole.gif"; } // maxMoles = maximum number of moles // param = getParameter("maxMoles"); int maxMoles = (param != null) ? Integer.parseInt(param) : 100; if (maxMoles < 1) { maxMoles = 100; } // numMoles = initial number of moles // param = getParameter("numMoles"); int numMoles = (param != null) ? Integer.parseInt(param) : 10; if ((numMoles < 1) || (numMoles > maxMoles)) { numMoles = 10; } // speed = controls over all mole speeds // param = getParameter("speed"); int k = (param != null) ? Integer.parseInt(param) : 1; double speed = (double)k * m.dT; if ((speed < 0.0) || (speed > 100.0)) { speed = 1.0; } // 5 audio files // String audioName[] = new String[5]; for (int i = 0; i < 5; i++) { audioName[i] = getParameter("audio"+i); audioClip[i] = getAudioClip(getCodeBase(), audioName[i]); } // bgColor = background color // param = getParameter("bgColor"); if (param != null) { bgColor = parseColorString(param); } // // Initializes graphics elements // // moles // m = new Moles(maxMoles, numMoles, speed); m.image = getImage(getCodeBase(), imageName); // loads the image waitForImage(this, m.image); // waits until the image is loaded m.size = m.image.getWidth(this); // gets the image's width // System.out.println("image width =" + m.size); // piston // p = new HotSpot(0.60, 0.80, 0.10, 0.15, this.size().width, this.size().height); p.yMin = p.y1 + 2 * m.size; p.yMax = p.y2 - m.size; p.y = (p.yMax + p.yMin)/2; // thermometer // t = new HotSpot(0.10, 0.80, 0.80, 0.15, this.size().width, this.size().height); t.yMin = t.y1 + t.w/2; t.yMax = t.y2 - t.w; t.y = (t.yMax + t.yMin)/2; // buttons and text field // // With a layout manager running buttons and a text field will not // be moved to specified locations. // // setLayout(null); // stops the layout manager setBackground(bgColor); delButton = new Button("-"); delButton.setForeground(onFore); delButton.setBackground(onBack); delButton.resize(delButton.preferredSize()); addButton = new Button("+"); addButton.setForeground(onFore); addButton.setBackground(onBack); addButton.resize(addButton.preferredSize()); stopButton = new Button("Stop"); stopButton.setForeground(offFore); stopButton.setBackground(offBack); stopButton.resize(stopButton.preferredSize()); startButton = new Button("Start"); startButton.setForeground(onFore); startButton.setBackground(onBack); startButton.resize(startButton.preferredSize()); moleNum = new TextField(""+ m.num +"", 3); moleNum.setForeground(Color.black); moleNum.setBackground(Color.gray); moleNum.resize(moleNum.preferredSize()); moleNum.setEditable(true); // Moves the buttons and text field // delButton.move(this.size().width/2-10, 12); // addButton.move(this.size().width/2+75, 12); // moleNum.move(this.size().width/2+20, 10); add(delButton); add(moleNum); add(addButton); add(startButton); add(stopButton); // Loops up from the current panel with a getParent() until an // instance of a Frame is found. setCursor() is done on this // frame // frame = getParent(); while (! (frame instanceof Frame)) { frame = ((Component) frame).getParent(); } } // Parses a string and returns a color // // This part was copied from // http://javaboutique.internet.com/billsClock/billsClock.java // private Color parseColorString(String colorString) { if(colorString.length() == 6) { int R = Integer.valueOf(colorString.substring(0,2),16).intValue(); int G = Integer.valueOf(colorString.substring(2,4),16).intValue(); int B = Integer.valueOf(colorString.substring(4,6),16).intValue(); return new Color(R,G,B); } else { return Color.lightGray; } } public void run() { while (bouncing != null) { repaint(); // repaint() schedules a call to update() asap try { Thread.sleep(30); } catch (InterruptedException e) {} } bouncing = null; } public void start() { if (bouncing == null) { bouncing = new Thread(this); // Starts the bouncing thread bouncing.start(); } } public void stop() { if (bouncing != null) { bouncing.stop(); // Stops the bouncing thread bouncing = null; } for (int i = 0; i < 5; i++) { if (audioClip[i] != null) { audioClip[i].stop(); // Stops all the audio clips } } } // Overrides update() to call paint() because update() redraws the // background firsts and calls paint() // public void update(Graphics g) { paint(g); } // Painting with double buffering // // The entire canvas is update if updateOnce flag is on; otherwise // the piston's gas chamber only. // public void paint(Graphics g) { if ((stopAnim == false) || (updateOnce == true)) { // Creates an off-screen image and gets the graphics context // for it // Image offImage = createImage(this.size().width, this.size().height); g = offImage.getGraphics(); // Paints into the off-screen image // if (updateOnce == true) { // entire canvas // g.clipRect(p.x1, p.y1, t.x2, t.y2); } else { // piston's gas chamber only // g.clipRect(p.x1, p.y1, p.w, p.y-p.y1); } paintAll(g); // Switches to the real screen // g = this.getGraphics(); // Copies the offscreen image to the screen. // if (updateOnce == true) { // entire canvas // g.clipRect(p.x1, p.y1, t.x2, t.y2); } else { // piston's gas chamber only // g.clipRect(p.x1, p.y1, p.w, p.y-p.y1); } g.drawImage(offImage, 0, 0, this); // Turns off updateOnce flag // if (updateOnce == true) updateOnce = false; } } public void paintAll(Graphics g) { // Paints the background // paintBackground(g); // Paints the movable elements // paintPiston(g); paintThermo(g); paintMoles(g); // Outputs the current number of moles // g.setColor(Color.black); if (m.num == 1) { g.drawString(""+ m.num +" colored molecule", p.x1+10, p.y1+15); } else { g.drawString(""+ m.num +" colored molecules", p.x1+10, p.y1+15); } } public void paintBackground(Graphics g) { // Paints the background // g.setColor(bgColor); g.fillRect(0, 0, this.size().width, this.size().height); } public void paintPiston(Graphics g) { // piston's gas chamber // g.setColor(Color.white); g.fillRect(p.x1, p.y1, p.x2-p.x1, p.y-p.y1); // piston's disc part // if (areaCurr == AREA_PISTON) { g.setColor(Color.green); } else { g.setColor(Color.black); } g.drawLine(p.x1, p.y, p.x2, p.y); // disc g.drawLine((p.x1+p.x2)/2, p.y, (p.x1+p.x2)/2, this.size().height); // handle // piston's cylindrical part // g.setColor(Color.blue); g.drawLine(p.x1, p.y2, p.x1, p.y1); g.drawLine(p.x1, p.y1, p.x2, p.y1); g.drawLine(p.x2, p.y1, p.x2, p.y2); } public void paintThermo(Graphics g) { // thermometer's glass part // g.setColor(Color.white); g.fillRoundRect(t.x1, t.y1, t.w, t.h, m.size, m.size); g.setColor(Color.blue); g.drawRoundRect(t.x1, t.y1, t.w, t.h, m.size, m.size); // thermometer's mercury // g.setColor(Color.white); // white line g.drawLine(t.cx, t.yMin, t.cx, t.y); if (areaCurr == AREA_THERM) { // red line and ball g.setColor(Color.magenta); } else { g.setColor(Color.red); } g.drawLine(t.cx, t.y, t.cx, t.yMax); g.fillArc(t.cx-t.w/4, t.yMax, t.w/2, t.w/2, 0, 360); // shadow for the mercury // g.setColor(Color.darkGray); g.drawLine(t.cx+1, t.yMin, t.cx+1, t.yMax); g.drawArc(t.cx-t.w/4, t.yMax, t.w/2, t.w/2, 270, 180); } public void paintMoles(Graphics g) { // left upper corner of a mole image should stay inside the area // of size (wx x wy) // int wx = p.w - m.size; int wy = (p.y - p.y1) - m.size; // Updates the positions of moles if updateOnce flag is off // if (updateOnce == false) { for (int i = 0; i < m.num; i++) { // Updates x-coordinate // double vx = m.vx[i] * temp; m.x[i] += vx * m.speed; int ix = roundInt(m.x[i]); // rounds off to the nearest integer m.x[i] -= (double)ix; // When the mole hits the left or right wall of the piston, // it changes the direction // if (ix != 0) { m.x[i] = -m.x[i]; m.vx[i] = -m.vx[i]; }; // Updates y-coordinats // // For the mapping from the unit square space // ((-.5, -.5)-(.5, .5)) to the rectangular area // ((p.x1, p.y1)-(p.x2-m.size, py2-m.size)), vy needs to be // scaled by p.w / (p.y - p.y1). Without this scaling // the pressure (represented by the frequency of sounds that // moles make) would not change when the volume in the piston // changes. // double adjust = (double)p.w / (p.y - p.y1); double vy = m.vy[i] * temp * adjust; m.y[i] += vy * m.speed; int iy = roundInt(m.y[i]); // rounds off to the nearest intger m.y[i] -= (double)iy; // When the mole hits the top or bottom wall of the piston, // it changes the direction // if (iy != 0) { m.y[i] = -m.y[i]; m.vy[i] = -m.vy[i]; // When a mole hits the piston's bottom wall (disc), makes // a sound // if (iy == 1) { soundEffect(vx, vy); } } } } // Draws moles with mapping from the unit square space to the // rectangular area of size (wx x wy) // for (int i = 0; i < m.num; i++) { g.drawImage(m.image, (int)((m.x[i] + 0.5) * (double)wx) + p.x1, (int)((m.y[i] + 0.5) * (double)wy) + p.y1, this); } } // Does what rint () on SUNs, SGIs, and Macs (but not the one on // PCs) does // public int roundInt(double d) { return ((d > 0) ? (int)(d + 0.5) : -(int)(-d + 0.5)); } // Plays sound effects for moles // // velocity (vx, vy) decides which sound to be played // public void soundEffect(double vx, double vy) { int which = (int)(vx*vx + vy*vy); which /= 200; if (which > 4) which = 4; audioClip[which].play(); // getAppletContext().showStatus("vx = "+ vx +": vy = "+ vy +" \n"); // getAppletContext().showStatus("vx*vx+vy*vy = "+ vx*vx+vy*vy +" \n"); // getAppletContext().showStatus("which = "+ which +" \n"); } // When the cursor is in a hot spot, it turns into the "move" // cursor: otherwise, stays the default cursor // public boolean mouseMove(java.awt.Event evt, int mouse_x, int mouse_y) { // Cursor is in the piston hot spot // if ((p.x1 < mouse_x) && (mouse_x < p.x2) && (p.y1 < mouse_y) && (mouse_y < p.y2)) { areaPrev = areaCurr; areaCurr = AREA_PISTON; ((Frame) frame).setCursor(Frame.MOVE_CURSOR); // Cursor just entered this hop spot. Needs to update the // entire graphics. // if (areaPrev == AREA_OTHER) { updateOnce = true; } // getAppletContext().showStatus("Change volume by dragging here\n"); // Cursor is in the thermometer hot spot // } else if ((t.x1 < mouse_x) && (mouse_x < t.x2) && (t.y1 < mouse_y) && (mouse_y < t.y2)) { areaPrev = areaCurr; areaCurr = AREA_THERM; ((Frame) frame).setCursor(Frame.MOVE_CURSOR); // Cursor just entered this hop spot. Needs to update the // entire graphics. // if (areaPrev == AREA_OTHER) { updateOnce = true; } // getAppletContext().showStatus("Change temperature by dragging here\n"); // Cursor is not a any hot spot // } else { areaPrev = areaCurr; areaCurr = AREA_OTHER; ((Frame) frame).setCursor(Frame.DEFAULT_CURSOR); // Cursor just left a hop spot. Needs to update the entire // graphics. // if (areaPrev != AREA_OTHER) { updateOnce = true; } // getAppletContext().showStatus("\n"); } return true; } // When the mouse is down, saves the current coordinates // public boolean mouseDown(java.awt.Event evt, int mouse_x, int mouse_y) { old_mouse_x = mouse_x; old_mouse_y = mouse_y; return true; } // When the mouse is dragged into a hot spot, updates the movable // element // public boolean mouseDrag(java.awt.Event evt, int mouse_x, int mouse_y) { if (mouse_y != old_mouse_y) { // // piston area // if ((p.x1 < mouse_x) && (mouse_x < p.x2) && (p.y1 < mouse_y) && (mouse_y < p.y2)) { // moves the piston's disc // p.y += (mouse_y - old_mouse_y) / SENSITIV; if (p.y < p.yMin) { p.y = p.yMin; } else if (p.yMax < p.y) { p.y = p.yMax; } // Updates the image updateOnce = true; // // thermometer area // } else if ((t.x1 < mouse_x) && (mouse_x < t.x2) && (t.y1 < mouse_y) && (mouse_y < t.y2)) { // changes the temperature // t.y += (mouse_y - old_mouse_y) / SENSITIV; if (t.y < t.yMin) { t.y = t.yMin; } else if (p.yMax < t.y) { t.y = t.yMax; } temp = MAXTEMP-(MAXTEMP-MINTEMP)*(double)(t.y-t.yMin)/(t.yMax-t.yMin) ; // Updates the image updateOnce = true; } } return true; } // When the add/delete button is pressed, a mole is added/deleted // When a number is typed into the text field, the number of // bouncing moles is changed // public boolean action(Event event, Object arg) { if (event.target instanceof Button) { if(event.target == addButton) { m.num++; updateOnce = true; if (m.num == 2) { // recovers the delete button delButton.setForeground(onFore); delButton.setBackground(onBack); } else if (m.num == m.max) { // gray-outs the add button addButton.setForeground(offFore); addButton.setBackground(offBack); } else if (m.num > m.max) { // no more moles can be added m.num--; } moleNum.setText(""+ m.num +"");// writes out the new number return true; } else if (event.target == delButton) { m.num--; updateOnce = true; if (m.num == m.max-1) { // recovers the add button addButton.setForeground(onFore); addButton.setBackground(onBack); } else if (m.num == 1) { // gray-outs the delete button delButton.setForeground(offFore); delButton.setBackground(offBack); } else if (m.num < 1) { // no more moles can be deleted m.num++; } moleNum.setText(""+ m.num +"");// writes out the new number return true; } else if (event.target == stopButton) { stopButton.setForeground(offFore); stopButton.setBackground(offBack); startButton.setForeground(onFore); startButton.setBackground(onBack); stopAnim = true; return true; } else if (event.target == startButton) { stopButton.setForeground(onFore); stopButton.setBackground(onBack); startButton.setForeground(offFore); startButton.setBackground(offBack); updateOnce = true; stopAnim = false; return true; } else { return super.action(event, arg); } } else if (event.target instanceof TextField) { int newNum = -1; if (event.target == moleNum) { if (moleNum.getText() != null) { newNum = Integer.parseInt(moleNum.getText()); if ((1 <= newNum) && (newNum <= m.max)) { m.num = newNum; moleNum.setText(""+ m.num +""); // writes the new number if (m.num == 1) { // gray-outs the delete button // and recovers the add button delButton.setForeground(offFore); delButton.setBackground(offBack); addButton.setForeground(onFore); addButton.setBackground(onBack); } else if (m.num == m.max) { // gray-outs the add button // and recovers the delete button addButton.setForeground(offFore); addButton.setBackground(offBack); delButton.setForeground(onFore); delButton.setBackground(onBack); } else { // recovers both buttons addButton.setForeground(onFore); addButton.setBackground(onBack); delButton.setForeground(onFore); delButton.setBackground(onBack); } updateOnce = true; } // end if the new number is in range } // end if text is not null return true; } else { return super.action(event, arg); } } else { return super.action(event, arg); } } // Wait until the image is fully loaded // // From "Graphics Java: Mastering the AWT" page 135 // public static void waitForImage (Component component, Image image) { MediaTracker track = new MediaTracker(component); try { track.addImage(image, 0); track.waitForID(0); } catch (InterruptedException e) {} } }