
import java.awt.*;

public class LittleGhost1 extends PixApplet
{
   final int LEFT_EYE     = 0,
	     RIGHT_EYE    = 1,
	     BODY         = 2,
	     SKIRT        = 3,
	     LEFT_ARM     = 4,
	     RIGHT_ARM    = 5,
             TOP_WAFER    = 6,
	     YUMMY_CREME  = 7,
             BOTTOM_WAFER = 8,
	     NSHAPES      = 9;

   Shape shapes[] = new Shape[NSHAPES];
   double c0[] = {1, 1,1, 0,0,0, 0,0,0, -1}; // ELLIPSOID
   double c1[] = {1,-1,1, 0,0,0, 0,0,0, -1}; // HYPERBOLOID
   KM km = new KM();
   double nearest_t = 0, theta = 0;
   double focal_length = 3.;             // FOCAL LENGTH OF "LENS"
   double v[] = {0,0,1};                 // VIEW POINT
   double w[] = {0,0,-focal_length};     // RAY DIRECTION
   double t[] = new double[2];           // PLACE TO STORE ROOTS
   double p[] = {0,0,0};                 // SURFACE POINT
   double n[] = {0,0,0};                 // SURFACE NORMAL
   double L[][] = {{1,1,1},{-1,.8,.1}};  // LIGHT DIRECTIONS

   boolean isAnimating = false;
   boolean isBlinking = false;
   boolean isCremeFilling = false;
   boolean isEdgeSmoothing = false;
   boolean isRising = false;
   boolean isShowingRays = false;
   boolean isTexturing = false;
   boolean isWideBase = false;

   public void init() {
      super.init();

      Material cookieMaterial = new Material(),
               whiteMaterial  = new Material(),
               blackMaterial  = new Material();

      cookieMaterial.setAmbient(.05,.02,0).setDiffuse(.35,.14,0).setSpecular(.25,.25,.25,10);
      cookieMaterial.addNoise(4,.27);
      whiteMaterial.setAmbient(.55,.4,.3).setDiffuse(.7,.7,.7).setSpecular(0,0,0,1);
      blackMaterial.setAmbient(0,0,0).setDiffuse(0,0,0).setSpecular(1,1,1,10);

      for (int i = 0 ; i < shapes.length ; i++) {
         shapes[i] = new Quadratic(i==SKIRT ? c1 : c0);
         Quadratic q = (Quadratic)shapes[i];
	 switch (i) {
	 case SKIRT:
            q.addClipPlane(0, 1,0,.3);
            q.addClipPlane(0,-1,0,.3);
	    break;
	 case BODY:
            q.addClipPlane(0,1,0,.7);
	    break;
         }
         switch (i) {
	 case TOP_WAFER:
	 case BOTTOM_WAFER:
	    shapes[i].material = cookieMaterial;
	    break;
	 case BODY:
	 case SKIRT:
	 case LEFT_ARM:
	 case RIGHT_ARM:
	 case YUMMY_CREME:
	    shapes[i].material = whiteMaterial;
	    break;
	 case LEFT_EYE:
	 case RIGHT_EYE:
	    shapes[i].material = blackMaterial;
	    break;
	 }
      }
      for (int i = 0 ; i < L.length ; i++)
         normalize(L[i]);

      mark = new int[W*H];
   }

   int mark[];

   public void setPix(int frame) {
      super.setPix(frame);
      animate(frame);

      for (int i = 0 ; i < W*H ; i++)
         mark[i] = 0;

      // RENDER EVERY 4TH x 4TH PIXEL

      for (int row = 0 ; row < H ; row += 4)
      for (int col = 0 ; col < W ; col += 4)
         render(row, col);

      // RENDER ALL PIXELS ALONG RIGHT AND BOTTOM EDGES

      for (int row = 0 ; row < H ; row++)
	 render(row,W-1);
      for (int col = 0 ; col < W ; col++)
	 render(H-1,col);

      // SELECTIVELY SAMPLE DOWN TO 2x2

      for (int row = 0 ; row < H   ; row += 4)
      for (int col = 0 ; col < W-4 ; col += 4)
         interpolate(xy2i(col,row),xy2i(col+4,row), row,col+2, 2);

      for (int row = 0 ; row < H-4 ; row += 4)
      for (int col = 0 ; col < W   ; col += 2)
         interpolate(xy2i(col,row),xy2i(col,row+4), row+2,col, 2);

      // SELECTIVELY SAMPLE DOWN TO 1x1

      for (int row = 0 ; row < H   ; row += 2)
      for (int col = 0 ; col < W-2 ; col += 2)
         interpolate(xy2i(col,row),xy2i(col+2,row), row,col+1, 1);

      for (int row = 0 ; row < H-2 ; row += 2)
      for (int col = 0 ; col < W   ; col++)
         interpolate(xy2i(col,row),xy2i(col,row+2), row+1,col, 1);

      // SUPERSAMPLE PIXELS WHEREVER COLOR CHANGES RAPIDLY

if (isEdgeSmoothing) {
      for (int row = 0 ; row < H-1 ; row++)
      for (int col = 0 ; col < W-1 ; col++) {
	 int i = xy2i(col, row);
         unpack(a, pix[i]);
         unpack(b, pix[i+1]);
         unpack(c, pix[i+W]);
         unpack(d, pix[i+W+1]);
         if (edge(a,b,0) || edge(b,d,0) || edge(d,c,0) || edge(c,a,0)) {
	    render(row+.5,col   ,b);
	    render(row   ,col+.5,c);
	    render(row+.5,col+.5,d);
            pix[i] = pack(a[0]+b[0]+c[0]+d[0]>>2,
                          a[1]+b[1]+c[1]+d[1]>>2,
                          a[2]+b[2]+c[2]+d[2]>>2);
	 }
      }
}

if (isShowingRays) {
      for (int i = 0 ; i < W*H ; i++)
         switch (mark[i]) {
	 case 0:
	    break;
	 case 1:
	    pix[i] = pack(180,0,0);
	    break;
	 case 2:
	    pix[i] = pack(0,180,0);
	    break;
	 case 3:
	    pix[i] = pack(255,255,255);
	    break;
	 }
}

      damage = true;
   }
   int a[] = new int[3], b[] = new int[3], c[] = new int[3], d[] = new int[3];
   void interpolate(int i1, int i2, int row, int col, int eps) {
      unpack(a, pix[i1]);
      unpack(b, pix[i2]);
      if (edge(a, b, eps))
         render(row, col);
      else
         pix[xy2i(col,row)] = pack(a[0]+b[0]>>1,a[1]+b[1]>>1,a[2]+b[2]>>1);
   }
   boolean edge(int a[], int b[], int shft) {
      int dr = b[0]-a[0], dg = b[1]-a[1], db = b[2]-a[2];
      return (dr*dr + 2*dg*dg + 3*db*db > (1000 >> shft));
   }

   double ndw[] = {0,0,0};
   int rgb[] = {0,0,0};

   void render(double row, double col) {
       render(row, col, rgb);
       int i = xy2i((int)col, (int)row);
       pix[i] = pack(rgb[0],rgb[1],rgb[2]);
   }

   void render(double row, double col, int rgb[]) {

      mark[xy2i((int)col, (int)row)]++;

      w[0] =  col / (double)W - 0.5;      // CONSTRUCT RAY DIRECTION VECTOR
      w[1] = -row / (double)W + 0.5 * H / W;
      w[2] = -focal_length;
      normalize(w);

      Shape nearest_s = traceRay(v, w, t);

      if (nearest_s != null) { // IF RAY HITS ANYTHING, SHADE NEAREST SHAPE

         p[0] = v[0] + nearest_t * w[0];       // COMPUTE SURFACE POINT
         p[1] = v[1] + nearest_t * w[1];
         p[2] = v[2] + nearest_t * w[2];

	 Material m = nearest_s.material;

         double red = m.ambient[0], grn = m.ambient[1], blu = m.ambient[2];

         double nde = -100;
         for (int i = 0 ; i < L.length ; i++)
            if (traceRay(p, L[i], t) == null) {       // IF NOT IN SHADOW,
               nearest_s.computeNormal(p, n);         // GET SURFACE NORMAL
	       double d = dot(L[i],n);
	       if (d > 0) {                           // ADD DIFFUSE
	          d = d * d;
	          red += d * m.diffuse[0];
	          grn += d * m.diffuse[1];
	          blu += d * m.diffuse[2];
		  if (nde < -1) {
	             nde = -dot(n,w);
		     ndw[0] = 2 * n[0]*nde + w[0];
		     ndw[1] = 2 * n[1]*nde + w[1];
		     ndw[2] = 2 * n[2]*nde + w[2];
                  }
	          double s = dot(ndw, L[i]);
                  if (s > 0) {                        // ADD SPECULAR
	             s = Math.pow(s, m.specular[3]);
	             red += s * m.specular[0];
	             grn += s * m.specular[1];
	             blu += s * m.specular[2];
	          }
	       }
            }
if (isTexturing) {
         // COMPUTE NOISE TEXTURE

         if (nearest_s.material.nBands > 0) {
	    Matrix.transform(nearest_s.matrix, p, pp);
	    double t = 1;
	    for (int b = 0 ; b < nearest_s.material.nBands ; b++) {
	       double freq = nearest_s.material.noise[2*b  ];
	       double ampl = nearest_s.material.noise[2*b+1];
	       t *= 1 + ampl * INoise.noise((int)(freq * pp[0]),
	                                    (int)(freq * pp[1]),
	                                    (int)(freq * pp[2]));
            }
            red *= t;
            grn *= t;
            blu *= t;
	 }
}
	 rgb[0] = f2i(red);
	 rgb[1] = f2i(grn);
	 rgb[2] = f2i(blu);
      }

      else {           // MISSED EVERYTHING: RENDER BACKGROUND COLOR
	 rgb[0] = 40;
	 rgb[1] = 40;
	 rgb[2] = 120;
      }
   }

   int f2i(double f) { return (int)Math.max(0,Math.min(255,255*f)); }
   double pp[] = {0,0,0};
   double rate = 20., old_time = 0, elapsed = 0, throttle = 0;

   void animate(int frame) {
      double time = System.currentTimeMillis() / 1000.;
      if (old_time == 0)
         old_time = time;
      throttle = .9 * throttle + .1 * (time - old_time);
      //System.out.println(throttle);
      elapsed += rate * (time - old_time);
      old_time = time;

      rate = .9*rate + .1*20;

      boolean blink = false;
if (isBlinking) {
      blink = (elapsed % 8 > 6) && (elapsed % 9.1 > 7);
}

      double cos = 1, sin = 0;
if (isAnimating) {
      cos = Math.cos(.5*elapsed);
      sin = Math.sin(.5*elapsed);
}
      double t = .1, r = 1;
if (isRising) {
      t = .1 * (1 + .0025*rate * cos);
      r = (rate-15)/25;
}
else
      rate = 20;

      km.initFrame();

      km.rotateX(.2);
      km.rotateY(theta);
      km.translate(0,-.01,0);

      // COOKIE

      km.push();
         km.translate(0,-.03,0);
         km.rotateX(.1);
         km.scale(.1,.03,.1);
         shapes[TOP_WAFER].transform(km.get());
	 km.translate(0,-1,0);
	 km.push();
	    double tt = lerp(r, 1.1-2*t, .9);
	    km.scale(tt,isCremeFilling ? 1.3 : 0,tt);
            shapes[YUMMY_CREME].transform(km.get());
         km.pop();
	 km.translate(0,isCremeFilling ? -1 : -1000,0);
         shapes[BOTTOM_WAFER].transform(km.get());
      km.pop();

if (isAnimating) {
      km.rotateZ(r*.2*Math.sin(.22*elapsed));
      km.rotateX(r*.2*Math.sin(.19*elapsed));
}
      km.translate(0,-.02+.03*(t/.1*rate-20)/20,0);

      // MAIN BODY OF GHOST

      km.push();

         km.scale(.003/t,.5*t,.003/t);
         km.translate(0,1,0);
         shapes[BODY].transform(km.get());

	 km.push();
	    km.translate(.3,.65,.5);
	    km.scale(.3,blink ? 0 : .22,.3);
	    shapes[LEFT_EYE].transform(km.get());
	 km.pop();
	 km.push();
	    km.translate(-.3,.65,.5);
	    km.scale(.3,blink ? 0 : .22,.3);
	    shapes[RIGHT_EYE].transform(km.get());
	 km.pop();

	 km.translate(0,-.5,0);
	 km.push();
	    km.translate(0,-.12,0);
            km.scale(.84,isWideBase ? .45 : 0,.84);
            shapes[SKIRT].transform(km.get());
	 km.pop();
      km.pop();
      km.translate(0,t - .04,0);

      // ARMS

      km.push();
         km.translate(.01,0,0);
         km.rotateZ(r-.5+r*.5*sin);
         km.rotateX(r*.3*sin);
         km.rotateY(r*.5*cos);
         km.scale(.03,.01,.015);
         km.translate(.7,0,0);
         shapes[LEFT_ARM].transform(km.get());
      km.pop();

      km.push();
         km.translate(-.01,0,0);
         km.rotateZ(-(r-.5)-r*.5*sin);
         km.rotateX(-r*.3*sin);
         km.rotateY(-r*.5*cos);
         km.scale(.03,.01,.015);
         km.translate(-.7,0,0);
         shapes[RIGHT_ARM].transform(km.get());
      km.pop();
   }

   double V[] = new double[24];
   Shape traceRay(double v[], double w[], double t[]) {
      Shape nearest_s = null;
      nearest_t = 100000;
      Quadratic.compute_V(v, w, V);
      for (int i = 0 ; i < shapes.length ; i++) {
         Shape s = shapes[i];
         if (s.traceRay(V,t)>0 && t[0]>0 && t[0]<nearest_t) {
            nearest_s = s;
            nearest_t = t[0];
         }
      }
      return nearest_s;
   }

   int mx = 0, my = 0;
   public boolean mouseDown(Event e, int x, int y) {
      mx = x; my = y; return true;
   }
   public boolean mouseDrag(Event e, int x, int y) {
      theta += .02 * (x - mx);
      rate = .9*rate + .1*40;
      damage = true;
      mx = x; my = y; return true;
   }
   public boolean keyUp(Event e, int key) {
      switch (key) {
      case 'a':
         isAnimating = ! isAnimating;
	 break;
      case 'b':
         isBlinking = ! isBlinking;
	 break;
      case 'c':
         isCremeFilling = ! isCremeFilling;
	 break;
      case 'e':
         isEdgeSmoothing = ! isEdgeSmoothing;
	 break;
      case 'r':
         isRising = ! isRising;
	 break;
      case 's':
         isShowingRays = ! isShowingRays;
	 break;
      case 't':
         isTexturing = ! isTexturing;
	 break;
      case 'w':
         isWideBase = ! isWideBase;
	 break;
      }
      return true;
   }

   static double lerp(double t, double a, double b) {
      return a + t * (b - a); 
   }
   static double dot(double a[],double b[]) {
      return a[0]*b[0]+a[1]*b[1]+a[2]*b[2];
   }
   static void normalize(double v[]) {
      double s = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
      v[0] /= s; v[1] /= s; v[2] /= s;
   }
}

