//-------------------------------------------------------------------------- // I M P A C T Version 2.3 // // (C) COPYRIGHT International Business Machines Corp. 1996 // // by John Henckel, john@formulus.com June 1996 // // This is a little java program that simulates the interactions of balls. // You can grab the balls with your mouse and drag them or throw them. You // can control the forces of viscosity and gravity. You can make the walls // be bouncy, or wrap-around. You can turn on trace and make interesting // designs. Have fun with it! // // The public class (Impact) can be executed as either an applet from html // or as a standalone application using the java interpreter. // // If you have any comments about Impact, or if you make any modifications // to the program, please send me a note. // // changes for 2.2 // fixed gravity to be m/r^2 instead of m/r. THANKS Ron Legere! // added a moon to the initial screen. // add fill option // changes for 2.3 // fix the add ball button. THANKS Jim Worley // //--------------------------------------------------------------------------- // Permission to use, copy, modify, distribute and sell this software // and its documentation for any purpose is hereby granted without fee, // provided that the above copyright notice appear in all copies and // that both that copyright notice and this permission notice appear // in supporting documentation. // // This program has not been thoroughly tested under all conditions. IBM, // therefore, cannot guarantee or imply reliability, serviceability, or // function of this program. The Program contained herein is provided // 'AS IS'. THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A // PARTICULAR PURPOSE ARE EXPRESSLY DISCLAIMED. // IBM shall not be liable for any lost revenue, lost profits or other // consequential damages caused by the Program even if advised of the // possibility of such damages. Furthermore, IBM shall not be liable for // any delays, losses or any other damages which may result from // furnishing the Program, even if advised of the possibility of such // damages. import java.awt.*; //------------------------------------------------------------------- // This is the main applet class public class Impact extends java.applet.Applet { MainWindow b; public void init() { add(new Button("Start")); } public boolean action(Event e,Object arg) { if (e.target instanceof Button && ((String)arg).equals("Start")) { b = new MainWindow("Impact Simulator", true); b.setSize(640,480); b.show(); b.start(); } return true; } public void stop() { if (b!=null) b.stop(); } // These methods allow the applet to also run as an application. public static void main(String args[]) { new Impact().begin(); } private void begin() { b = new MainWindow("Impact Simulator", false); b.setSize(640,480); b.show(); b.start(); } } //------------------------------------------------------------------- // This class controls the main window class MainWindow extends Frame { Animator anim; Ticker tick; Thread anim_thread; Thread tick_thread; Dialog db; // dialog box for new ball boolean is_applet; // This method creates layout and main objects. MainWindow(String title, boolean isapp) { super(title); is_applet = isapp; Panel p = new Panel(); // control panel Label n = new Label("Impact 2.3"); p.setLayout(new GridLayout(0,1)); // vertical layout p.setFont(new Font("Helvetica",Font.PLAIN,10)); n.setFont(new Font("TimesRoman",Font.BOLD,16)); p.add(n); p.add(new Button("add ball")); p.add(new Button("show data")); p.add(new Button("hide data")); p.add(new Button("delete ball")); p.add(new Button("quit")); tick = new Ticker(p); // pace maker for the animator anim = new Animator(tick,p); setLayout(new BorderLayout(2,2)); add("Center",anim); add("West",p); TextField t; db = new Dialog(this, "New Ball", false); db.setLayout(new GridLayout(8,2,2,0)); db.add(new Label("Mass")); db.add(t=new TextField(13)); t.setText("64"); db.add(new Label("Radius"));db.add(t=new TextField(13)); t.setText("0"); db.add(new Label("Color")); db.add(t=new TextField(13)); t.setText("any"); db.add(new Label("X Loc")); db.add(t=new TextField(13)); t.setText("100"); db.add(new Label("Y Loc")); db.add(t=new TextField(13)); t.setText("100"); db.add(new Label("X Vel")); db.add(t=new TextField(13)); t.setText("2"); db.add(new Label("Y Vel")); db.add(t=new TextField(13)); t.setText("1"); db.setSize(200,300); db.setResizable(true); } // This starts the threads. public void start() { if (anim_thread==null) { anim_thread = new Thread(anim); anim_thread.start(); // start new thread } if (tick_thread==null) { tick_thread = new Thread(tick); tick_thread.start(); // start new thread } } // This stops the threads. public void stop() { if (anim_thread!=null) { anim_thread.stop(); // kill the thread anim_thread = null; // release object } if (tick_thread!=null) { tick_thread.stop(); // kill the thread tick_thread = null; // release object } } // This handles user input events. public boolean action(Event e, Object arg) { if (e.target instanceof Button) { if (((String)arg).equals("clean")) anim.clearAll = true; else if (((String)arg).equals("delete ball")) anim.delBall(); else if (((String)arg).equals("hide data")) db.hide(); else if (((String)arg).equals("show data")) db.show(); else if (((String)arg).equals("quit")) { stop(); db.hide(); hide(); // I don't know if all this is necessary. removeAll(); dispose(); if (!is_applet) System.exit(0); } else if (((String)arg).equals("add ball")) anim.addBall(new Ball(get_field(db,1), get_field(db,3), get_color(db,5), get_field(db,7), anim.ysize-get_field(db,9), get_field(db,11), -get_field(db,13), anim)); else if (((String)arg).equals("drift")) anim.zeroMomentum(); else if (((String)arg).equals("center")) anim.centerMass(); else if (((String)arg).equals("orbit")) anim.orbit(); else return false; return true; } return false; } // These are little utility methods to get a dialog data private double get_field(Dialog d, int i) { TextComponent t = (TextComponent)d.getComponent(i); if (t instanceof TextComponent) return Double.valueOf(t.getText()).doubleValue(); return 1; } private Color get_color(Dialog d, int i) { TextComponent t = (TextComponent)d.getComponent(i); if (!(t instanceof TextComponent)) return Color.cyan; if (t.getText().equals("red")) return Color.red; if (t.getText().equals("orange")) return Color.orange; if (t.getText().equals("white")) return Color.white; if (t.getText().equals("gray")) return Color.gray; if (t.getText().equals("pink")) return Color.pink; if (t.getText().equals("black")) return Color.black; if (t.getText().equals("yellow")) return Color.yellow; if (t.getText().equals("green")) return Color.green; if (t.getText().equals("blue")) return Color.blue; if (t.getText().equals("magenta")) return Color.magenta; if (t.getText().equals("cyan")) return Color.cyan; return new Color(Color.HSBtoRGB((float)Math.random(),1.0F,1.0F)); } } //------------------------------------------------------------------- // This class performs the animation in the main canvas. class Animator extends Canvas implements Runnable { final int max = 100; int num; // number of balls int cur,mx,my; // current ball and mouse x y Ball[] ball = new Ball[max]; // array of balls Ticker tick; boolean clearAll; // The following are some "physical" properties. Each property // has a value and a control. The values are updated once per // animation loop (this is for efficiency). public double g,mg,f,r; public boolean trc,col,mu,wr,sm,fi; public int xsize,ysize; Scrollbar grav,mgrav,fric,rest; Checkbox trace,collide,mush,wrap,smooth,filled; // The ctor method creates initial objects. Animator(Ticker t, Panel p) { tick = t; setBackground(Color.black); grav = new Scrollbar(Scrollbar.HORIZONTAL,0,1,0,20); p.add(new Label("v-gravity")); p.add(grav); mgrav = new Scrollbar(Scrollbar.HORIZONTAL,10,1,0,20); p.add(new Label("m-gravity")); p.add(mgrav); fric = new Scrollbar(Scrollbar.HORIZONTAL,0,1,0,20); p.add(new Label("viscosity")); p.add(fric); rest = new Scrollbar(Scrollbar.HORIZONTAL,17,1,0,20); p.add(new Label("restitution")); p.add(rest); trace = new Checkbox("trace"); // initially false p.add(trace); collide = new Checkbox("collide"); collide.setState(true); p.add(collide); mush = new Checkbox("mush"); p.add(mush); wrap = new Checkbox("wrap"); p.add(wrap); smooth = new Checkbox("flicker"); p.add(smooth); filled = new Checkbox("filled"); filled.setState(true); p.add(filled); p.add(new Button("orbit")); p.add(new Button("drift")); p.add(new Button("center")); p.add(new Button("clean")); // Add three balls addBall(new Ball(100,0,Color.yellow,300,200,0.45,0,this)); // sun addBall(new Ball(16,0,Color.blue,300,100,-2.8,0,this)); // earth addBall(new Ball(1,0,Color.gray,300,85,-5.9,0,this)); // moon my = -17; // mouse up } // The run method updates the locations of the balls. public void run() { while (true) { readControls(); for (int i=0; i0) { if (cur=num) a=num-1; if (a<0) return; b = nearestBall((int)ball[a].x, (int)ball[a].y, a); if (b==a) return; double d,m,dx,dy,t; d = Ball.hypot(ball[a].x-ball[b].x,ball[a].y-ball[b].y); if (d<1e-20) return; // too close m = ball[a].m + ball[b].m; if (m<1e-50 && m>-1e-50) return; // too small t = Math.sqrt(mg/m/d)/d; dy = t*(ball[a].x - ball[b].x); // perpendicular direction vector dx = t*(ball[b].y - ball[a].y); ball[a].vx = ball[b].vx - dx*ball[b].m; ball[a].vy = ball[b].vy - dy*ball[b].m; ball[b].vx += dx*ball[a].m; ball[b].vy += dy*ball[a].m; } // This adjusts the frame of reference so that the total momentum becomes zero. void zeroMomentum() { double mx=0,my=0,M=0; for (int i=0; i xsize*0.75) x -= xsize; if (y > ysize*0.75) y -= ysize; } cx += ball[i].x * ball[i].m; cy += ball[i].y * ball[i].m; M += ball[i].m; } if (M != 0) for (int i=0; i xsize) ball[i].x -= xsize; while (ball[i].y < 0) ball[i].y += ysize; while (ball[i].y > ysize) ball[i].y -= ysize; } } } //------------------------------------------------------------------- // The Ball class class Ball { double x,y; // location double z; // radius double vx,vy; // velocity Color c; // color double m; // mass boolean hit; // scratch field double ox,oy; // old location (for smooth redraw) final double vmin = 1e-20; // a weak force to prevent overlapping Animator a; boolean iok; // image is ok. Image img; // a bitmap to use in "filled" mode Ball(double mass, double radius, Color color, double px, double py, double sx, double sy, Animator an) { m=mass; z=radius-0.5; c=color; if (z<0.5) z = Math.min(Math.sqrt(Math.abs(m)),Math.min(px,py)); if (z<0.5) z=0.5; x=px; y=py; vx=sx; vy=sy; iok = false; a = an; } // This updates a ball according to the physical universe. // The reason I exempt a ball from gravity during a hit is // to simulate "at rest" equilibrium when the ball is resting // on the floor or on another ball. public void update() { x += vx; if (x+z > a.xsize) if (a.wr) x -= a.xsize; else { if (vx > 0) vx *= a.r; // restitution vx = -Math.abs(vx)-vmin; // reverse velocity hit = true; // Check if location is completely off screen if (x-z > a.xsize) x = a.xsize + z; } if (x-z < 0) if (a.wr) x += a.xsize; else { if (vx < 0) vx *= a.r; vx = Math.abs(vx)+vmin; hit = true; if (x+z < 0) x = -z; } y += vy; if (y+z > a.ysize) if (a.wr) y -= a.ysize; else { if (vy > 0) vy *= a.r; vy = -Math.abs(vy)-vmin; hit = true; if (y-z > a.ysize) y = a.ysize + z; } if (y-z < 0) if (a.wr) y += a.ysize; else { if (vy < 0) vy *= a.r; vy = Math.abs(vy)+vmin; hit = true; if (y+z < 0) y = -z; } if (a.f > 0 && m != 0) { // viscosity double t = 100/(100 + a.f*hypot(vx,vy)*z*z/m); vx *= t; vy *= t; } if (!hit) vy += a.g; // if not hit, exert gravity hit = false; // reset flag } // This computes the interaction of two balls, either collision // or gravitational force. // Returns TRUE if ball b should be deleted. public boolean interact(Ball b) { double p = b.x - x; double q = b.y - y; if (a.wr) { // wrap around, use shortest distance if (p > a.xsize/2) p-=a.xsize; else if (p < -a.xsize/2) p+=a.xsize; if (q > a.ysize/2) q-=a.ysize; else if (q < -a.ysize/2) q+=a.ysize; } double h2 = p*p + q*q; double h = Math.sqrt(h2); if (a.col) { // collisions enabled if (h < z+b.z) { // HIT hit = b.hit = true; if (a.mu) { // mush together if (m < b.m) c=b.c; // color if (b.m+m != 0) { double t = b.m/(b.m+m); x += p*t; y += q*t; vx += (b.vx - vx)*t; vy += (b.vy - vy)*t; if (x > a.xsize) x -= a.xsize; if (x < 0) x += a.xsize; if (y > a.ysize) y -= a.ysize; if (y < 0) y += a.ysize; } m += b.m; z = hypot(b.z,z); iok = false; return true; // delete b } else if (h > 1e-10) { // Compute the elastic collision of two balls. // The math involved here is not for the faint of heart! double v1,v2,r1,r2,s,t,v; p /= h; q /= h; // normalized impact direction v1 = vx*p + vy*q; v2 = b.vx*p + b.vy*q; // impact velocity r1 = vx*q - vy*p; r2 = b.vx*q - b.vy*p; // remainder velocity if (v1 1e-10 && !hit && !b.hit) { // gravity is enabled double dv; dv = a.mg*b.m/h2/h; // for ver 2.2 added '/h' vx += dv*p; vy += dv*q; dv = a.mg*m/h2/h; b.vx -= dv*p; b.vy -= dv*q; } return false; } public void fiximg() { img = a.createImage((int)(2*z+1),(int)(2*z+1)); // int t = a.getColorModel().getTransparentPixel(); Graphics g = img.getGraphics(); g.setColor(Color.black); // transparent I hope! g.fillRect(0,0,(int)(2*z+1),(int)(2*z+1)); float hsb[] = Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(), null); g.setColor(c); for (double i=z; i>0; --i) { if (z > 2) g.setColor(Color.getHSBColor(hsb[0],hsb[1],(float)Math.sqrt(1-i*i/z/z))); g.fillOval((int)((z-i)*0.5),(int)((z-i)*0.7),(int)(2*i+1),(int)(2*i+1)); } iok = true; } public void draw(Graphics g) { if (!a.sm || a.trc) { // if not smooth, always draw if (a.fi) { if (!iok) fiximg(); g.drawImage(img,(int)(x-z),(int)(y-z),(int)(2*z+1),(int)(2*z+1),a); } else { g.setColor(c); g.drawOval((int)(x-z),(int)(y-z),(int)(2*z),(int)(2*z)); } ox = x; oy = y; // save new location } // For smooth mode, only redraw if ball moved a pixel or more. else if (Math.abs(x - ox) > 1 || Math.abs(y - oy) > 1) { if (a.fi) { if (!iok) fiximg(); g.setColor(Color.black); // g.fillOval((int)(ox-z),(int)(oy-z),(int)(2*z+1),(int)(2*z+1)); g.clearRect((int)(ox-z),(int)(oy-z),(int)(2*z+1),(int)(2*z+1)); g.drawImage(img,(int)(x-z),(int)(y-z),(int)(2*z+1),(int)(2*z+1),a); } else { g.setColor(Color.black); g.drawOval((int)(ox-z),(int)(oy-z),(int)(2*z),(int)(2*z)); g.setColor(c); g.drawOval((int)(x-z),(int)(y-z),(int)(2*z),(int)(2*z)); } ox = x; oy = y; // save new location } } static double hypot(double x,double y) { return Math.sqrt(x*x + y*y); } } //------------------------------------------------------------------- // To use the Ticker class, create an object and create a thread // to run it. Thereafter, any other thread can use it as a // pacemaker. class Ticker implements Runnable { int t; // ticks elapsed Checkbox pause; // pause ticker Scrollbar speed; // animation rate Ticker(Panel p) { speed = new Scrollbar(Scrollbar.HORIZONTAL,4,1,0,7); p.add(new Label("speed")); p.add(speed); pause = new Checkbox("pause"); // initially false p.add(pause); } public void run() { while (true) { try { Thread.sleep(dura()); } catch (InterruptedException e) {} t += pause.getState() ? 0 : 1; } } void poll(int eat) { // poll for non-zero tick while (t==0) { try { Thread.sleep(dura()/10 + 1); } catch (InterruptedException e) {} } if (eat > t) t = 0; else t-=eat; } void poll() { poll(30000); } int dura() { return 1<<(10-speed.getValue()); } }