/*
 * atom.java
 *
 * Written 4/27/96 by Chuck McManis
 * Created with Symantec Cafe' 1.0
 *
 */

import java.awt.*;
import java.applet.*;
import java.util.StringTokenizer;

/**
 * The Atomic Applet
 *
 * This simple applet originated because I was inspired by an
 * applet that Jef Poskanzer created for the Acme java pages.
 *
 * His however loaded some images with the electrons in different
 * places and I thought, "Gee, Java should be able to animate that."
 * So I wrote this.
 *
 * The applet displays several "rings" and on each ring an electron
 * is moving. The electrons alternately move in a clockwise or anticlockwise
 * direction.
 *
 * You can set lots of parameters on this applet, I've itemized the major
 * ones below:
 *      bgcolor = color of the background.
 *      ringcolor = color of the electron rings.
 *      ecolor = color of the electrons themselves.
 *      phase = ratio of the ring's ovalness (0 is a line, 90 is a circle)
 *      electrons = Number of electron rings to show.
 *      speed = number of degrees per step the electrons move.
 *      rotate = number of degrees the rings rotate when they spin.
 *
 *  @version    1.0
 *  @author     Chuck McManis
 *
 */

public class atom extends Applet implements Runnable {

    // Cached width and height of the applet.
    int myHeight;
    int myWidth;

    // This applet's background color.
    Color bgColor;

    // This is the color of the rings
    Color ringColor;

    // This is the color of the electrons
    Color eColor;

    // Current rotation angle of all the rings.
    double angle = 0;

    // Number of line segments in an oval.
    static final int SAMPLES = 32;

    // The number of degrees to rotate the rings, on each tick.
    double rotIncrement;

    public final double twoPI = (Math.PI * 2);
    public final double toRadians = (Math.PI * 2)/360;

    // Current Position of the electrons and their speed.
    double epos[];
    double eincr[];

    // Phase relationship of rings (determines roundness)
    double phase = 30;

    /**
     * This method allows you to specify a color as one of
     * red, green, blue, yellow, cyan, magenta, orange, pink,
     * black, gray, or white.
     *
     * Returns null if the color name didn't match.
     */
    Color primaryColorbyName(String colorName) {
	    if (colorName.equalsIgnoreCase("red")) {
	        return Color.red;
	    } else if (colorName.equalsIgnoreCase("green")) {
	        return Color.green;
	    } else if (colorName.equalsIgnoreCase("blue")) {
	        return Color.blue;
	    } else if (colorName.equalsIgnoreCase("yellow")) {
	        return Color.yellow;
	    } else if (colorName.equalsIgnoreCase("cyan")) {
	        return Color.cyan;
	    } else if (colorName.equalsIgnoreCase("magenta")) {
	        return Color.magenta;
	    } else if (colorName.equalsIgnoreCase("orange")) {
	        return Color.orange;
	    } else if (colorName.equalsIgnoreCase("pink")) {
	        return Color.pink;
	    } else if (colorName.equalsIgnoreCase("white")) {
	        return Color.white;
        } else if (colorName.equalsIgnoreCase("gray")) {
            return Color.gray;
	    } else if (colorName.equalsIgnoreCase("black")) {
	        return Color.black;
	    }
	    return null;
    }


   /**
     * Return a Color value from the parameter list.
     * Colors may be specified by name or by red, green, and
     * blue tuple values.
     *
     * By name the acceptable colors are
     * <pre>
     *    ("light" | "dark" | "") followed by one of
     *       ("red" | "green" | "blue" | "yellow" |
     *        "magenta" | "cyan" | "pink" | "orange" |
     *        "white" | "gray" | "black" )
     * </pre>
     *
     * Alternatively the color may be specified as #rrggbb where
     * rr, gg, and bb are hex digits (0-f) representing the value
     * of the red, green, and blue values in the color.
     *
     * Or finally the color may be specified as rrr,ggg,bbb where
     * rrr, ggg, and bbb are decimal numbers between 0 and 255 that
     * specify the red, green, and blue tuple values.
     *
     * On pretty much any error the default color is returned.
     *
     */
    public Color getColorParameter(String name, Color defaultColor) {
    	String x = getParameter(name);
    	int z;
    	Color res;


    	// Return the default color if no color was specified
    	if (x == null)
    	    return defaultColor;

    	// Parse a color that starts with '#'
    	if (x.startsWith("#")) {
		    try {
		        z = Integer.parseInt(x.substring(2), 16);
		        return( new Color((z >>> 16) & 0xff,
				          (z >>> 8) & 0xff,
				          (z & 0xff)));
		    } catch (NumberFormatException e) { return defaultColor; }
		}

		// Parse a numerical triple
		if ((x.charAt(0) <= '9') && (x.charAt(0) >= '0')) {
		    StringTokenizer st = new StringTokenizer(x, ",");
		    String y;
		    int c[] = new int[3];

		    for (int i = 0; (i < 3) && st.hasMoreTokens(); i++) {
    		    y = st.nextToken();
	    	    if (y != null) {
		            try { c[i] = Integer.parseInt(y); }
		            catch (NumberFormatException e) { c[i] = 0; }
    		    } else
    		        c[i] = 0;
		    }
		    return new Color(c[0], c[1], c[2]);
		}

		// Now parse a string based color choice
		if (x.length() < 5) {
		    res = primaryColorbyName(x);
		    return ((res == null) ? defaultColor : res);
		}

		if (x.substring(0, 4).equalsIgnoreCase("dark")) {
          	res = primaryColorbyName(x.substring(4));
          	return ((res == null) ? defaultColor : res.darker());
        }

        if (x.substring(0, 5).equalsIgnoreCase("light")) {
            res = primaryColorbyName(x.substring(5));
            return ((res == null) ? defaultColor : res.brighter());
        }

        res = primaryColorbyName(x);
        return ((res == null) ? defaultColor : res);
    }

    /**
     * Return an integer parameter from the param list or
     * the default if the parameter wasn't specified.
     */
    public int getIntParameter(String name, int defaultValue) {
        String x = getParameter(name);

        if (x == null)
            return defaultValue;

        try {
            return Integer.parseInt(x);
        } catch (NumberFormatException e) { return defaultValue; }
    }

    /**
     * Compute the max magnitude of the rings given the
     * applet's width and height.
     */
    double maxMagnitude() {
        return ((Math.min(myHeight - 2, myWidth -2) / 2) * .66);
    }

    /**
     * This applet's init method. Nothing too exciting, check for a
     * background color, how many electrons etc.
     */
    public void init() {

        myWidth = size().width;
        myHeight = size().height;
        int n;

        n = getIntParameter("electrons", 3);

        epos = new double[n];
        eincr = new double[n];
        int erate = getIntParameter("speed", 5);
        for (int i = 0; i < n; i++) {
            epos[i] = (360 * i) / n;
            eincr[i] = ((i & 1) == 0) ? erate : -erate;
        }

        phase = getIntParameter("phase", 30);
        rotIncrement = getIntParameter("rotate", 10);
        bgColor = getColorParameter("bgcolor", new Color(255, 255, 240));
        ringColor = getColorParameter("ringcolor", Color.red);
        eColor = getColorParameter("ecolor", Color.black);
    }


    /**
     * Draw an electron on its ring.
     *
     * After the ring is drawn this is called to draw the electron on
     * it. The size of the electron is dynamically computed based on the
     * size of the applet.
     *
     */
    void drawElectron(Graphics g, int x, int y, int e, double r) {
        int ex, ey, exx, eyy;
        int eSize = (int)(maxMagnitude()/5);
        int numElectrons = epos.length;

        ex = (int)(maxMagnitude() * Math.sin(epos[e] * toRadians));
        ey = (int)(maxMagnitude() * Math.sin((epos[e] + phase) * toRadians));

        // compute the rotated coordinates.
        exx = X(ex, ey, r);
        eyy = Y(ex, ey, r);

        g.fillOval((exx + x)-(eSize/2), (eyy + y)-(eSize/2), eSize, eSize);

        // move the electron around the ring.
        epos[e] = epos[e] + eincr[e];
        if (epos[e] < 0) epos[e] += 360;
        else if (epos[e] > 360) epos[e] -= 360;
    }

    // Classic graphics rotation about the origin transformation.
    int X(int x, int y, double r) {
        return ( (int)(x * Math.cos(r * toRadians) +
                       y * Math.sin(r * toRadians)) );
    }

    // Classic graphics rotation about the origin transformation.
    int Y(int x, int y, double r) {
        return ( (int)( (-x * Math.sin(r * toRadians)) +
                        ( y * Math.cos(r * toRadians)) ));
    }

    /**
     * Draw the ring. Rather than try to draw an oval in rotation I
     * use the two sine waves trick. This makes plotting the electron
     * position really easy too.
     *
     */
    void drawOval(Graphics g, int x, int y, double r) {
        int nx, ny;
        double z;

        // r is the rotation value

        // Compute z as degrees, compute last point in the circle.
        z = ((SAMPLES - 1) * 360) / SAMPLES;
        nx = (int) (maxMagnitude() * Math.sin(z * toRadians) );
        ny = (int) (maxMagnitude() * Math.sin((z + phase) * toRadians) );

        // draw line segments in the oval.
        for (int i = 0 ; i < SAMPLES; i++) {
            z = (i * 360) / SAMPLES;
            int dx = (int)(maxMagnitude() * Math.sin(z * toRadians));
            int dy = (int)(maxMagnitude() * Math.sin((z + phase) * toRadians));

            // draw the line, rotate about the origin 'r' degrees
            g.drawLine(X(nx, ny, r)+x, Y(nx, ny, r)+y,
                       X(dx, dy, r)+x, Y(dx, dy, r)+y);
            nx = dx;
            ny = dy;
        }
    }

    /**
     * Paint the rings. This is the paint method, we double buffer it
     * with update below. Draws each electron ring and electron.
     */
    public void paint(Graphics g) {
        int x = myWidth/2;
        int y = myHeight/2;
        g.setColor(bgColor);
        g.fillRect(0, 0, myWidth, myHeight);
        for (int i = 0; i < epos.length; i++) {
            double o = ((180 * i) / epos.length) + angle;

            g.setColor(ringColor);
            drawOval(g, x, y, o);
            g.setColor(eColor);
            drawElectron(g, x, y, i, o);
        }

    }

    // Double buffering offscreen image.
    Image altImage;
    Graphics offscreen;

    /**
     * Update the screen. Calls paint on the offscreen bitmap and
     * then redraws altImage.
     */
    public void update(Graphics g) {

        // If we're new or resized, create new bitmap
        if ((offscreen == null) ||
             (size().width != myWidth) ||
             (size().height != myHeight)) {
            myWidth = size().width;
            myHeight = size().height;
            altImage = createImage(myWidth, myHeight);
            offscreen = altImage.getGraphics();
        }

        // draw ourselves on to the screen.
        paint(offscreen);
        g.drawImage(altImage, 0, 0, null);
    }

    /*
     * Total boiler plate thread start up and stop code.
     */

    Thread updater = null;
    public void start() {
        updater = new Thread(this);
        updater.start();
    }

    public void stop() {
        updater = null;
    }

    /**
     * Our run method. Every click we move the electrons, every
     * fifth tick we rotate the rings.
     */

    public void run() {
        int rotater = 0;
        while (Thread.currentThread() == updater) {
            rotater = (rotater + 1) % 5;
            if (rotater == 0) {
                angle = angle + rotIncrement;
                angle = (angle > 360) ? (angle - 360) : angle;
            }
            repaint();
            try { Thread.sleep(50); } catch (InterruptedException e) { break; }
        }

    }

}
