/*----------------------------------------------------------------------+ | Title: Tumble.java | | Java beta applet | | | | Author: David E. Joyce | | Department of Mathematics and Computer Science | | Clark University | | Worcester, MA 01610-1477 | | U.S.A. | | | | http://aleph0.clarku.edu/~djoyce/home.html | | djoyce@clarku.edu | | | | Date: December, 1995. Modifications in January, 1996. | +----------------------------------------------------------------------*/ import java.awt.*; import java.applet.*; import java.util.StringTokenizer; /*----------------------------------------------------------------------+ | Collider class. Nearly all the physics is in Collider. It | | determines when the collisions occur and what happens in a | | collision. The circular wall has radius 1 as far as the | | collider is concerned. | +----------------------------------------------------------------------*/ class Collider extends Thread { Tumble tble; int nextColl_i, nextColl_j; double wallColl[]; // collision time tables double ballColl[][]; double timeToCollide=Double.POSITIVE_INFINITY; double qg2; Collider (Tumble tble) { int i,j; this.tble = tble; qg2 = 0.25*tble.grav*tble.grav; // initialize the collision tables // determine collision times with walls wallColl = new double[tble.nBalls]; for (i=0; i1e-10d) ? x2 : x1; } double solvequart (double a, double b, double c, double d, double e) { // find smallest positive root of ax^4+bx^3+cx^2+dx+e double err = 1e-6d; double num, den; int i; // first find a root using Newton's method double x1 = 10.0; double diff = 1.0; for (i=0; (diff>err || diff<-err) && i<24; ++i) { num = ((((a*x1+b)*x1)+c)*x1+d)*x1+e; den = ((4.0*a*x1+3.0*b)*x1+2.0*c)*x1+d; diff = num/den; if (diff<-1e10d || diff>1e10d) x1 = 10.0*Math.random(); else x1 -= diff; } b += a*x1; c += b*x1; d += c*x1; // now find a root of the cubic ax^3+bx^2+cx+d using Newton's method double x2 = -10.0; diff = 1.0; for (i=0; (diff>err || diff<-err) && i<20; ++i) { num = (((a*x2+b)*x2)+c)*x2+d; den = (3.0*a*x2+2.0*b)*x2+c; if (-0.000011e-10d && x11e-10d && x2= 2.1) { timeToCollide = 2.0; nextColl_i = -1; nextColl_j = -1; } } double findWallColl (int i) { // when does ball i hit the wall? double when; double dr = 1.0 - tble.r[i]; double dist2 = tble.x[i]*tble.x[i] + tble.y[i]*tble.y[i]; double inprod = tble.x[i]*tble.xp[i] + tble.y[i]*tble.yp[i]; if (dist2 > dr*dr && inprod > 0.0) return 0.0; double speed2 = tble.xp[i]*tble.xp[i] + tble.yp[i]*tble.yp[i]; if (qg2 == 0.0) { if (Math.abs(speed2) < 1e-8d) // motionless return Double.POSITIVE_INFINITY; when = solvequad ( speed2, 2.0*inprod, dist2 - dr*dr ); } else when = solvequart ( qg2, -tble.grav*tble.yp[i], speed2 - tble.grav*tble.y[i], 2.0*inprod, dist2 - dr*dr ); if (when == Double.POSITIVE_INFINITY) tble.getAppletContext().showStatus("ball "+i+" never returning. x="+ tble.x[i]+", y="+tble.y[i]+", x'="+tble.xp[i]+", y'="+tble.yp[i]); return when; } double findBallColl (int i, int j) { // when might ball i hit ball j? double norm2 = tble.r[i]+tble.r[j]; norm2 *= norm2; double dx = tble.x[i] - tble.x[j]; double dy = tble.y[i] - tble.y[j]; double dist2 = dx*dx + dy*dy; double d2x = tble.xp[i] - tble.xp[j]; double d2y = tble.yp[i] - tble.yp[j]; if (norm2 > dist2) return Double.POSITIVE_INFINITY; return solvequad ( d2x*d2x + d2y*d2y, 2.0*(dx*d2x + dy*d2y), dist2 - norm2 ); } void updateWallColl() { for (int i=0; i 1.0) { // somehow the ball got way outside the unit circle tble.x[i] = 0.0; tble.y[i] = 0.0; tble.xp[i] = 0.0; tble.yp[i] = 0.0; } else { double inprod = tble.x[i]*tble.xp[i] + tble.y[i]*tble.yp[i]; if (inprod > 0) { // ball headed outward double factor = 2.0*inprod / dist2; tble.xp[i] -= factor*tble.x[i]; tble.yp[i] -= factor*tble.y[i]; if (tble.sound) tble.play(tble.getDocumentBase(), "sounds/ip.au"); } } } else { // two balls collide double dx = tble.x[i] - tble.x[j]; double dy = tble.y[i] - tble.y[j]; double norm2 = dx*dx +dy*dy; double xgp = (tble.m[i]*tble.xp[i]+tble.m[j]*tble.xp[j])/(tble.m[i]+tble.m[j]); double ygp = (tble.m[i]*tble.yp[i]+tble.m[j]*tble.yp[j])/(tble.m[i]+tble.m[j]); double factor_i = 2.0*(dx*(tble.xp[i]-xgp)+dy*(tble.yp[i]-ygp))/norm2; double factor_j = 2.0*(dx*(tble.xp[j]-xgp)+dy*(tble.yp[j]-ygp))/norm2; tble.xp[i] -= factor_i*dx; tble.yp[i] -= factor_i*dy; tble.xp[j] -= factor_j*dx; tble.yp[j] -= factor_j*dy; if (tble.sound) tble.play(tble.getDocumentBase(), "sounds/thin.bell.au"); } } public void run () { tble.lastCollisionTime = System.currentTimeMillis(); while (true) { if (timeToCollide > 0.0) { tble.nextCollisionTime = tble.lastCollisionTime + (long)(1000.0*timeToCollide); int sleepPeriod = (int) (tble.nextCollisionTime - System.currentTimeMillis()); try {Thread.sleep(sleepPeriod);} catch (InterruptedException e) {} } if (tble.movedBall) { tble.movedBall = false; if (tble.pick != -2) { updateBallColl(tble.pick); updateWallColl(); tble.pick = -2; } } resetTimes(); if (tble.pick != nextColl_i && tble.pick != nextColl_j) { bounceBall(); updateWallColl(); if (nextColl_i != -1) updateBallColl(nextColl_i); if (nextColl_j != -1) updateBallColl(nextColl_j); } nextColl(); } } } /*----------------------------------------------------------------------+ | Tumble class. There are two threads that run. One is the | | painter, an ordinary thread which just paints the position of | | balls at regular intervals. The other is the collider. | +----------------------------------------------------------------------*/ public class Tumble extends Applet implements Runnable { Thread painter; int paintFrequency = 100; // milliseconds Collider cldr; boolean suspended=false; int nBalls = 4; double grav = 0.0; boolean sound = true; int radius, gMidx, gMidy; Color circleColor; int grid; Color c[] = new Color[nBalls]; // color of balls double r[] = new double[nBalls]; // radius of ball double m[] = new double[nBalls]; // mass of ball double x[] = new double[nBalls]; double y[] = new double[nBalls]; // location of ball double xp[] = new double[nBalls]; double yp[] = new double[nBalls]; // velocity of ball long lastCollisionTime,nextCollisionTime,stopTime,lastDragTime; int pick = -2; boolean movedBall = false; double lastDrag_x, lastDrag_y; Image offscreen; Dimension offscreensize; Graphics offgraphics; public String getAppletInfo() { return "Tumble. Copyright 1995, David Joyce, Clark University. Version 0.1f"; } public String[][] getParameterInfo() { String[][] pinfo = { {"balls", "int", "number of balls"}, {"gravity", "int", "gravitational constant * 1000"}, {"sound", "boolean", "noisy applet?"}, {"balldata", "ball array", "data for balls"}, {"color", "color", "circle color"}, {"grid", "int", "size of grid, if any"} }; return pinfo; } Color randomColor() { int c; float hue = (float)(Math.random()*1.0); if (Math.random()<0.2) c = Color.HSBtoRGB(hue, (float)(1.0-Math.random()*Math.random()), // saturation (float)(1.0-Math.random()*Math.random()) // brightness ); else if (Math.random()<0.4) c = Color.HSBtoRGB(hue,1.0f,(float)(0.3+0.7*Math.random())); else c = Color.HSBtoRGB(hue,(float)(Math.random()),1.0f); return new Color(c); } Color parseColor (String str) { StringTokenizer t = new StringTokenizer(str,":"); float hue = (float)(Integer.parseInt(t.nextToken())/360.0); float sat = (float)(Integer.parseInt(t.nextToken())/100.0); float bri = (float)(Integer.parseInt(t.nextToken())/100.0); return new Color(Color.HSBtoRGB(hue,sat,bri)); } void parseBall (String str, int i) { StringTokenizer u = new StringTokenizer(str,","); r[i] = Integer.parseInt(u.nextToken())*0.001; m[i] = r[i]*r[i]*r[i]; x[i] = Integer.parseInt(u.nextToken())*0.001; y[i] = Integer.parseInt(u.nextToken())*0.001; xp[i] = Integer.parseInt(u.nextToken())*0.001; yp[i] = Integer.parseInt(u.nextToken())*0.001; c[i] = randomColor(); c[i] = (u.hasMoreTokens()) ? parseColor(u.nextToken()) : randomColor(); } void randomBall(int i) { double a,b; boolean bad; c[i] = randomColor(); do { r[i] = 0.050+Math.random()/(2.0+i); a = (Math.random()-0.5)*2.0; b = (Math.random()-0.5)*2.0; bad = (a*a+b*b >= (1.0-r[i])*(1.0-r[i])); for (int j=0; !bad && (j0) { int rdg = radius/grid; for (int i=-rdg; i<=rdg; ++i) { int a = i*grid; int b = (int)Math.sqrt(radius*radius-a*a); offgraphics.setColor((i==0)?Color.black:Color.darkGray); offgraphics.drawLine(-a+gMidx,-b+gMidy, -a+gMidx,b+gMidy); offgraphics.drawLine(a+gMidx,-b+gMidy, a+gMidx,b+gMidy); offgraphics.drawLine(-b+gMidx,a+gMidy, b+gMidx,a+gMidy); offgraphics.drawLine(-b+gMidx,-a+gMidy, b+gMidx,-a+gMidy); } } for (int i=0; i 1.0-r[i]) { a = a*(1.0-r[i])/norm; b = b*(1.0-r[i])/norm; } x[i] = a; y[i] = b; if (motion==1) { // stop the ball xp[i] = 0.0; yp[i] = 0.0; } else { // toss or release the ball double dragtime = (System.currentTimeMillis()-lastDragTime) * 0.001; xp[i] = (xp[i] + (a-lastDrag_x)/dragtime) * 0.5; yp[i] = (yp[i] + (b-lastDrag_y)/dragtime) * 0.5; } lastDragTime = System.currentTimeMillis(); lastDrag_x = a; lastDrag_y = b; } void makePick (double c, double d) { lastDrag_x = c; lastDrag_y = d; double bestdist2 = Double.POSITIVE_INFINITY; double t = (System.currentTimeMillis()-lastCollisionTime)/1000.0; for (int i=0; i