//**************************************************************************
//*  Curve Applet by Michael Heinrichs
//*
//*  This applet allows the user to specify control points to be used for
//*  plotting curves.  Three curve types are supported: Hermite, Bezier,
//*  and B-Spline.  The user can add, delete and move control points.
//*  
//*  This applet was inspired by the DrawTest demo applet included in the
//*  1.0 beta Java Developer's Kit from Sun Microsystems.
//*  If you are interested in the algorithms used to display the curves,
//*  Please see  the book: "Computer Graphics.  Principles and Practice" by
//*  Foley, VanDam, Feiner, and Hughes.
//**************************************************************************

import java.awt.*;
import java.applet.*;

import java.util.Vector;

public class Curve extends Applet {
    public void init() {
	setLayout(new BorderLayout());
	CurvePanel dp = new CurvePanel();
	add("Center", dp);
	add("South",new CurveControls(dp));
	add("North",new CurveControls2(dp));
    }

    public boolean handleEvent(Event e) {
	switch (e.id) {
	  case Event.WINDOW_DESTROY:
	    System.exit(0);
	    return true;
	  default:
	    return false;
	}
    }

    public static void main(String args[]) {
	Frame f = new Frame("Curve");
	Curve curve = new Curve();
	curve.init();
	curve.start();

	f.add("Center", curve);
	f.show();
    }
}

class ControlPoint extends Object {
    public int x;
    public int y;
    public static final int PT_SIZE = 4;

    public ControlPoint(int a, int b) {
	x = a;
	y = b;
    }

    public boolean within(int a, int b) {
	if (a >= x - PT_SIZE && 
	    b >= y - PT_SIZE &&
	    a <= x + PT_SIZE && 
	    b <= y + PT_SIZE)
	    return true;
	else
	    return false;
    }
}

class CurvePanel extends Panel {
    public static final int HERMITE = 0;
    public static final int BEZIER = 1;
    public static final int BSPLINE = 2;
    private int	   mode = HERMITE;

    public static final int ADD = 0;
    public static final int MOVE = 1;
    public static final int DELETE = 2;
    private int	   action = ADD;

    private Vector points = new Vector(16,4);

    // If a control point is being moved, this is the index into the list
    // of the moving point.  Otherwise it contains -1
    private int moving_point;
    private int precision;
    private static float hermiteMatrix[][] = new float[4][4];
    private static float bezierMatrix[][] = new float[4][4];
    private static float bsplineMatrix[][] = new float[4][4];
    private float eMatrix[][] = new float[4][4];

    // Initialize the curve-type matrices
    static {
	hermiteMatrix[0][0] = 2;
	hermiteMatrix[0][1] = -2;
	hermiteMatrix[0][2] = 1;
	hermiteMatrix[0][3] = 1;
	hermiteMatrix[1][0] = -3;
	hermiteMatrix[1][1] = 3;
	hermiteMatrix[1][2] = -2;
	hermiteMatrix[1][3] = -1;
	hermiteMatrix[2][0] = 0;
	hermiteMatrix[2][1] = 0;
	hermiteMatrix[2][2] = 1;
	hermiteMatrix[2][3] = 0;
	hermiteMatrix[3][0] = 1;
	hermiteMatrix[3][1] = 0;
	hermiteMatrix[3][2] = 0;
	hermiteMatrix[3][3] = 0;

	bezierMatrix[0][0] = -1;
	bezierMatrix[0][1] = 3;
	bezierMatrix[0][2] = -3;
	bezierMatrix[0][3] = 1;
	bezierMatrix[1][0] = 3;
	bezierMatrix[1][1] = -6;
	bezierMatrix[1][2] = 3;
	bezierMatrix[1][3] = 0;
	bezierMatrix[2][0] = -3;
	bezierMatrix[2][1] = 3;
	bezierMatrix[2][2] = 0;
	bezierMatrix[2][3] = 0;
	bezierMatrix[3][0] = 1;
	bezierMatrix[3][1] = 0;
	bezierMatrix[3][2] = 0;
	bezierMatrix[3][3] = 0;
	
	float mult = (float)(1.0/6.0);
	bsplineMatrix[0][0] = -mult;
	bsplineMatrix[0][1] = 3 * mult;
	bsplineMatrix[0][2] = -3 * mult;
	bsplineMatrix[0][3] = mult;
	bsplineMatrix[1][0] = 3 * mult;
	bsplineMatrix[1][1] = -6 * mult;
	bsplineMatrix[1][2] = 3 * mult;
	bsplineMatrix[1][3] = 0;
	bsplineMatrix[2][0] = -3 * mult;
	bsplineMatrix[2][1] = 0;
	bsplineMatrix[2][2] = 3 * mult;
	bsplineMatrix[2][3] = 0;
	bsplineMatrix[3][0] = mult;
	bsplineMatrix[3][1] = 4 * mult;
	bsplineMatrix[3][2] = mult;
	bsplineMatrix[3][3] = 0;
    }

    public CurvePanel() {
	setBackground(Color.white);
    }

    private void calcEMatrix(int prec) {
    // In order to use the "forward difference" method of curve plotting,
    // we must generate this matrix.  The parameter indicates the precision;
    // the number of line segments to use for each curve.

	float step = (float) (1.0/(float)prec);

	eMatrix[0][0] = 0;
	eMatrix[0][1] = 0;
	eMatrix[0][2] = 0;
	eMatrix[0][3] = 1;
	
	eMatrix[1][2] = step;
	eMatrix[1][1] = eMatrix[1][2] * step;
	eMatrix[1][0] = eMatrix[1][1] * step;
	eMatrix[1][3] = 0;

        eMatrix[2][0] = 6 * eMatrix[1][0]; 
	eMatrix[2][1] = 2 * eMatrix[1][1];
	eMatrix[2][2] = 0;
	eMatrix[2][3] = 0;

	eMatrix[3][0] = eMatrix[2][0];
	eMatrix[3][1] = 0;
	eMatrix[3][2] = 0;
	eMatrix[3][3] = 0;
    }

    public void setAction(int action) {
    // Change the action type
	switch (action) {
	  case ADD:
	  case MOVE:
	  case DELETE:
	    this.action = action;
	    break;
	  default:
	    throw new IllegalArgumentException();
	}
    }

    public void setCurveType(int mode) {
    // Change the curve display type
	switch (mode) {
	  case HERMITE:
	  case BEZIER:
	  case BSPLINE:
	    this.mode = mode;
	    break;
	  default:
	    throw new IllegalArgumentException();
	}
    }

    public void setPrecision(int prec) {
	precision = prec;
	calcEMatrix(prec);
    }

    public void clearPoints() {
	points.removeAllElements();
    }

    private int findPoint(int a, int b) {
    // Scan the list of control points to find out which (if any) point
    // contains the coordinates: a,b.
    // If a point is found, return the point's index, otherwise return -1
        int max = points.size();

	for(int i = 0; i < max; i++) {
	    ControlPoint pnt = (ControlPoint)points.elementAt(i);
	    if (pnt.within(a,b)) {
		return i;
	    }
	}
	return -1;
    }

    public boolean handleEvent(Event e) {
	switch (e.id) {
	  case Event.MOUSE_DOWN:
	    // How we handle a MOUSE_DOWN depends on the action mode
	    switch (action) {
	      case ADD:
		// Add a new control point at the specified location
		ControlPoint pnt;
		points.addElement(pnt = new ControlPoint(e.x, e.y));
		repaint();
		break;
	      case MOVE:
		// Attempt to select the point at the location specified.
		// If there is no point at the location, findPoint returns
		// -1 (i.e. there is no point to be moved)
		moving_point = findPoint(e.x, e.y);
		break;
	      case DELETE:
		// Delete a point if one has been clicked
		int delete_pt = findPoint(e.x, e.y);
		if(delete_pt >= 0) {
		   points.removeElementAt(delete_pt);
		   repaint();
		}
		break;
	      default:
	        throw new IllegalArgumentException();
	    }
	    return true;
	  case Event.MOUSE_UP:
	    // We only care about MOUSE_UP's if we've been moving a control
	    // point.  If so, drop the control point.
	    if (moving_point >=0) {
		moving_point = -1;
	        repaint();
	    }
	    return true;
	  case Event.MOUSE_DRAG:
	    // We only care about MOUSE_DRAG's while we are moving a control
	    // point.  Otherwise, do nothing.
	    if (moving_point >=0) {
		ControlPoint pnt = (ControlPoint) points.elementAt(moving_point);
		pnt.x = e.x;
		pnt.y = e.y;
		repaint();
	    }
	    return true;
	  case Event.WINDOW_DESTROY:
	    System.exit(0);
	    return true;
	  default:
	    return false;
	}
    }

    private void multMatrix(float m[][], float g[][], float mg[][]) {
    // This function performs the meat of the calculations for the
    // curve plotting.  Note that it is not a matrix multiplier in the
    // pure sense.  The first matrix is the curve matrix (each curve type
    // has its own matrix), and the second matrix is the geometry matrix
    // (defined by the control points).  The result is returned in the
    // third matrix.
    int i,j,k;
    
	// First clear the return array
	for(i=0; i<4; i++) 
	    for(j=0; j<2; j++) 
		mg[i][j]=0;

	// Perform the matrix math
	for(i=0; i<4; i++) 
	    for(j=0; j<2; j++) 
		for(k=0; k<4; k++) 
		    mg[i][j]=mg[i][j] + (m[i][k] * g[k][j]);
    }

    public void paint(Graphics g) {
	int np = points.size();           // number of points
        float geom[][] = new float[4][2]; // geometry matrix
        float mg[][] = new float[4][2];   // 
        float plot[][] = new float[4][2];

	g.setColor(getForeground());
	g.setPaintMode();

	// draw a border around the canvas
	g.drawRect(0,0, size().width-1, size().height-1);

	// draw the control points
	for (int i=0; i < np; i++) {
	    ControlPoint p = (ControlPoint)points.elementAt(i);
	    g.drawRect(p.x-p.PT_SIZE, p.y-p.PT_SIZE, p.PT_SIZE*2, p.PT_SIZE*2);
	    g.drawString(String.valueOf(i),p.x+p.PT_SIZE,p.y-p.PT_SIZE);
	}

	for(int i = 0; i < np-3;) {
	// Four control points are needed to create a curve.
	// If all the control points are used, the last series of four 
	// points begins with point np-4.
	    switch (mode) {
		// The geometry matrix for a series of control points is
		// different for each curve type.
	        case(HERMITE):
	    	    geom[0][0] = ((ControlPoint)points.elementAt(i)).x;
	    	    geom[0][1] = ((ControlPoint)points.elementAt(i)).y;
	    	    geom[1][0] = ((ControlPoint)points.elementAt(i+3)).x;
	    	    geom[1][1] = ((ControlPoint)points.elementAt(i+3)).y;
	    	    geom[2][0] = ((ControlPoint)points.elementAt(i+1)).x-geom[0][0];
	    	    geom[2][1] = ((ControlPoint)points.elementAt(i+1)).y-geom[0][1];
	    	    geom[3][0] = geom[1][0]-((ControlPoint)points.elementAt(i+2)).x;
	    	    geom[3][1] = geom[1][1]-((ControlPoint)points.elementAt(i+2)).y;
		    multMatrix(hermiteMatrix, geom, mg);

		    // The beginning of the next Hermite curve is the last
		    // point of the previous curve.
		    i += 3;
		    break;
	        case(BEZIER):
		    for(int j = 0; j <4 ;j++) {
	    	        geom[j][0] = ((ControlPoint)points.elementAt(i+j)).x;
	    	        geom[j][1] = ((ControlPoint)points.elementAt(i+j)).y;
		    }
		    multMatrix(bezierMatrix, geom, mg);

		    // The beginning of the next Bezier curve is the last
		    // point of the previous curve.
		    i += 3;
		    break;
	        case(BSPLINE):
		    for(int j = 3; j >= 0; j--) {
	    	        geom[3-j][0] = ((ControlPoint)points.elementAt(i+j)).x;
	    	        geom[3-j][1] = ((ControlPoint)points.elementAt(i+j)).y;
		    }	
		    multMatrix(bsplineMatrix, geom, mg);

		    // B-Spline is the slowest curve, since the beginning of
		    // the next series of four control points is the second
		    // control point of the previous series.
		    i++;
		    break;
	     }

	     // In order to plot the curve using forward differences
	     // (a speedier way to plot the curve), another matrix
	     // calculation is required, taking into account the precision
	     // of the curve.
	     multMatrix(eMatrix, mg, plot);
	     float startX = plot[0][0];
	     float x = startX;
	     float startY = plot[0][1];
	     float y = startY;
	     // Plot the curve using the forward difference method
	     for(int j=0; j<precision; j++) {
		 x += plot[1][0];
		 plot[1][0] += plot[2][0];
		 plot[2][0] += plot[3][0];
		 y += plot[1][1];
		 plot[1][1] += plot[2][1];
		 plot[2][1] += plot[3][1];
		 g.drawLine((int)startX,(int)startY,(int)x,(int)y);
		 startX = x;
		 startY = y;
	     }
	}
    }
}

class CurveControls extends Panel {
    CurvePanel target;
    Checkbox cb_add;
    Checkbox cb_move;
    Checkbox cb_delete;
    String st_add_label = "Add Points";
    String st_move_label = "Move Points";
    String st_delete_label = "Delete Points";

    public CurveControls(CurvePanel target) {
	this.target = target;
	setLayout(new FlowLayout(FlowLayout.CENTER));
	setBackground(Color.lightGray);
	Button clear = new Button("Clear");
	add("West",clear);

	CheckboxGroup action_group = new CheckboxGroup();
	add(cb_add = new Checkbox(st_add_label, action_group, true));
	add(cb_move = new Checkbox(st_move_label, action_group, false));
	add(cb_delete = new Checkbox(st_delete_label, action_group, false));
    }

    public void paint(Graphics g) {
	Rectangle r = bounds();

	g.setColor(Color.lightGray);
	g.draw3DRect(0, 0, r.width, r.height, false);
    }

    public boolean action(Event e, Object arg) {
	if (e.target instanceof Checkbox) {
	    String cbox = ((Checkbox)(e.target)).getLabel();
	    if (cbox.equals(st_add_label)) {
	    	target.setAction(CurvePanel.ADD);
	    } else if (cbox.equals(st_move_label)) {
	    	target.setAction(CurvePanel.MOVE);
	    } else if (cbox.equals(st_delete_label)) {
	    	target.setAction(CurvePanel.DELETE);
	    }
	} else if (e.target instanceof Button) {
	    String button = ((Button)(e.target)).getLabel();
	    if (button.equals("Clear")) {
	    	target.clearPoints();

		// After clearing the control points, put the user back into
		// ADD mode, since none of the other modes make any sense.
		cb_add.setState(true);
		cb_delete.setState(false);
		cb_move.setState(false);
	    	target.setAction(CurvePanel.ADD);
		target.repaint();
	    }
	}
	return true;
    }
}

class CurveControls2 extends Panel {
    CurvePanel target;
    Checkbox cb_hermite;
    Checkbox cb_bezier;
    Checkbox cb_bspline;
    String st_hermite_label = "Hermite";
    String st_bezier_label = "Bezier";
    String st_bspline_label = "B-Spline";
    Scrollbar scroll;
    Label precision_display;

    public CurveControls2(CurvePanel target) {
	this.target = target;
	setLayout(new FlowLayout(1));

	CheckboxGroup type_group = new CheckboxGroup();
	add(cb_hermite = new Checkbox(st_hermite_label, type_group, true));
	add(cb_bezier = new Checkbox(st_bezier_label, type_group, false));
	add(cb_bspline = new Checkbox(st_bspline_label, type_group, false));

	// Set up the scrollbar: Value 15, Page 5, Min 2, Max 40
	add(scroll = new Scrollbar(Scrollbar.HORIZONTAL, 15, 5, 2, 40));
	target.setPrecision(scroll.getValue());
	add(precision_display = new Label("Precision: "+
		String.valueOf(scroll.getValue()),Label.LEFT));
    }

    public void paint(Graphics g) {
	Rectangle r = bounds();

	g.setColor(Color.lightGray);
	g.draw3DRect(0, 0, r.width, r.height, false);
    }

    public boolean handleEvent(Event e) {
	switch (e.id) {
	  case Event.SCROLL_LINE_DOWN:
	  case Event.SCROLL_LINE_UP:
	  case Event.SCROLL_PAGE_DOWN:
	  case Event.SCROLL_PAGE_UP:
	  case Event.SCROLL_ABSOLUTE:
	  // For any of these events, get the precision value from the
	  // scrollbar, and update the curve precision, and the label.
		target.setPrecision(((Scrollbar)e.target).getValue());
		precision_display.setText("Precision: "+
			String.valueOf(((Scrollbar)e.target).getValue()));
		target.repaint();
		return true;
	  case Event.ACTION_EVENT:
	  // Handle other action events
		if (e.target instanceof Checkbox) {
		   String cbox = ((Checkbox)(e.target)).getLabel();
		   if (cbox.equals(st_hermite_label)) {
		      target.setCurveType(CurvePanel.HERMITE);
		   } else if (cbox.equals(st_bezier_label)) {
		      target.setCurveType(CurvePanel.BEZIER);
		   } else if (cbox.equals(st_bspline_label)) {
		      target.setCurveType(CurvePanel.BSPLINE);
		   }
		}
		target.repaint();
		return(true);
	  default:
		return(false);
	}
    }
}
