import java.applet.*; 
import java.awt.*; 
import java.awt.image.*; 
import java.awt.event.*; 
import java.io.*; 
import java.net.*; 
import java.text.*; 
import java.util.*; 
import java.util.zip.*; 

public class Spirograph extends BApplet {
int ITERATIONS = 500;

// Used in calculating the Hypotrochoid
int R, O, r;
float step;

MovableBox centerBox;
MovableBox offsetBox;
MovableBox majorBox;

void setup()
{
  size( 400, 400 );
  
  centerBox = new MovableBox( width - 100, height/2, 10, 20 );
  PointTransform linearTrans = new ColinearPointTransform( 0, height/2 );
  centerBox.setPointTransform( linearTrans );
  Point p = centerBox.getPoint();
  offsetBox = new MovableBox( p.x, p.y - 20, 10, 20 );
  majorBox = new MovableBox( height/3, width/3, 10, 20 );

  updateParams();

  BFont theFont = loadFont( "fonts/OCR-A.vlw.gz" );
  setFont( theFont, 15 );  
  background( 0xffEEEEEE );
}

void loop()
{
  updateParams();
  // Draw the main spirograph circle
  ellipseMode( CENTER_RADIUS );
  fill( 0xffE0E0E0 );
  stroke( 0xff202020 );
  int radius = getMajorRadius();
  ellipse( width/2, height/2 , radius, radius );

  radius = getMinorRadius();
  noFill();
  ellipse( centerBox.x, centerBox.y, radius, radius );

  line( offsetBox.x, offsetBox.y, centerBox.x, centerBox.y );

  stroke( 0xff404080 );
  drawSpirograph();

  fill( 0xffEE2222 );
  stroke( 0xff000000 );
  centerBox.display();
  offsetBox.display();
  majorBox.display();
  drawParameters();
}

void mousePressed()
{
  if( centerBox.hit() )
    centerBox.clicked();
  else if( offsetBox.hit() )
    offsetBox.clicked();
  else if( majorBox.hit() )
    majorBox.clicked();
}

void mouseReleased()
{
  centerBox.released();
  offsetBox.released();
  majorBox.released();
}

/**
 * Updates the R, O, and r parameters.
 **/
void updateParams()
{
  R = getMajorRadius();
  O = getOffset();
  r = getMinorRadius();
  if( r == 0 ) r = 1;
  if( R+r+O > 0 )
    step = 5.0f/(R+r+O);
  else
    step = 5;
}

/**
 * Get the R parameter.
 **/
int getMajorRadius()
{
  return majorBox.getPoint().distanceTo( new Point( width/2, height/2 ));
}

/**
 * Get the r parameter.
 **/
int getMinorRadius()
{
  int myR = getMajorRadius();
  int rightEdgeMajor = width - (width/2 - myR);
  return abs(rightEdgeMajor - centerBox.x);
}

int getOffset()
{
  return (centerBox.getPoint().distanceTo( offsetBox.getPoint() )) - getMinorRadius();
}

void drawParameters()
{
  fill( 0xff000000 );
  text( "R: " + R, 11, 21 );
  text( "r: " + r, 11, 41 );
  text( "O: " + O, 11, 61 );
  fill( 0xffEE2222 );
  text( "R: " + R, 10, 20 );
  text( "r: " + r, 10, 40 );
  text( "O: " + O, 10, 60 );
}

void drawSpirograph()
{
  push();
  translate( width/2, height/2 );
  int oldX = 0, oldY =0 , x = 0, y = 0;

  for( int i=0; i < ITERATIONS; i++ )
  {
    float t = i * step;
    oldX = x;
    oldY = y;
    x = (int)( (R+r)*cos(t) - (r+O)*cos(((R+r)/r)*t) );
    y = (int)( (R+r)*sin(t) - (r+O)*sin(((R+r)/r)*t) );

    if( i > 0 )
      line(oldX, oldY, x, y);
  }
  pop();
}

class MovableBox
{
  int x, y;
  int normRadius, hitRadius;
  boolean isClicked;
  PointTransform myPtTransform;

  MovableBox( int anX, int aY, int normWidth, int hitWidth )
  {
    this.x = anX;
    this.y = aY;
    this.normRadius = (int)(normWidth/2);
    this.hitRadius = (int)(hitWidth/2);
  }

  void setPointTransform( PointTransform pt )
  {
    this.myPtTransform = pt;
  }

  /**
   * Returns a clone of the current point.  Mutations to the returned
   * point will not change the state of this object.
   **/
  Point getPoint()
  {
    return new Point( this.x, this.y );
  }

  void display()
  {
    if( this.isClicked )
    {
      int adjMouseX = bound( mouseX, 0, width );
      int adjMouseY = bound( mouseY, 0, height );
      
      if( this.myPtTransform == null )
      {
        this.x = adjMouseX;
        this.y = adjMouseY;      
      }
      else
      {
        Point p = new Point( adjMouseX, adjMouseY );
        Point p2 = this.myPtTransform.transform( p );
        this.x = p2.x;
        this.y = p2.y;
      }
    }

   if( this.hit() )
   {
     rectMode( CENTER_DIAMETER );
     rect( this.x, this.y, this.hitRadius, this.hitRadius );
   }
   else
   {
     rectMode( CENTER_DIAMETER );
     rect( this.x, this.y, this.normRadius, this.normRadius );
   }
  }

 boolean hit()
 {
   if( mouseX < (this.x + this.hitRadius)
   && mouseX > (this.x - this.hitRadius)
   && mouseY < (this.y + this.hitRadius)
   && mouseY > (this.y - this.hitRadius) )
   {
      return true;
   }
   else
   {
      return false;
   }
 }

  void clicked()
  {
    this.isClicked = true;
  }
  
  void released()
  {
    this.isClicked = false;
  }

}

/**
 * A Point struct allows for points to be passed by reference.
 **/
class Point
{
  int x, y;
  Point( int anX, int aY )
  {
    this.x = anX;
    this.y = aY;
  }
  
  int distanceTo( Point p )
  {
      int xt = p.x - this.x;
      int yt = p.y - this.y;
      int sum = xt*xt + yt*yt;
      return (int)( sqrt( sum ));
  }
}

abstract class PointTransform
{
  abstract Point transform( Point p );
}

/**
 * Transforms the given point to the closest possible colinear point
 * based on the x coordinate of the given point.
 **/
class ColinearPointTransform extends PointTransform
{
  float m;
  int b;
  
  /**
   * Construct an instance using slope intercept formula of a line, e.g. 
   * y = m * x + b.
   **/
  ColinearPointTransform( float aSlope, int aYIntercept )
  {
    this.m = aSlope;
    this.b = aYIntercept;
  }

  Point transform( Point p )
  {
    int x = p.x;
    int y = (int)(m * x) + b;
    return new Point( x, y );
  }
}

/**
 * Returns the value if it is withing the given min/max, otherwise 
 * returns the min if the value is less than the min, or the max if
 * it is greater than the max.
 **/
int bound( int value, int min, int max )
{
  if( value < min )
    return min;
  else if( value > max )
    return max;
  else
    return value;
}

}