//--------------------------------------------------------------------------
// 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()); }
}
               (
geocities.com/paris)