/* 
'Murray Dragons'
    or what happens to the curlicue fractal 
    (which is a discretisation of the equations of the Cornu spiral/Fresnel spiral/Euler's spiral/the clothoid) 
    when you animate it.        
    
    
   This version © Fergus Crawshay Murray, January 2000.
   Any comments or suggestions to dragons@oolong.co.uk.  */

import java.awt.*;
import java.applet.Applet;

public class Dragoric extends java.applet.Applet implements Runnable {

	// Main entry point when running standalone
	public static void main(String[] args) {
		Dragoric applet = new Dragoric();
		applet.isStandalone = true;
		Frame frame = new Frame();
		frame.addWindowListener(new java.awt.event.WindowAdapter() {
			public void windowClosing(java.awt.event.WindowEvent e) {
				Frame f = (Frame) e.getSource();
				f.setVisible(false);
				f.dispose();
				System.exit(0);
			}
		});
		frame.setTitle("Applet Frame");
		frame.add( applet, BorderLayout.CENTER );
		applet.init();
		applet.start();
		frame.setSize( 768, 576 );
		Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
		frame.setLocation( (d.width - frame.getSize().width) / 2,
			(d.height - frame.getSize().height) / 2);
		frame.setVisible( true );
	}
	
    
/*  Notes on variables:
    count is incremented each step, reset each frame;
    number is incremented each 'frame';
    idx is used by the sine and cosine lookup functions;
    i is used in setting up the sine/cosine & colour lookup tables;
    x and y are integer approximations of the true coordinates of the 'turtle';
    ox and oy are x and y from the previous frame;
    drawer represents the kind of 'paintbrush' used - 0 means lines, n (!=0) means squares of size n;
    xcentre and ycentre are pretty obvious;
    symmetry, unsurprisingly, represents the kind of symmetry imposed;
    direction says whether the shape is going in 'reverse' or not;
    realx and realy are the true, floating-point values of the turtle coordinates;
    f represents the angle of the turtle;
    d is the rate of change of f;
    seed is the rate of change of d, the main crucial parameter in determining the shape;
    hue represents the hue, used when creating the colour lookup table;
    sineArray[] holds the sine lookup table (also used for cosine);
    colours[] holds all the colours;
    clearNow is true if it's time to clear the frame;
    leng is the length of 'dragon' calculated each frame;
    prop is the length actually drawn (which part is drawn shifts each frame);
    rate indicates the rate of change of the 'dragon';
    zoom is, of course, the level of zoom;
    ecce is the 'eccentricity' - the distance from the centre each 'dragon' starts at;
    wipe is the number of frames between automatic wipes (-1 means automatic wiping is turned off);
    tillWipe is the number of frames until the next automatic wipe.
    
    */

	boolean isStandalone=false;
    int count, number=0, idx, i,
    x,y,ox,oy, drawer,
    xcentre=250,ycentre=250, symmetry=1, direction=1;
    double f,realx,realy,d,seed=0.0001;
    float hue=0;
    double sineArray[]=new double[257];
    Color colours[]=new Color[256];
    public int leng=72,prop=64,rate=100,zoom=4,ecce=0, wipe, tillWipe;
    boolean clearNow=true, paused=false, watch=false;
    public Scrollbar lengBar,propBar,rateBar,zoomBar,ecceBar,wipeBar;
    public Label wipeLabel;
    public TextField inputBox;
    public TextArea paramArea;
    public CheckboxGroup drawType, symmGroup;
    public Checkbox reverseBox, pauseBox, watchBox;
    String theBox,symmName, moved, stepOrSteps;    
    public ReportFrame reportFrame;    

public void init() {
    
    //  Create lookup tables for colour and trig functions.
    
    for (i=0; i<colours.length; i++){
        colours[i]=
        Color.getHSBColor(hue,(float)1.0,(float)1.0);
        hue+=0.0036;
    }
    for (i=0; i<sineArray.length;i++){
        sineArray[i]=Math.sin(i*Math.PI/128);
    }
    setBackground (Color.black);
    setLayout(new BorderLayout());
    Panel eastPanel = new Panel();
    add("East", eastPanel);
    eastPanel.setBackground (Color.black);
    eastPanel.setLayout(new GridLayout(34,1));
    inputBox=new TextField();
    eastPanel.setForeground (Color.white);
    eastPanel.add(new Label("Seed"));
    eastPanel.setBackground (Color.white);
    eastPanel.setForeground (Color.black);    
    eastPanel.add(inputBox);
    eastPanel.setForeground (Color.black);
    Button seedButton = new Button ("Input seed");
    Button reseedButton = new Button ("Random seed");
    Button wipeButton = new Button ("Wipe");
    eastPanel.add(seedButton);
    eastPanel.add(reseedButton);
    eastPanel.add(wipeButton);
    eastPanel.setBackground (Color.black);
    eastPanel.add(new Panel());
    eastPanel.setForeground (Color.white);
    eastPanel.add(new Label("Length"));
    lengBar=new Scrollbar (Scrollbar.HORIZONTAL,144,4,1,5555);
    eastPanel.add(lengBar);
    eastPanel.add(new Label("Length visible"));
    propBar=new Scrollbar (Scrollbar.HORIZONTAL,5555,4,1,5555);
    eastPanel.add(propBar);
    eastPanel.add(new Label("Rate of curl"));    
    rateBar=new Scrollbar (Scrollbar.HORIZONTAL,768,4,1,10000);
    eastPanel.add(rateBar);
    eastPanel.add(new Label("Zoom"));    
    zoomBar=new Scrollbar (Scrollbar.HORIZONTAL,8,4,1,22);    
    eastPanel.add(zoomBar);
    eastPanel.add(new Label("Eccentricity"));
    ecceBar=new Scrollbar (Scrollbar.HORIZONTAL,0,4,0,300);
    eastPanel.add(ecceBar);
    wipeLabel=new Label("Wipe");
    wipeBar=new Scrollbar (Scrollbar.HORIZONTAL,1000,4,1,1000);
    eastPanel.add(wipeLabel);
    eastPanel.add(wipeBar);
    eastPanel.add(new Panel());
    
    drawType=new CheckboxGroup ();
    eastPanel.add (new Checkbox("Lines", drawType, false));
    eastPanel.add (new Checkbox("Dots", drawType, false));
    eastPanel.add (new Checkbox("2x2 squares", drawType, true));    
    eastPanel.add (new Checkbox("3x3 squares", drawType, false));
    eastPanel.add(new Panel());
    eastPanel.add(new Label("Symmetry:"));
    symmGroup=new CheckboxGroup ();
    eastPanel.add (new Checkbox("1 R", symmGroup, true));
    eastPanel.add (new Checkbox("2 R", symmGroup, false));
    eastPanel.add (new Checkbox("1 R, 1 M", symmGroup, false));
    eastPanel.add (new Checkbox("1 R, 2 M", symmGroup, false));
    eastPanel.add(new Panel());
    reverseBox=new Checkbox("Reverse", null, false);
    eastPanel.add (reverseBox);
    pauseBox=new Checkbox("Pause", null, false);
    eastPanel.add (pauseBox);
    watchBox=new Checkbox("Catch repeats", null, false);
    eastPanel.add (watchBox);
    
    reportFrame = new ReportFrame ();
    reportFrame.setLayout(new BorderLayout());
    paramArea = new TextArea (10,20);
    reportFrame.add ("Center",paramArea);    
    reportFrame.resize (400,200);
    
    scrollUpdate();
    boxUpdate();
    }    

// Deal with any button presses.

public boolean action (Event evt, Object arg) {
    if (evt.target instanceof Button){
        String bname=(String)arg;
        if (bname.equals("Random seed")) { 
            clearNow = true;
            inputBox.setText(Double.toString(Math.random()));            
            seed=( (Double.valueOf (inputBox.getText() ) ).doubleValue() )*(2*Math.PI);
        }                    
        if (bname.equals("Input seed")){
            clearNow = true;
            seed=( (Double.valueOf (inputBox.getText() ) ).doubleValue() )*(2*Math.PI);
        }
        if (bname.equals("Wipe")) clearNow = true;
        return true;
    }
    else return false;        
}

// Check if the Enter key has been pressed.

public boolean keyDown (Event evt, int key){
    if (key==Event.ENTER)     {        
        clearNow = true;
        seed = ( (Double.valueOf (inputBox.getText() ) ).doubleValue() )*(2*Math.PI);
        return true;
    }
    else return false;
}

// Check for other user-interface events.

public boolean handleEvent (Event evt) {
    if (evt.target instanceof Scrollbar) {
        scrollUpdate();
        return true;
    }
    else if (evt.id==Event.MOUSE_DOWN||evt.id==Event.MOUSE_DRAG){
        xcentre=evt.x;
        ycentre=evt.y;
        return true;
    }
    else if (evt.target instanceof Checkbox){
        boxUpdate();
        return true;
    }    
    return super.handleEvent(evt);
}

// Update values associated with scrollbars.

public void scrollUpdate(){
    leng = lengBar.getValue();
    prop = propBar.getValue();
    rate = rateBar.getValue();
    zoom = zoomBar.getValue();
    ecce = ecceBar.getValue();    
    wipe = wipeBar.getValue();
    tillWipe = wipe;
    if (wipe>995) {
        wipeLabel.setText("Wipe: never.");
        wipe=-1;
    }
    else wipeLabel.setText("Wipe every: " + wipeBar.getValue() + " frames");
}

// Update values associated with boxes.

public void boxUpdate(){
    theBox=drawType.getCurrent().getLabel();
    if(theBox=="Lines") drawer=0;
    if(theBox=="Dots") drawer=1;
    if(theBox=="2x2 squares") drawer=2;
    if(theBox=="3x3 squares") drawer=3;
    symmName=symmGroup.getCurrent().getLabel();
    if(symmName=="1 R") symmetry=1;
    if(symmName=="2 R") symmetry=2;
    if(symmName=="1 R, 1 M") symmetry=3;
    if(symmName=="1 R, 2 M") symmetry=4;
    if(reverseBox.getState()==true) direction=-1;
    if(reverseBox.getState()==false) direction=1;
    paused=pauseBox.getState();
    watch=watchBox.getState();
}

// Starts a thread for the animation.

Thread dragonThread;        
public void start() {
    if (dragonThread == null) {
        dragonThread = new Thread(this, "dragon");
        dragonThread.start();
        }
}

// Defines the basic behaviour of the applet.
    
public void run() {
    while (dragonThread != null) {
        if (!paused){
        repaint();
        number+=1;}
        try {
        dragonThread.sleep(10);
        } catch (InterruptedException e){        }
    }
}

// The real substance of the program is contained here, in the paint method.
    
public void paint (Graphics drago) {
  if(!paused){
    if (tillWipe==0) {    
        clearNow=true;   
        tillWipe=wipe;
    }
    tillWipe-=1;
    if (clearNow){
        drago.setColor(Color.black);
        drago.fillRect(0,0,1280,1024);
        clearNow=false;
    }
    seed=seed+0.00000004*rate*direction;
    count=0;
    ox=ecce;
    oy=ecce;
    x=ox;
    y=oy;
    realx=ecce;
    realy=ecce;
    f=0;
    d=0;    
        while (count<leng){
            count+=1;
            f+=d;
            d+=seed;
            if (f>Math.PI*2) f-=Math.PI*2;
            if (d>Math.PI*2) d-=Math.PI*2;
            if (f<0) f+=Math.PI*2;
            if (d<0) d+=Math.PI*2;
            realx+=zoom*(cosine(f));
            realy+=zoom*(sine(f));
            if (Math.abs(f)<0.0001 && Math.abs(d)<0.0001 && watch) {
                paused=true;
                report();
                break;
            }
            x=(int)realx;
            y=(int)realy;
            drago.setColor(colours[(count+number)%255]);

            if ((count+number)%leng<prop){                   
                    if (symmetry==1){
                        draw(xcentre+x,ycentre+y,xcentre+ox,ycentre+oy,drawer,drago);
                        draw(xcentre-x,ycentre-y,xcentre-ox,ycentre-oy,drawer,drago);
                    }
                    if (symmetry==2){
                        draw(xcentre+x,ycentre+y,xcentre+ox,ycentre+oy,drawer,drago);
                        draw(xcentre-x,ycentre-y,xcentre-ox,ycentre-oy,drawer,drago);
                        draw(xcentre-y,ycentre+x,xcentre-oy,ycentre+ox,drawer,drago);
                        draw(xcentre+y,ycentre-x,xcentre+oy,ycentre-ox,drawer,drago);
                    }
                    if (symmetry==3){
                        draw(xcentre+x,ycentre+y,xcentre+ox,ycentre+oy,drawer,drago);
                        draw(xcentre-x,ycentre-y,xcentre-ox,ycentre-oy,drawer,drago);
                        draw(xcentre-x,ycentre+y,xcentre-ox,ycentre+oy,drawer,drago);
                        draw(xcentre+x,ycentre-y,xcentre+ox,ycentre-oy,drawer,drago);                            
                    }
                    if (symmetry==4){
                        draw(xcentre+x,ycentre+y,xcentre+ox,ycentre+oy,drawer,drago);
                        draw(xcentre-x,ycentre-y,xcentre-ox,ycentre-oy,drawer,drago);
                        draw(xcentre-y,ycentre+x,xcentre-oy,ycentre+ox,drawer,drago);
                        draw(xcentre+y,ycentre-x,xcentre+oy,ycentre-ox,drawer,drago);
                        
                        draw(xcentre+x,ycentre-y,xcentre+ox,ycentre-oy,drawer,drago);
                        draw(xcentre-x,ycentre+y,xcentre-ox,ycentre+oy,drawer,drago);
                        draw(xcentre-y,ycentre-x,xcentre-oy,ycentre-ox,drawer,drago);
                        draw(xcentre+y,ycentre+x,xcentre+oy,ycentre+ox,drawer,drago);                        
                    }                    
            }            
            ox=x;
            oy=y;

        }
  }
}

// Draw a line or a square, as appropriate.

public void draw (int x,int y,int ox,int oy,int drawer,Graphics g) {
    if (drawer==0)     g.drawLine (x,y,ox,oy);
    else    {    
        g.fillRect (x,y,drawer,drawer);
    }
}

public synchronized void report () {
    pauseBox.setState(true);
    boxUpdate();
    if (count==1) stepOrSteps=" step,\n";
    else stepOrSteps=" steps,\n";
    if (realx==ecce && realy==ecce) moved = " having returned to the starting point, ";
    else moved = " having shifted "+(realx-ecce)+" pixels horizontally \nand "+
                 (realy-ecce)+" pixels vertically, ";
    paramArea.setText("Dragon seeded with " + (seed/(Math.PI*2)) +
                      "\nrepeated after " + count + stepOrSteps +
                      moved + 
                      "\nat zoom level " + zoom);
    reportFrame.show(); 
}
                      
// The update method is over-written so that the drawing area is not cleared every frame.

public void update (Graphics g) {
    paint (g);
}

public double sine (double angle){
    idx=((int)(256+angle*128/Math.PI))%256;
    return sineArray[idx];
}

public double cosine (double angle){
    idx=((int)(192+angle*128/Math.PI))%256;
    return sineArray[idx];
}

public void stop() {
    dragonThread.stop();
    dragonThread = null;
    }
}


