/*
 * ChernoffFaceApplet.java
 *
 * Copyright 1998 John Wiseman
 */
 
import java.awt.*;
import java.applet.Applet;
import java.io.*;
import java.util.Random;


/**

  This applet draws Chernoff faces, a method of representing multivariate data
  developed by statistician Herman Chernoff.
    
  Chernoff faces are described by ten facial characteristic parameters: head eccentricity,
  eye eccentricity, pupil size, eyebrow slant, nose size, mouth shape, eye spacing,
  eye size, mouth length and degree of mouth opening.  Each parameter is repsented by
  a number between 0 and 1.
  
  The code for drawing the faces is based on the code in the book <em>Computers, Pattern,
  Chaos and Beauty</em> by Clifford Pickover.
  

  @author John Wiseman
  @version 1.0, 3/24/98

 */	

public class ChernoffFaceApplet extends Applet implements Animator, Runnable {
	// offscreenContext and offscreenImage are used for double-buffered animation.
	Graphics offscreenContext;
	Image offscreenImage;
	
	// The description of the face to be drawn.
	AniFaceVector facev = new AniFaceVector();

	// The utility class that draws faces.
	FacePainter face = new FacePainter();
	
	// Color to draw the face in.
	Color foregroundColor = Color.black;

	// Background color
	Color backgroundColor = Color.white;

	// Speed at which to animate faces. This value determines the number of frames that are
	// used to animate a transition from one face to another.
	int speed = 10;
	
	// The target frames per second for animation of faces.
	int fps = 20;
	
	
	/** General applet information.  I don't really know how to use this. */
	public String getAppletInfo() {
		return "Chernoff Face v1.00 (03/24/98), by John Wiseman <wiseman@neodesic.com>";
  }

	
	/** Applet parameter information.  I don't really know how to use this. */
  public String[][] getParameterInfo() {
		String[][] info = {
	    {"backgroundcolor",	"int",					"color (24 bit RGB number) used as background color"},
	    {"foregroundcolor", "int",					"color (24 bit RGB number) used as face color"},
	    {"face",						"face vector",	"a description of the face to display.  Either 10 " +
	                                        "comma delimited numbers in the range 0 to 1 or " +
	                                        "\"animate\", which causes the applet to animate " +
	                                        "transitions between random faces forever"},
	    {"speed",						"int",					"speed of animation (1-1000)"},
			{"fps",							"int",					"frames per second"},
		};
		return info;
  }

	
	/** Initialize the applet.  Get parameters and figure out whether to animate. */
	public void init() {
		String v, param;
		int i;

		// Set up the offscreen drawing context so we can do double buffered animation.
		offscreenImage = this.createImage(size().width, size().height);
		offscreenContext = offscreenImage.getGraphics();
		
		// Get the background color, if one was specified.  If it is not specified, use
		// our container's background color.
		param = getParameter("backgroundcolor");
		if (param != null) {
			backgroundColor = decodeColor(param);
		} else {
			backgroundColor = getBackground();
		}

		// Get the foreground color, if one was specified.
		param = getParameter("foregroundcolor");
		if (param != null) {
			foregroundColor = decodeColor(param);
		}
		
		// Get the animation speed, if one was specified.
		param = getParameter("speed");
		if (param != null) {
			speed = Integer.parseInt(param);
		}

		// Get the desired frames per second, if one was specified.
		param = getParameter("fps");
		if (param != null) {
			fps = Integer.parseInt(param);
		}
	
		// If there is no face parameter specified, just draw a random one.
		// If face = "random", then animate transitions between random faces in an endless loop.
		// Otherwise, assume face is a list of 10 numbers and draw the described face.
		param = getParameter("face");
		if (param != null && param.equals("animate")) {
			// Enter endless animation loop.
			animateRandomFaces();
		} else if (param != null) {
			// Parse the list of numbers making up the face vector.
			StringBufferInputStream arg_stream = new StringBufferInputStream(param);
			StreamTokenizer tokenizer = new StreamTokenizer(arg_stream);
			i = 1;
			
			try {
				while (i < 11 && tokenizer.nextToken() != StreamTokenizer.TT_EOF) {
					if (tokenizer.ttype == StreamTokenizer.TT_NUMBER) {
						facev.p[i] = tokenizer.nval;
						i++;
					}
				}
			}
			catch (IOException e) {
			}
		}
		
		// If we reached this line, we're just drawing a static face.  So draw it.
		repaint();
	}
		
	
	/** Runs the Applet in an endless animation loop. */	
	public void animateRandomFaces() {
		// Start a new thread to do the animation.  This applet is very simple and does not
		// require a separate animation thread, but tests show that if we don't have one
		// we get strange behavior in some situations.  The applet runs fine in Codewarrior 11,
		// dies with a Null pointer exception in IE 4, and never draws in Netscape 3.0.
		(new Thread(this)).start();
	}
	
	
	/** Actually does the work of animating transitions between random faces. */
	public void run() {
		// Create a new animation timer tied to this object.
		AnimationTimer animation_timer = new AnimationTimer(this);
		AniFaceVector v, new_v;
	
		animation_timer.FPS = fps;
		v = new AniFaceVector();
		// Do forever.
		for (;;) {
			// Choose a new random face.
			new_v = new AniFaceVector();
			// Animate (synchronously) the transtion from the last face to the new face.
			animation_timer.doSyncAnimation(v, new_v, (int)(v.distance(new_v) * (1000 / speed)) + 1);

			v = new_v;
		}	
	}

	
	/** The implementation of the Animator method.  Draws the given state, which is an
	    AniFaceVector. */
	public void animate(AnimationState state) {
		facev = (AniFaceVector) state;
		repaint();		
	}
	
	
	public void resize(int w, int h) {
		super.resize(w, h);
		repaint();
	}

	
	/** Paint the current frame.  Uses double-buffering. */
	public void paint(Graphics g) {
		// Because I don't know whether this could be called before the init() method
		// has completed, I take a conservative approach of checking the validity of
		// various parameters. 
		if (facev != null) {
			if (offscreenContext != null && offscreenImage != null) {
				// Erase the last face (uses offscreen context).
				offscreenContext.setColor(backgroundColor);
				offscreenContext.fillRect(0, 0, size().width, size().height);
		
				// Draw the current face (uses offscreen context).
				offscreenContext.setColor(foregroundColor);
				face.draw(offscreenContext, facev, 0, 0, size().width, size().height);
			
				// Blast the offscreen image to the screen.
				g.drawImage(offscreenImage, 0, 0, this);
			} else {
				// The safe way.
				face.draw(g, facev, 0, 0, size().width, size().height);
			}
		}
	}
	
	
	/** We override this method because there's no need to erase anything; just paint. */
	public void update(Graphics g) {
		paint(g);
	}
	
	
	/** Converts strings of the form "#FFFFFF" and "0xffffff" (and even octal nonsense) to
	    colors. */
	Color decodeColor(String s) {
		int val = 0;
		try {
		  if (s.startsWith("0x")) {
				// We got a C programmer.
				val = Integer.parseInt(s.substring(2), 16);
	    } else if (s.startsWith("#")) {
	    	// Caught an HTML weenie.
				val = Integer.parseInt(s.substring(1), 16);
	    } else if (s.startsWith("0") && s.length() > 1) {
				// Good Lord, an unreconstructed unix oldtimer.
				val = Integer.parseInt(s.substring(1), 8);
	    } else {
	    	// Must be a Visual Basic programmer.
				val = Integer.parseInt(s, 10);
	    }
	    return new Color(val);
		} catch (NumberFormatException e) {
	    return null;
		}
   }

}


class AniFaceVector extends FaceVector implements AnimationState {

	public AniFaceVector(double p1, double p2, double p3, double p4, double p5, double p6, double p7, double p8, double p9, double p10) {
		super(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10);
	}

	public AniFaceVector() {
		super();
	}
	
	/** Interpolates smoothly from one FaceVector to another.
	
	    @return   A new vector that is the result of the interpolation.
	 */
	public AnimationState interpolate(AnimationState start, AnimationState end, double t) {
		int i;
		AniFaceVector interpolated = new AniFaceVector(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
		
		for (i = 1; i < 11; i++) {
			interpolated.p[i] = ((FaceVector) start).p[i] + (((FaceVector) end).p[i] - ((FaceVector) start).p[i]) * t;
		}
		return interpolated;
	}
}
	


