
/*------------------------------------------------------------------------*
 * Originally implemented by Scott Flinn, in association with the
 * Imager Graphics Laboratory at the University of British Columbia
 * (scottflinn@alumni.uwaterloo.ca).
 *
 * This source code may be freely distributed and modified for any purpose
 * as long as these introductory comments are not removed.  Please be
 * aware that this represents the author's initial experiments with the
 * Java platform and should not necessarily be considered good examples
 * of Java programming.
 *------------------------------------------------------------------------*/

/*------------------------------------------------------------------------*
 *  The NC class implements the animated Necker Cube illusion.  It is
 *    meant to be run in a separate window using AppletStarter.
 *------------------------------------------------------------------------*/

import java.awt.*;

/*------------------------------  Class NC  ------------------------------*/

public class NC extends WindowApplet
{
  NCPanel     canvas;
  NCControls  controls;
  Button      dismiss;

    /*-----------------------  constructor  ------------------------*/

    NC()
    {
      GridBagLayout       gridbag;
      GridBagConstraints  c;

        // Create the layout manager and a constraint object
        gridbag = new GridBagLayout();
        c = new GridBagConstraints();
        setLayout( gridbag );

        // Create the canvas panel and put it on top
        canvas = new NCPanel();
        c.gridwidth = GridBagConstraints.REMAINDER;
        c.fill = GridBagConstraints.BOTH;
        c.weightx = 1.0;
        c.weighty = 1.0;
        gridbag.setConstraints( canvas, c );
        add( canvas );

        // Create the control panel and put it second from the bottom
        controls = new NCControls( canvas );
        c.fill = GridBagConstraints.HORIZONTAL;
        c.weighty = 0.0;
        gridbag.setConstraints( controls, c );
        add( controls );

        // Give the canvas access to the angle slider control
        canvas.angleSlider = controls.angleSlider;

        // Create the dismiss button and put it at the bottom
        dismiss = new Button( "Dismiss" );
        gridbag.setConstraints( dismiss, c );
        add( dismiss );
        validate();
    }

    /*--------------------------  action  --------------------------*/

    public boolean action( Event event, Object obj )
    {
        if ( event.target == dismiss )
        {
            canvas.stopMotor();
            controls.motorRunning( false );
            hide();
            return true;
        }
        return false;
    }
}

/*---------------------------  Class NCPanel  ----------------------------*/

class NCPanel extends Panel implements Runnable
{
  // Cube viewport in world coordinates
  private Rectangle  viewWindow = new Rectangle( -2, -2, 4, 4 );

  // Conversion constant
  private final double  radiansPerDegree = Math.atan( 1.0 ) / 45.0;

  // Default tilt, rotation angle and motor switch
  public  final int        defaultTilt = 35;
  private       double     tilt = (double)defaultTilt * radiansPerDegree;
  private       double     cosTilt = Math.cos( tilt );
  private       double     sinTilt = Math.sin( tilt );
  private       double     dtheta;
  private       double     theta = 0.0;
  private       double     cosTheta = Math.cos( theta );
  private       double     sinTheta = Math.sin( theta );
  private       boolean    motorOn = false;
  public        Scrollbar  angleSlider;

  // Public animation speed parameters
  public final int  minPeriod =  10;
  public final int  maxPeriod = 100;
  public       int  period    =  50;

  // View selection
  public final int  SideView  = 1;
  public final int  TopView   = 2;
  public final int  BothViews = 3;
  public       int  whichView = SideView;

  // Render mode
  public final int  WireFrame  = 1;
  public final int  FlatShade  = 2;
  public final int  WireShade  = 3;
  public       int  renderMode = WireFrame;

  // Animation thread
  private Thread  animator = null;

  // Cube model data
  Vertex[]  vertex;
  Edge[]    edge;
  Face[]    face;

  // Off-screen animation buffer and frame number
  private int        frame = 0;
  private Image      offImage;
  private Dimension  offDimension;
  private Graphics   offGraphics = null;

  // Point arrays and vectors for face polygon specification
  int  xpoints[] = new int[4];
  int  ypoints[] = new int[4];
  int  zpoints[] = new int[4];

  Vertex  p = new Vertex();
  Vertex  q = new Vertex();
  Vertex  n = new Vertex();

    /*-----------------------  constructor  ------------------------*/

    NCPanel()
    {
        // Allocate and initialize vertex array
        vertex = new Vertex[8];
        vertex[0] = new Vertex( -1.0, -1.0, -1.0 );
        vertex[1] = new Vertex( -1.0, -1.0,  1.0 );
        vertex[2] = new Vertex( -1.0,  1.0, -1.0 );
        vertex[3] = new Vertex( -1.0,  1.0,  1.0 );
        vertex[4] = new Vertex(  1.0, -1.0, -1.0 );
        vertex[5] = new Vertex(  1.0, -1.0,  1.0 );
        vertex[6] = new Vertex(  1.0,  1.0, -1.0 );
        vertex[7] = new Vertex(  1.0,  1.0,  1.0 );

        // Allocate and initialize edge array
        edge = new Edge[12];
        edge[0]  = new Edge( 0, 1 );
        edge[1]  = new Edge( 0, 2 );
        edge[2]  = new Edge( 0, 4 );
        edge[3]  = new Edge( 1, 3 );
        edge[4]  = new Edge( 1, 5 );
        edge[5]  = new Edge( 2, 3 );
        edge[6]  = new Edge( 2, 6 );
        edge[7]  = new Edge( 3, 7 );
        edge[8]  = new Edge( 4, 5 );
        edge[9]  = new Edge( 4, 6 );
        edge[10] = new Edge( 5, 7 );
        edge[11] = new Edge( 6, 7 );

        // Allocate and initialize face array
        face = new Face[6];
        face[0] = new Face( 0, 1, 3, 2 );
        face[1] = new Face( 0, 4, 5, 1 );
        face[2] = new Face( 0, 2, 6, 4 );
        face[3] = new Face( 1, 5, 7, 3 );
        face[4] = new Face( 2, 3, 7, 6 );
        face[5] = new Face( 4, 6, 7, 5 );

        // Compute initial angle increment
        dtheta = 5.0 * radiansPerDegree;
    }

    /*--------------------------  paint  ---------------------------*/

    public void paint( Graphics g )
    {
        update( g );
    }

    /*--------------------------  update  --------------------------*/

    public synchronized void update( Graphics g )
    {
      Dimension  d = size();
      int        i, j;
      Vertex     v1, v2, v3, v4;
      double     length;

        // Create off-screen image for double buffering
        if ( ( offGraphics == null ) ||
             ( d.width  != offDimension.width  ) ||
             ( d.height != offDimension.height ) )
        {
            offDimension = d;
            offImage = createImage( d.width, d.height );
            offGraphics = offImage.getGraphics();
        }

        // Draw black background
        offGraphics.setColor( Color.black );
        offGraphics.fillRect( 0, 0, d.width, d.height );

        // Adjust width dimension for split view
        if ( whichView == BothViews )
            d.width /= 2;

        if ( ( renderMode & FlatShade ) != 0 )
        {
            // Draw faces in face array
            for ( i = 0; i < 6; i++ )
            {
                // Transform vertices
                v1 = xform( vertex[face[i].v1], d );
                v2 = xform( vertex[face[i].v2], d );
                v3 = xform( vertex[face[i].v3], d );
                v4 = xform( vertex[face[i].v4], d );
                xpoints[0] = (int)( v1.x );
                ypoints[0] = (int)( v1.y );
                zpoints[0] = (int)( v1.z );
                xpoints[1] = (int)( v2.x );
                ypoints[1] = (int)( v2.y );
                zpoints[1] = (int)( v2.z );
                xpoints[2] = (int)( v3.x );
                ypoints[2] = (int)( v3.y );
                zpoints[2] = (int)( v3.z );
                xpoints[3] = (int)( v4.x );
                ypoints[3] = (int)( v4.y );
                zpoints[3] = (int)( v4.z );

                // Compute face normal
                p.x = v2.x - v1.x;
                p.y = v2.y - v1.y;
                p.z = v2.z - v1.z;
                q.x = v3.x - v2.x;
                q.y = v3.y - v2.y;
                q.z = v3.z - v2.z;

                // Reverse sign of n.y because z maps to -y
                n.x = ( p.y * q.z ) - ( p.z * q.y );
                n.y = ( p.x * q.z ) - ( p.z * q.x );
                n.z = ( p.x * q.y ) - ( p.y * q.x );
                length = 2.0 * Math.sqrt(
                    ( n.x * n.x ) + ( n.y * n.y ) + ( n.z * n.z ) );
                n.x /= length;
                n.y /= length;
                n.z /= length;

                if ( ( ( whichView & SideView ) != 0 ) && ( n.z > 0.0 ) )
                {
                    offGraphics.setColor(
                        new Color( (float)( n.z + 0.5 ),
                                   (float)0.0, (float)0.0 ) );
                    offGraphics.fillPolygon( xpoints, ypoints, 4 );
                }
                if ( whichView == BothViews )
                    for ( j = 0; j < 4; j++ )
                        xpoints[j] += (double)( d.width );
                if ( ( ( whichView & TopView ) != 0 ) && ( n.y > 0.0 ) )
                {
                    offGraphics.setColor(
                        new Color( (float)( n.y + 0.5 ),
                                   (float)0.0, (float)0.0 ) );
                    offGraphics.fillPolygon( xpoints, zpoints, 4 );
                }
            }
        }

        if ( ( renderMode & WireFrame ) != 0 )
        {
            // Draw edges in edge array
            offGraphics.setColor( Color.white );
            for ( i = 0; i < 12; i++ )
            {
                v1 = xform( vertex[edge[i].v1], d );
                v2 = xform( vertex[edge[i].v2], d );
                if ( ( whichView & SideView ) != 0 )
                    offGraphics.drawLine(
                        (int)( v1.x ), (int)( v1.y ),
                        (int)( v2.x ), (int)( v2.y ) );
                if ( whichView == BothViews )
                {
                    v1.x += (double)( d.width );
                    v2.x += (double)( d.width );
                }
                if ( ( whichView & TopView ) != 0 )
                    offGraphics.drawLine(
                        (int)( v1.x ), (int)( v1.z ),
                        (int)( v2.x ), (int)( v2.z ) );
            }
        }

        // Copy the off-screen frame to the screen
        g.drawImage( offImage, 0, 0, this );
    }

    /*--------------------------  xform  ---------------------------*/

    private Vertex xform( Vertex v, Dimension d )
    {
      double  w, x, y, z;
      double  dx, dy, vs;

        // Default rotation about the x-axis
        x = v.x;
        y = v.y * cosTilt - v.z * sinTilt;
        z = v.y * sinTilt + v.z * cosTilt;

        // Default rotation about the z-axis
        w = x;
        x = x * cosTilt - y * sinTilt;
        y = w * sinTilt + y * cosTilt;

        // Rotate by theta radians around y-axis
        w = z;
        z = z * cosTheta - x * sinTheta;
        x = w * sinTheta + x * cosTheta;

        // Keep viewport square by using smaller dimension plus offsets
        if ( d.width < d.height )
        {
            vs = (double)( d.width );
            dx = 0.0;
            dy = (double)( ( d.height - d.width ) / 2 );
        }
        else
        {
            vs = (double)( d.height );
            dx = (double)( ( d.width - d.height ) / 2 );
            dy = 0.0;
        }

        // Map window to square viewport:  v = (w-w0)(v1-v0)/(w1-w0) + v0
        // z maps to -y in top view
        return new Vertex(
            ( x - (double)(viewWindow.x) ) *
                vs / (double)(viewWindow.width) + dx,
            ( y - (double)(viewWindow.y) ) *
                vs / (double)(viewWindow.height) + dy,
            ( z - (double)(viewWindow.y) ) *
                vs / (double)(viewWindow.height) + dy
          );
    }

    /*---------------------------  run  ----------------------------*/

    public void run()
    {
      long  lastTime = System.currentTimeMillis();
      long  t;

        while ( true )
        {
            t = ( lastTime + (long)period ) - System.currentTimeMillis();
            if ( t > (long)1 )
                try {
                    animator.sleep( (int)t );
                } catch ( InterruptedException e ) {
                    System.out.println(
                        "Interrupted exception in NCPanel.run" );
                }
            lastTime = System.currentTimeMillis();
            step();
        }
    }

    /*---------------------------  step  ---------------------------*/

    public void step()
    {
      double  dt;

        // Update rotation angle
        theta = theta + dtheta;
        dt = theta / radiansPerDegree;
        if ( dt >= 360.0  )
        {
            dt -= 360.0;
            theta = dt * radiansPerDegree;
        }
        cosTheta = Math.cos( theta );
        sinTheta = Math.sin( theta );

        // Set slider value
        angleSlider.setValue( (int)dt );

        // Repaint
        repaint();
    }

    /*-----------------------  toggleMotor  ------------------------*/

    public boolean toggleMotor()
    {
        motorOn = ! motorOn;
        if ( motorOn && ( animator == null ) )
        {
            try {
                animator = new Thread( this );
                animator.start();
            } catch ( IllegalThreadStateException e ) {
                System.out.println( "Thread state exception in start" );
            }
        }
        else if ( ( ! motorOn ) && ( animator != null ) )
        {
            animator.stop();
            animator = null;
        }

        return( motorOn );
    }

    /*------------------------  stopMotor  -------------------------*/

    public void stopMotor()
    {
        if ( motorOn )
            toggleMotor();
    }

    /*-------------------------  setTheta  -------------------------*/

    public void setTheta( double theta )
    {
        this.theta = theta * radiansPerDegree;
        cosTheta = Math.cos( this.theta );
        sinTheta = Math.sin( this.theta );
        update( getGraphics() );
    }

    /*-------------------------  setTilt  -------------------------*/

    public void setTilt( double tilt )
    {
        this.tilt = tilt * radiansPerDegree;
        cosTilt = Math.cos( this.tilt );
        sinTilt = Math.sin( this.tilt );
        update( getGraphics() );
    }
}

/*----------------------------  Class Vertex  ----------------------------*/

class Vertex extends Object
{
  public double  x, y, z;

    /*-----------------------  constructors  ------------------------*/

    Vertex()
    {
        new Vertex( 0.0, 0.0, 0.0 );
    }

    Vertex( double x, double y, double z )
    {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

/*----------------------------  Class NCEdge  ----------------------------*/

class Edge extends Object
{
  public int  v1, v2;

    /*-----------------------  constructor  ------------------------*/

    Edge( int v1, int v2 )
    {
        this.v1 = v1;
        this.v2 = v2;
    }
}

/*----------------------------  Class NCFace  ----------------------------*/

class Face extends Object
{
  public int  v1, v2, v3, v4;

    /*-----------------------  constructor  ------------------------*/

    Face( int v1, int v2, int v3, int v4 )
    {
        this.v1 = v1;
        this.v2 = v2;
        this.v3 = v3;
        this.v4 = v4;
    }
}

/*--------------------------  Class NCControls  --------------------------*/

class NCControls extends Panel
{
  private Checkbox       topView, sideView, bothViews;
  private Checkbox       wireFrame, flatShade, wireShade;
  private CheckboxGroup  viewChoice, renderChoice;
  private Button         motor;
  private Panel          viewPanel, renderPanel, buttonPanel;
  private Scrollbar      speedSlider;
  private Scrollbar      tiltSlider;
  public  Scrollbar      angleSlider;
  private NCPanel        canvas;      // The main drawing canvas

    /*-----------------------  constructor  ------------------------*/

    NCControls( NCPanel canvas )
    {
      GridBagLayout       gridbag, buttonGridbag;
      GridBagConstraints  c;

        // Record id of drawing canvas
        this.canvas = canvas;

        // Use a grid bag to lay things out
        gridbag = new GridBagLayout();
        c = new GridBagConstraints();
        setLayout( gridbag );

        // Create sliders
        speedSlider =
            new Scrollbar(
                Scrollbar.HORIZONTAL,
                canvas.maxPeriod + canvas.minPeriod - canvas.period,
                ( canvas.maxPeriod - canvas.minPeriod ) / 10,
                canvas.minPeriod, canvas.maxPeriod );
        tiltSlider =
            new Scrollbar(
                Scrollbar.HORIZONTAL, canvas.defaultTilt, 9, 0, 90 );
        angleSlider =
            new Scrollbar(
                Scrollbar.HORIZONTAL, 0, 36, 0, 359 );

        // Create view selection panel
        viewChoice = new CheckboxGroup();
        sideView   = new Checkbox( "Side view", viewChoice, true );
        topView    = new Checkbox( "Top view", viewChoice, false );
        bothViews  = new Checkbox( "Both", viewChoice, false );
        viewPanel  = new Panel();
        viewPanel.setLayout( new GridLayout( 3, 1 ) );
        viewPanel.add( sideView );
        viewPanel.add( topView );
        viewPanel.add( bothViews );

        // Create render method selection panel
        renderChoice = new CheckboxGroup();
        wireFrame    = new Checkbox( "Wire frame", renderChoice, true );
        flatShade    = new Checkbox( "Flat shaded", renderChoice, false );
        wireShade    = new Checkbox( "Both", renderChoice, false );
        renderPanel  = new Panel();
        renderPanel.setLayout( new GridLayout( 3, 1 ) );
        renderPanel.add( wireFrame );
        renderPanel.add( flatShade );
        renderPanel.add( wireShade );

        // Create motor button
        motor = new Button( "Start motor" );

        // Create button panel for top row of buttons
        buttonPanel = new Panel();
        buttonGridbag = new GridBagLayout();
        buttonPanel.setLayout( buttonGridbag );
        addControl( buttonPanel, (Component)motor, 1.0,
            GridBagConstraints.NONE, 1, buttonGridbag, c );
        addControl( buttonPanel, (Component)viewPanel, 1.0,
            GridBagConstraints.NONE,
            GridBagConstraints.RELATIVE, buttonGridbag, c );
        addControl( buttonPanel, (Component)renderPanel, 1.0,
            GridBagConstraints.NONE,
            GridBagConstraints.REMAINDER, buttonGridbag, c );

        // Add components to control panel
        addControl( this, (Component)buttonPanel, 1.0,
            GridBagConstraints.HORIZONTAL,
            GridBagConstraints.REMAINDER, gridbag, c );

        addControl( this, (Component)(new Label( "Angle", Label.RIGHT )),
            0.0, GridBagConstraints.NONE, 1, gridbag, c );
        addControl( this, (Component)angleSlider, 1.0,
            GridBagConstraints.HORIZONTAL,
            GridBagConstraints.REMAINDER, gridbag, c );
        addControl( this, (Component)(new Label( "Tilt", Label.RIGHT )),
            0.0, GridBagConstraints.NONE, 1, gridbag, c );
        addControl( this, (Component)tiltSlider, 1.0,
            GridBagConstraints.HORIZONTAL,
            GridBagConstraints.REMAINDER, gridbag, c );
        addControl( this, (Component)(new Label( "Speed", Label.RIGHT )),
            0.0, GridBagConstraints.NONE, 1, gridbag, c );
        addControl( this, (Component)speedSlider, 1.0,
            GridBagConstraints.HORIZONTAL,
            GridBagConstraints.REMAINDER, gridbag, c );
    }

    /*------------------------  addControl  ------------------------*/

    private void
    addControl( Panel panel, Component component, double wx,
        int fill, int gw, GridBagLayout gb, GridBagConstraints c )
    {
        c.weightx   = (float)wx;
        c.weighty   = 1.0;
        c.fill      = fill;
        c.gridwidth = gw;
        gb.setConstraints( component, c );
        panel.add( component );
    }

    /*--------------------------  insets  --------------------------*/

    public Insets insets()
    {
        return( new Insets( 5, 5, 5, 5 ) );
    }

    /*-----------------------  motorRunning  -----------------------*/

    public void motorRunning( boolean motorOn )
    {
        if ( motorOn )
            motor.setLabel( "Stop motor" );
        else
            motor.setLabel( "Start motor" );
    }

    /*--------------------------  action  --------------------------*/

    public boolean action( Event event, Object obj )
    {
        if ( event.target == motor )
        {
            motorRunning( canvas.toggleMotor() );
            return true;
        }
        else if ( event.target == sideView )
        {
            canvas.whichView = canvas.SideView;
            canvas.repaint();
            return true;
        }
        else if ( event.target == topView )
        {
            canvas.whichView = canvas.TopView;
            canvas.repaint();
            return true;
        }
        else if ( event.target == bothViews )
        {
            canvas.whichView = canvas.BothViews;
            canvas.repaint();
            return true;
        }
        else if ( event.target == wireFrame )
        {
            canvas.renderMode = canvas.WireFrame;
            canvas.repaint();
            return true;
        }
        else if ( event.target == flatShade )
        {
            canvas.renderMode = canvas.FlatShade;
            canvas.repaint();
            return true;
        }
        else if ( event.target == wireShade )
        {
            canvas.renderMode = canvas.WireShade;
            canvas.repaint();
            return true;
        }
        else
            return false;
    }

    /*-----------------------  handleEvent  ------------------------*/

    public boolean handleEvent( Event event )
    {
        if ( event.target == speedSlider )
        {
            // simply adjust animation speed
            canvas.period =
                canvas.maxPeriod + canvas.minPeriod - speedSlider.getValue();
            return true;
        }
        else if ( event.target == tiltSlider )
        {
            // adjust default tilt of cube
            canvas.setTilt( tiltSlider.getValue() );
            return true;
        }
        else if ( event.target == angleSlider )
        {
            // adjust rotation angle
            canvas.setTheta( angleSlider.getValue() );
            return true;
        }
        else
            return super.handleEvent( event );
    }
}

/*------------------------------------------------------------------------*/

