/*
 * @(#)SpiroAnim.java   1.0 97/11/04 Jos van den Oever
 *
 * based on: <A HREF=http://www.wordsmith.org/anu/java/spirograph.html>Spiro.java</A> by Anu Garg
 *
 * Copyright (c) 1997 Jos van den Oever
 *
 */

import java.awt.AWTEvent;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Window;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;

/**
 * SpiroAnim - a class that produces an animated spirograph.
 *
 * SpiroAnim is an ideal component for entertaining users waiting for
 * downloading applications. You can add it to any Container and an animation of a
 * spirograph starts. It's thread has minimum priority. You can configure the
 * component to stop and start or to reset when clicked.
 * <P>
 * If you like this class, feel free to use it. I'd like to hear how you like
 * it, so don't hesitate to inform me if you use it.
 * <P>
 * @author <A HREF="mailto:Jos.vandenOever@fenk.wag-ur.nl">Jos van den Oever</A>
 * @version 1.00
 */
public class SpiroAnim extends Canvas implements Runnable, WindowListener {

    private int I = 100;                                 // Number of Iterations

    private float Rvar = 0.03f, rvar = 0.03f, ovar = 0.03f, // variation in radii
                  R, r, O,                               // radii per frame
                  Rnew, rnew, Onew,                      // new radii
                  Rold, rold, Oold;                      // old radii

    private int cvar=200,                                // variation in color
                svar = 10,                               // variation in #frames
                redBits, blueBits, greenBits,            // current color
                rednew, greennew, bluenew,               // new colors
                redold, blueold, greenold,               // old colors
                step,                                    // current frame
                steps;                                   // # of frames

    private Thread t;                                    // the animation thread

    private boolean go,       // if paint has drawn new frame, go == true
                    mouseUpStopsAnimation = true,
                              // this boolean determines wheter mouseUp stops
                              // the animation or reset it.
                    stopped = false;

    private Image osi;        // yes, the double buffering image again

    /**
    * Constructs a SpiroAnim with 100 lines per frame.
    */
    public SpiroAnim() {
        this(100);
    }

    /**
    * Constructs a SpiroAnim with 100 lines per frame.
    * @param mouseUpStopsAnimation if true the animation stops when clicked else resets when clicked
    */
    public SpiroAnim(boolean mouseUpStopsAnimation) {
        this(100, mouseUpStopsAnimation);
    }

    /**
    * Constructs a SpiroAnim with the specified number of lines per frame.
    * @param I the number of lines per frame
    */
    public SpiroAnim(int I) {
        this(I, true);
    }

    /**
    * Constructs a SpiroAnim with the specified number of lines per frame.
    * @param I the number of lines per frame
    * @param mouseUpStopsAnimation if true the animation stops when clicked else resets when clicked
    */
    public SpiroAnim(int I, boolean mouseUpStopsAnimation) {
        super();
        this.I = (I > 0) ? I : this.I;
        this.mouseUpStopsAnimation = mouseUpStopsAnimation;
        reset();
        enableEvents(AWTEvent.COMPONENT_EVENT_MASK);
        enableEvents(AWTEvent.MOUSE_EVENT_MASK);
    }

    /**
    * Starts the animation.
    */
    public void start() {
        if (t == null && !stopped) {
            t = new Thread(this);
            t.setPriority(Thread.MIN_PRIORITY);
            t.start();
        }
    }

    /**
    * Stops the animation.
    */
    public void stop() {
        if (t != null) {
            t.stop();
            t = null;
        }
    }

    /**
    * Called when the animation is started. Shouldn't be called by user.
    */
    public void run() {
        while (true) {
            try {
                t.sleep(40);  // 25 frames per second at maximum speed
            } catch (InterruptedException e) {
                System.out.println(e);
            }
            if (go && isShowing()) {
                nextFrame();
                repaint();
            }
        }
    }

    private void nextFrame() {
        if (step == 0) {

            Rold = Rnew;
            rold = rnew;
            Oold = Onew;

            redold = rednew;
            blueold = bluenew;
            greenold = greennew;

            Rnew = (float)Math.max(0.1,
                                   Math.min(1,
                                            Rold+((Math.random()-0.5)*Rvar)));
            rnew = (float)Math.max(0.1,
                                   Math.min(1,
                                            rold+((Math.random()-0.5)*rvar)));
            Onew = (float)Math.max(0,
                                   Math.min(1,
                                            Oold+((Math.random()-0.5)*rvar)));

            rednew = Math.max(0,
                              Math.min(255,
                                       redold+(int)((Math.random()-0.5)*cvar)));
            bluenew = Math.max(0,
                              Math.min(255,
                                      blueold+(int)((Math.random()-0.5)*cvar)));
            greennew = Math.max(0,
                              Math.min(255,
                                     greenold+(int)((Math.random()-0.5)*cvar)));
            steps = Math.max(50,
                             Math.min(100,
                                      steps+(int)((Math.random()-0.5)*svar)));
        }

        R = Rold + (Rnew-Rold)*step/steps;
        r = rold + (rnew-rold)*step/steps;
        O = Oold + (Onew-Oold)*step/steps;

        redBits = redold + (rednew-redold)*step/steps;
        blueBits = blueold + (bluenew-blueold)*step/steps;
        greenBits = greenold + (greennew-greenold)*step/steps;
        go = false;
        step++;
        if (step == steps) {
            step = 0;
        }
    }

    /**
    * Update method, calls paint().
    * @param g the graphics object on which is to be drawn
    */
    public void update(Graphics g) {
        paint(g);
    }

    /**
    * The paint method draws the spirograph using double buffering.
    * @param g the graphics object on which is to be drawn
    */
    public void paint(Graphics g) {
        if(osi == null) {
            osi = createImage(getSize().width, getSize().height);
        }
        Graphics osg = osi.getGraphics();
        osg.setColor(getBackground());
        osg.fillRect(0,0,getSize().width, getSize().height);
        osg.setColor(g.getColor());
        osg.setClip(0, 0, getSize().width, getSize().height);

        int x = 0, xpos = getSize().width/2;
        int y = 0, ypos = getSize().height/2;
        int prevx = 0;
        int prevy = 0;
        int t;

        float scalex = getSize().width / (2*(R + O + 2 * r));
        float scaley = getSize().height/ (2*(R + O + 2 * r));

        osg.setColor(new Color(redBits, greenBits, blueBits));

        for(t=0; t<=I; t++){
            prevx = x;
            prevy = y;
            x = (int)(scalex*((R+r)*Math.cos((double)t)
                - (r+O)*Math.cos(((R+r)/r)*t)));
            y = (int)(scaley*((R+r)*Math.sin((double)t)
                - (r+O)*Math.sin(((R+r)/r)*t)));

            if (t > 0){
                osg.drawLine(prevx+xpos, prevy+ypos,
                           x+xpos, y+ypos);
            }
        }
        go = true;
        g.drawImage(osi, 0, 0, null);
        osg.dispose();
    }

    /**
    * Resets the animation.
    */

    public void reset() {
        Rnew = (float)Math.random();
        rnew = (float)Math.random();
        Onew = (float)Math.random();
        rednew = (int)(Math.random()*255);
        greennew = (int)(Math.random()*255);
        bluenew = (int)(Math.random()*255);
        nextFrame();
        repaint();
        step = 0;
    }

    /**
    * Invalidate the component. Shouldn't be called by user.
    */
    public void invalidate() {
        super.invalidate();
        osi = null;
        stop();
    }

    /**
    * Validate the component. Is called by the system before the component is painted,
    * if it has been invalidated. Shouldn't be called by user.
    */
    public void validate() {
        super.validate();
        start();
    }

    /**
    * Notifies the component it has been added to a Container. Shouldn't be called by user.
    */
    public void addNotify() {
        super.addNotify();
        try { // in Hotjava a securityerror can occur.
            Container p = getParent();
            while (p != null && !(p instanceof Window)) {
                p = p.getParent();
            }
            if (p instanceof Window) {
                ((Window)p).addWindowListener(this);
            }
            start();
        } catch(Exception e) {
            System.out.println(e);
        }
    }

    /**
    * Notifies the component is has been removed from a component. Shouldn't be called by
    * user.
    */
    public void removeNotify() {
        super.removeNotify();
        try { // in Hotjava a securityerror can occur.
            Container p = getParent();
            while (p != null && !(p instanceof Window)) {
                p = p.getParent();
            }
            if (p instanceof Window) {
                ((Window)p).removeWindowListener(this);
            }
        } catch(Exception e) {
            System.out.println(e);
        }
        stop();
    }

    public void setBounds(int x, int y, int w, int h) {
        super.setBounds(x,y,w,h);
        validate();
    }

    /**
    * Processes ComponentEvents concerning this component. Deals with showing and hiding
    * this component. The SpiroAnim doesn't animate when hidden to save cpu time.
    * @param e The ComponentEvent to be handled.
    */
    protected void processComponentEvent(ComponentEvent e) {
        if (e.getID() == ComponentEvent.COMPONENT_HIDDEN) {
            stop();
        } else if (e.getID() == ComponentEvent.COMPONENT_SHOWN) {
            start();
        }
    }

    /**
    * Processes MouseEvents concerning this component. Deals with mouse clicking on this
    * component. The animation starts/stops or resets on mouse click according to
    * configuration.
    * @param e The MouseEvent to be handled.
    */
    protected void processMouseEvent(MouseEvent e) {
        if (e.getID() == MouseEvent.MOUSE_CLICKED) {
            if (mouseUpStopsAnimation) {
                if (stopped) {
                    stopped = false;
                    start();
                } else {
                    stopped = true;
                    stop();
                }
            } else {
                reset();
            }
        }
    }

    /**
    * Unused WindowListener interface function.
    * @param e The WindowEvent to be handled.
    */
    public void windowOpened(WindowEvent e) {
    }

    /**
    * Unused WindowListener interface function.
    * @param e The WindowEvent to be handled.
    */
    public void windowClosing(WindowEvent e) {
    }

    /**
    * Unused WindowListener interface function.
    * @param e The WindowEvent to be handled.
    */
    public void windowClosed(WindowEvent e) {
    }

    /**
    * Function handles iconification of parent window to stop animation.
    * This save cpu time.
    * @param e The WindowEvent to be handled.
    */
    public void windowIconified(WindowEvent e) {
        stop();
    }

    /**
    * Function handles deiconification of parent window to start animation.
    * @param e The WindowEvent to be handled.
    */
    public void windowDeiconified(WindowEvent e) {
        start();
    }

    /**
    * Unused WindowListener interface function.
    * @param e The WindowEvent to be handled.
    */
    public void windowActivated(WindowEvent e) {
    }

    /**
    * Unused WindowListener interface function.
    * @param e The WindowEvent to be handled.
    */
    public void windowDeactivated(WindowEvent e) {
    }
}

