// COPYRIGHT 2001 KEN PERLIN
import java.applet.*;
import java.awt.*;
import java.awt.image.*;

public class Bucky extends PixApplet {

// CALLED AT THE START OF EACH FRAME

   public void initFrame(int frame) {

   // IF FIRST TIME THROUGH

      if (S == null) {

         // GENERATE THE SPHERES

         defineSpheres();

         // COMPUTE THE SPHERE DISK SILHOUETTES

         R = W/31;
         S  = new int[2][colors.length][2*R+1][2*R+1];
         d  = new int[nd][2*R+1][2*R+1];
         dd = new int[nd][2*R+1][2];
         int c, rgb[] = new int[3];
         double RGB[] = new double[3];
         for (int x = -R ; x <= R ; x++)
         for (int y = -R ; y <= R ; y++) {

            // DIFF DISK FOR IN FOCUS (FRONT) AND OUT OF FOCUS (BACK) SPHERES

            for (int u = 0 ; u < nd ; u++) {
               double fuzz = (1.5*u + 2) * R;
               d[u][R+y][R+x] = f2i(disk(x*x + y*y, R, fuzz));
            }

            // SHADE AS FROSTED (k=0) OR POLISHED (k=1)

            if (d[0][R+y][R+x] > 0)
               for (int i = 0 ; i < colors.length ; i++) {
                  int k = (i!=0&&i!=4 ? 1 : 0);

                  // SPHERES IN FRONT ARE SHADED BRIGHTER

                  for (int j = 0 ; j < 3 ; j++)
                     RGB[j] = colors[i][j] * 1.1;
                  S[0][i][R+y][R+x] = shade((double)x/R,(double)y/R,RGB,k);

                  // SPHERES IN BACK ARE SHADED DARKER

                  for (int j = 0 ; j < 3 ; j++)
                     RGB[j] = colors[i][j] * 0.7;
                  S[1][i][R+y][R+x] = shade((double)x/R,(double)y/R,RGB,k);
               }
         }

         // SPEEDUP: COMPUTE EACH DISK'S FIRST/LAST PIXEL IN EACH SCAN-LINE

         for (int u = 0 ; u < nd ; u++)
         for (int y = -R ; y <= R ; y++) {
            int x = -R;
            for ( ; x <= R ; x++)
               if (d[u][R+y][R+x] != 0)
                  break;
            dd[u][R+y][0] = x;
            for ( ; x <= R ; x++)
               if (d[u][R+y][R+x] == 0)
                  break;
            dd[u][R+y][1] = x;
         }

         // DRAW THE BACKGROUND GRAD NEAR THE TOP OF THE PICTURE

         fill(0, H/5, W, H - H/5, BG);
         for (int y = 0 ; y < H/5 ; y++) {
            double t = (double)(y - H/5) / (0 - H/5);
            fill(0, y, W, 1, (int)(BG + 3*t*t * (SH - BG)));
         }

         damage = true;
      }
   }

   // SET PIXEL VALUES FOR ONE FRAME

   public void setPix(int frame) {

      // FORCE A REFRESH EVERY 30 FRAMES

      if (frame % 30 == 0)
	 damage = true;

      if (!damage)
	 return;

      // INITIALIZE THE FRAME

      initFrame(frame);

      // COMPUTE THE BACK-TO-FRONT DISPLAY ORDER FOR THE SPHERES

      orderSpheres();

      // ERASE PREVIOUS FRAME'S SPHERES AND SHADOWS

      fill(W/5,  H/5, 3*W/5,3*H/5, BG); // ERASE PREVIOUS SPHERES
      fill(  0,3*H/5, 2*W/3,2*H/5, BG); // ERASE PREVIOUS SHADOWS

      // DRAW SHADOW IN MULTIPLE LAYERS: FROM PENUMBRA TO FULLSHADOW

      int ns = 6;
      for (int m = ns-1 ; m >= 0 ; m--) {
         double t = Math.pow((double)m / ns, 0.7);
         int sh = (int)(SH + t*(BG-SH));
         for (int n = 0 ; n < nxyz ; n++)
            drawShadow(d[0], dd[0], pX(xyz[n]), pY(xyz[n])/2, sh, m);
      }

      // THEN DRAW ALL THE SPHERES, IN BACK-TO-FRONT ORDER

      for (int n = 0 ; n < nxyz ; n++) {
         int u = Math.min(nd-1, (int)((.5 - xyz[n][2])/.12));
         int v = xyz[n][2] > -.18 ? 0 : 1;
         drawSphere(S[v][(int)xyz[n][3]],d[u],dd[u],pX(xyz[n]),pY(xyz[n]));
      }
   }

// FILL A RECTANGLE WITH A CONSTANT GRAY LEVEL

   private void fill(int x, int y, int w, int h, int gray) {
      rgb[0] = rgb[1] = rgb[2] = gray;
      int packed = pack(rgb);
      for (int Y = y ; Y < y + h ; Y++) {
         int i = xy2i(x, Y);
         for (int X = x ; X < x + w ; X++)
            pix[i++] = packed;
      }
   }

// COMPUTE AT WHICH PIXEL A SPHERE PROJECTS ONTO THE IMAGE

   private int pX(double xyz[]) {         // X COORDINATE OF PROJECTION
      return (int)(W/2 + W/2 * xyz[0]);
   }
   private int pY(double xyz[]) {         // Y COORDINATE OF PROJECTION
      return (int)(H/2 - W/2 * xyz[1]);
   }

// DRAW ONE SPHERE SHADOW

   private void drawShadow(int d[][],int dd[][],int ix,int iy,int sh,int p) {
       rgb[0] = rgb[1] = rgb[2] = sh;
       int shadow = pack(rgb);

       double dY = (double)(R + 2*p) / R;
       for (int Y = 1-R-2*p ; Y <= R+2*p ; Y += 2) {
          int y = (int)(Y / dY);
          int dy[] = d[R+y];
          int x0 = dd[R+y][0] - 5*p, x1 = dd[R+y][1] + p;
          int i = xy2i(ix + 2 - W/6, iy + Y/2 + H/2 + 30) + x0;
          for(int x = x0; x < x1; x++)
             pix[i++] = shadow;
       }
   }

// DRAW ONE SPHERE

   private void drawSphere(int s[][], int d[][], int dd[][], int ix, int iy) {
       int D, rgb1[] = new int[3], rgb2[] = new int[3];
       for(int y = -R; y <= R; y++) {
          int sy[] = s[R+y];
          int dy[] = d[R+y];
          int x0 = dd[R+y][0], x1 = dd[R+y][1];
          int i = xy2i(ix, iy+y) + x0;
          for(int x = x0; x < x1; x++)
             pix[i++] = dy[R+x]==255 ? sy[R+x] : blend(i,sy[R+x],dy[R+x]);
       }
   }

// METHOD TO BLEND SMOOTHLY OVER BACKGROUND; USED NEAR SILHOUETTE OF SPHERE.

   private int rgb1[] = new int[3], rgb2[] = new int[3];

   private int blend(int i, int p2, int d) {
      unpack(rgb1, pix[i]);
      unpack(rgb2, p2);
      for (int j = 0 ; j < 3 ; j++)
         rgb2[j] = rgb1[j] + ((rgb2[j]-rgb1[j]) * d >> 8);
      return pack(rgb2);
   }

// COMPUTE THE ANTIALIASED DISK IMAGE OF A SPHERE SILHOUETTE

   private double disk(double rr, double R, double fuzz) {
      double RR = R*R;
      return rr > RR-1 ? 0 : rr > RR-fuzz ? 1-(rr-RR)/fuzz : 1;
   }

// SHADE A SPHERE

   private int rgb[] = new int[3];

   private int shade(double x, double y, double RGB[], int type) {
      double z = Math.sqrt(1 - x*x - y*y);
      double d, ss1, ss2, s1, s2, ss;
      double R = RGB[0], G = RGB[1], B = RGB[2];

      s1 = -2*x+6*z;
      s2 = 4*y-x+6*z;
      ss1 = (0.7*x-1.5*y+2.5*z) / 3.24;
      ss2 = (1.5*x-0.7*y+2.5*z) / 3.14;

      switch (type) {
      case 0:                                    // IF FROSTED SURFACE

         ss = ( Math.pow((1+ss1)/2, 37) +
                Math.pow((1+ss2)/2, 37) ) * .6;    // AMBIENT ILLUMINATION
         s1 = Math.pow((1+s1/6.6)/2, 20) * .40;    // FIRST HILITE
         s2 = Math.pow((1+s2/8.6)/2, 25) * .35;    // SECOND HILITE

         d = (x-y) / 1.414;
         d = (.2 + .6 * d*d) * (R+G+B) + (s1 + s2) * .67; // DIFFUSE

         rgb[0] = f2i(R*d + G*s1 + B*s2 + ss);
         rgb[1] = f2i(G*d + B*s1 + R*s2 + ss);
         rgb[2] = f2i(B*d + R*s1 + G*s2 + ss);
         break;

      case 1:                                    // IF POLISHED SURFACE

         ss = ( Math.pow((1+ss1)/2, 67) +
                Math.pow((1+ss2)/2, 67) ) * 4;     // AMBIENT ILLUMINATION
         s1 = Math.pow((1+s1/7.6)/2, 27) * 3.50;   // FIRST HILITE
         s2 = Math.pow((1+s2/7.6)/2, 11) * 0.70;   // SECOND HILITE

         rgb[0] = f2i(R*(s1 + s2) + ss);
         rgb[1] = f2i(G*(s1 + s2) + ss);
         rgb[2] = f2i(B*(s1 + s2) + ss);
         break;
      }
      return pack(rgb);
   }

// CONVERT A FLOATING POINT VALUE TO A 0..255 INTEGER

   private int f2i(double t) {
      return (int)(255 * t) & 255;
   }

// INITIALIZE THE PLACEMENT AND COLOR SCHEME FOR SPHERES

   private void defineSpheres() {
      setView();
      I = J = K = 10;
      s = new int[I][J][K];
      xyz = new double[I * J * K][4];

      for(int i = 0; i < I; i++)
      for(int j = 0; j < J; j++)
      for(int k = 0; k < K; k++) {
         computeXYZ(i, j, k);
         double d = X * X + Y * Y + Z * Z;  // COMPUTER RADIUS SQUARED
         double d1 = 0.25; // R-SQUARED FOR OUTER-MOST SPHERES
	 double d2 = 0.17; // R-SQUARED FOR INNER-MOST SPHERES
         if (d < d1 && d > d2) { // IF R-SQUARED IS BETWEEN d1 AND d2
            s[i][j][k] =         // PLACE A SPHERE; VARY COLOR W. RADIUS
               1 + (int)(colors.length * (d - d2) / (d1 - d2));
         }
      }
   }

// COMPUTE PROPER BACK TO FRONT ORDER FOR TRAVERSING SPHERES

   private void orderSpheres() {

      // OUTER LOOP WILL BE WHICHEVER OF i,j, OR k CHANGES FASTEST IN Z

      double di = pz(1, 0, 0) - pz(0, 0, 0);
      double dj = pz(0, 1, 0) - pz(0, 0, 0);
      double dk = pz(0, 0, 1) - pz(0, 0, 0);
      double ai = Math.abs(di);
      double aj = Math.abs(dj);
      double ak = Math.abs(dk);
      byte b = ai<=aj || ai<=ak ? aj<=ai || aj<=ak ? (byte)2 : 1 : 0;
      nxyz = 0;
      for(int n = 0; n < I * J * K; n++) {
         int i = ( b != 0 ? b != 1 ? n : n / J : n / J / K ) % I;
         int j = ( b != 1 ? b != 2 ? n : n / K : n / K / I ) % J;
         int k = ( b != 2 ? b != 0 ? n : n / I : n / I / J ) % K;

         // IN EACH DIMENSION, SCAN ORDER IS FROM FURTHEST TO NEAREST SPHERES

         if (di < 0) i = I-1 - i;
         if (dj < 0) j = J-1 - j;
         if (dk < 0) k = K-1 - k;

         // IF A SPHERE IS AT GRID PT, ROTATE TO VIEW COORDS AND ADD TO LIST

         if (s[i][j][k] != 0) {
            computeXYZ(i,j,k);
            xyz[nxyz][0] = X;
            xyz[nxyz][1] = Y;
            xyz[nxyz][2] = Z;
            xyz[nxyz][3] = s[i][j][k] - 1;
            nxyz++;
         }
      }
   }

// ROTATE THE VIEW OF A POINT, DEPENDING ON USER DEFINED ROTATION ANGLES

   private void computeXYZ(double i, double j, double k) {

      double x1 = (i - (double)(I / 2)) / (double)(I - 1);
      double y1 = (j - (double)(J / 2)) / (double)(I - 1);
      double z1 = (k - (double)(K / 2)) / (double)(I - 1);

      double x2 = CP * x1 + SP * y1; // ROTATE LATITUDE BY PHI
      double y2 = SP * x1 - CP * y1;
      double z2 = z1;

      X = CT * x2 - ST * z2;         // ROTATE LONGITUDE BY THETA
      Y = y2;
      Z = ST * x2 + CT * z2;
   }

// RETURN THE Z COORDINATE OF THE CENTER OF THE SPHERE AT (i,j,k) ON GRID

   private double pz(int i, int j, int k) {

      computeXYZ(i, j, k);
      return Z;
   }

// UPDATE THE VIEW ROTATION VARIABLES

   private void setView() {

      CT = Math.cos(theta);
      ST = Math.sin(theta);
      CP = Math.cos(phi);
      SP = Math.sin(phi);
   }

// RESPOND TO USER MOUSE DOWN BY REMEMBERING CURSOR POSITION

   public boolean mouseDown(Event event, int x, int y) {

      mx = x;
      my = y;
      return true;
   }

// RESPOND TO USER MOUSE DRAG BY ROTATING VIEW

   public boolean mouseDrag(Event event, int x, int y) {

      theta += 0.03 * (double)(mx - x); // HORIZONTAL MOTION CHANGES THETA
      phi   -= 0.03 * (double)(my - y); // VERTICAL MOTION CHANGES PHI
      setView();
      mx = x;
      my = y;
      damage = true;
      return true;
   }

// INTERNAL VARIABLES

   private int S[][][][];  // PIXEL IMAGE FOR EACH TYPE OF SPHERE
   private int d[][][];    // DISK IMAGE FOR VARIOUSLY DEFOCUSED SPHERES
   private int dd[][][];   // X START/STOP FOR EACH SCAN-LINE OF DISK IMAGE
   private int nd = 5;     // HOW MANY DIFFERENT LEVELS OF FOCUS

   private int R;                      // SPHERE RADIUS
   private int BG = 110, SH =  94;     // BACKGROUND AND SHADOW GRAY LEVELS
   private int I, J, K;                // THE GRID DIMENSIONS
   private int nxyz;                   // NUMBER OF SPHERES TO DISPLAY
   private double xyz[][];             // VIEW XYZs, IN DISPLAY ORDER
   private int s[][][];                // SPHERE COLOR AT EACH GRID POINT
   private double X, Y, Z;             // VIEW XYZ FOR ONE SPHERE
   private double F;                   // CAMERA FOCAL LENGTH
   private double theta = 1, phi = .5; // USER-DEFINED VIEW ROTATION ANGLES
   private double CT, ST, CP, SP;      // INTERNAL VIEW ROTATION VARIABLES
   private int mx, my;                 // CURRENT MOUSE POSITION

// ALL THE PRETTY COLORS

   private double colors[][] = {
      {0.60, 0.50, 0.00}, // gold
      {0.60, 0.40, 0.10}, // amber
      {0.68, 0.20, 0.20}, // ruby
      {0.10, 0.10, 0.55}, // sapphire
      {0.50, 0.35, 0.40}, // satin
   };
}