/* AlexWarp image-warping program, copyright 1996 by Alex Rosen (axlrosen@tiac.net).
This code is provided for educational/informational purposes only; you may not copy 
it (or the AlexWarp applet) without the express permission of the author. */


/* AlexWarp is a port to Java of a Mac-based program I wrote. All the code in
the ImageWarper class was taken straight from the Mac, with a little 
search/replace magic thrown in. */



import java.awt.*;
import java.applet.*;
import java.awt.image.*;

public class AlexWarp extends Applet implements Runnable 
{
  	Thread			mStartup = null;						// initialization thread
  	ImageWarper		mWarper = null;							// warping thread
  	
	String			mImageName = null;						// filename of the image
	Image			mImage = null;							// the image to be warped
	int				mPixels[], mOldPixels[];				// pixel data from images
	int				mWidth, mHeight;						// dimensions of warp image
	String			mStatus = "";							// status message
	Point			mFromPoint = null, mToPoint = null; 	// for clicking & dragging
	boolean			mReady = false;							// ready to warp
	
	boolean			mCanUndo = false;						// can user select undo/redo?
	boolean			mRedo = false;							// if mCanUndo, is this an undo or a redo? 
	Button			mUndoButton = null;						// the undo/redo button
	
	final int		kHOffset = 30, kVOffset = 35;			// offset of image in applet
	
	
	public void run() 
	{
		Initialize();
		
			// need to keep a thread going to update status while warping
			// (this won't work from the ImageWarper thread for some reason)
		while( mStartup != null )
		{
   			try { Thread.sleep(500); } catch ( InterruptedException e ) {}
			if ( !mReady )
			{
					// if warping, add a period on the end of the status message
  				mStatus += ".";
   				showStatus( mStatus );
   			}
    	}
	}
	 
	void Initialize()
	{
		mStatus = "Loading image...";
		showStatus( mStatus );
		mReady = false;
		mCanUndo = false;
		mRedo = false;
		mUndoButton.disable();

			// get warp image & dimensions
		mImage = getImage( getCodeBase(), mImageName );
		while ( (mWidth=mImage.getWidth(this)) < 0 )						// this is not the way we're really
			try { Thread.sleep(100); } catch( InterruptedException e ) {}	// supposed to do this. oh well, it works.
		while ( (mHeight=mImage.getHeight(this)) < 0 )						
			try { Thread.sleep(100); } catch( InterruptedException e ) {}		

			// get the pixel data from the image
		mPixels = new int[mWidth*mHeight];
		mOldPixels = new int[mWidth*mHeight];
		PixelGrabber grabber = new PixelGrabber( mImage, 0, 0, mWidth, mHeight, mPixels, 0, mWidth );
		boolean done = false;
		do
		{
				// grab pixels for 500 msec
			try 
			{ 
				done = grabber.grabPixels( 500 );  
			} 
			catch ( InterruptedException e ) 
			{ 
				mStatus = "AlexWarp interrupted.";
				showStatus( mStatus );
				return; 
			}
			
				// update status message
			mStatus += "."; 
			showStatus( mStatus );
		}
		while( !done );

		if ( (grabber.status() & ImageObserver.ABORT) != 0)
		{
			mStatus = "AlexWarp interrupted.";
			showStatus( mStatus );
			return;
		}
		
		mReady = true;
		mStatus = "Ready for warping. Click and drag in the image to warp it.";
		showStatus( mStatus );
		repaint();
	}
	 
	 	// called by ImageWarper thread when warping is complete
	void DoneWithWarping()
	{
			// the new picture is now in mPixels - create a new Image from it
		mImage = createImage( new MemoryImageSource(mWidth, mHeight, mPixels, 0, mWidth) );

			// set button to "undo" state
		mUndoButton.enable();
		mUndoButton.setLabel( "Undo" );
		mRedo = false;
		mCanUndo = true;
		repaint();
		
			// all done, ready for the next one!
		mReady = true;
		mWarper = null;
		mStatus = "Ready for warping. Click and drag in the image to warp it.";
		showStatus( mStatus );
	}

	public void paint(Graphics g) 
	{                
			// draw image
		if ( mImage != null )
			g.drawImage( mImage, kHOffset, kVOffset, this );

			// if user is dragging, draw line
		if ( mFromPoint != null && mToPoint != null )
		{
			g.setColor( Color.red );
			g.drawLine( mFromPoint.x, mFromPoint.y, mToPoint.x, mToPoint.y );
			g.setColor( Color.black );
		}
	}
	
	public void update(Graphics g)
	{
		paint(g);
	}
	
	public void start() 
	{
	  	if( mImage == null ) // have we initialized yet?
	    {
				// add buttons
			Button b = new Button( "Undo" );
			add( b );
			b.disable();
			mUndoButton = b;
			b = new Button( "Stop" );
			add( b );
			b = new Button( "Reset" );
			add( b );
			
				// get name of the image we're supposed to warp
			mImageName = getParameter( "image" );
			if ( mImageName == null )
				mImageName = "warp.gif"; // default name

				// start initialization thread
	      	mStartup = new Thread(this);
	      	mStartup.start();
	    }
	}
	
	public void stop() 
	{
		if ( mStartup != null )
			mStartup.stop();
		mStartup = null;
		if ( mWarper != null )
			mWarper.stop();
		mWarper = null;
	}
	
	public boolean action( Event e, Object obj )
	{
		if ( "Undo".equals( obj ) || "Redo".equals( obj ) )
		{
				// user click on undo button
			int temp[] = mPixels;
			mPixels = mOldPixels;
			mOldPixels = temp;
			mImage = createImage( new MemoryImageSource(mWidth, mHeight, mPixels, 0, mWidth) );
			mRedo = !mRedo;
			mUndoButton.setLabel( mRedo ? "Redo" : "Undo" );
			repaint();
			return true;
		}
		else if ( "Stop".equals( obj ) )
		{
				// user click on stop button
			if ( mWarper != null )
			{
				mWarper.stop();
				mWarper = null;
				mReady = true;
				mCanUndo = false;
				mUndoButton.disable();
				repaint();
				mStatus = "Ready for warping. Click and drag in the image to warp it.";
				showStatus( mStatus );
			}
			return true;
		}
		else if ( "Reset".equals( obj ) )
		{
				// user click on reset button
			if ( mWarper != null )
			{
				mWarper.stop();
				mWarper = null;
			}
			if ( mStartup != null )
			{
				mStartup.stop();
				mStartup = new Thread(this);
				mStartup.start();
			}
			return true;
		}
		
		return false;
	}
	
		// user begins to click-n-drag
	public boolean mouseDown( Event e, int x, int y )
	{
		if ( !PointInImage( x, y ) || !mReady )
			return false;

		mFromPoint = new Point( x, y );
		return true;
	}
	
		// user continues dragging
	public boolean mouseDrag( Event e, int x, int y )
	{
		if ( mFromPoint == null )
			return false;

			// set mToPoint, so that paint will draw it
		mToPoint = ClipToImage( mFromPoint, x, y );
		repaint();
		return true;
	}
	
		// user is done dragging - start warping!
	public boolean mouseUp( Event e, int x, int y )
	{
		if ( mFromPoint == null )
			return false;

		mReady = false;
		mStatus = "Warping...";
		showStatus( mStatus );
		
			// swap new & old pixels; old pixels are saved in case of undo, new pixels will receive new image
		int temp[] = mOldPixels;
		mOldPixels = mPixels;
		mPixels = temp;
		
			// start warp thread
		Point clipPoint = ClipToImage( mFromPoint, x, y );
		mWarper = new ImageWarper( this, mOldPixels, mPixels, mWidth, mHeight, new Point( clipPoint.x - kHOffset, clipPoint.y - kVOffset ), new Point( mFromPoint.x - kHOffset, mFromPoint.y - kVOffset ) );
		mWarper.start();
		
		mFromPoint = mToPoint = null;
		return true;
	}

		// is this point inside the image area?
	boolean PointInImage( int x, int y )
	{
		return ( x >= kHOffset && x < kHOffset+mWidth && y >= kVOffset && y < kVOffset+mHeight );
	}
	
		// clip line to the image area. returns a point that contains replacements for x & y
	Point ClipToImage( Point from, int x, int y )
	{
		int dx = x - from.x, dy = y - from.y;
		if ( dx == 0 )
		{
			if ( y < kVOffset )				y = kVOffset;
			if ( y >= kVOffset + mHeight )	y = kVOffset + mHeight - 1;
		}
		else if ( dy == 0 )
		{
			if ( x < kHOffset )				x = kHOffset;
			if ( x >= kHOffset + mWidth )	x = kHOffset + mWidth - 1;
		}
		else
		{
			double slope = (double)dy / dx;
			if ( x < kHOffset )
			{
				x = kHOffset;
				y = from.y + (int)(slope * (x-from.x));
			}
			if ( x >= kHOffset + mWidth )
			{
				x = kHOffset + mWidth - 1;
				y = from.y + (int)(slope * (x-from.x));
			}
			if ( y < kVOffset )
			{
				y = kVOffset;
				x = from.x + (int)((y-from.y) / slope);
			}
			if ( y >= kVOffset + mHeight )
			{
				y = kVOffset + mHeight - 1;
				x = from.x + (int)((y-from.y) / slope);
			}
		}
		
		return new Point( x, y );
	}
	 
}
	
/* ImageWarper is the class that does the actual warping. Give it two pixels buffers -
one with the original image, and one to hold the new image. It calls DoneWithWarping
to let you know when it's all done. */

class ImageWarper extends Thread
{
	AlexWarp		mAlexWarp;
	Point			mFromPoint, mToPoint;
	int				mFromPixels[], mToPixels[];
	int				mWidth, mHeight;			// width & height of warp image
	
	ImageWarper( AlexWarp j, int fromPixels[], int toPixels[], int w, int h, Point fromPoint, Point toPoint )
	{
		mAlexWarp = j;
		mFromPixels = fromPixels; mToPixels = toPixels;
		mFromPoint = fromPoint; mToPoint = toPoint;
		mWidth = w; mHeight = h;
	}
	
		// warp the pixels, then notify the applet
	public void run()
	{
		WarpPixels();
		mAlexWarp.DoneWithWarping();
	}
			
		// warp mFromPixels into mToPixels
	void WarpPixels()
	{
		int			dx = mToPoint.x-mFromPoint.x, dy = mToPoint.y-mFromPoint.y, dist = (int)Math.sqrt(dx*dx+dy*dy)*2;
		Rectangle	r = new Rectangle();
		Point		ne = new Point(0,0), nw = new Point(0,0), se = new Point(0,0), sw = new Point(0,0);
		
			// copy mFromPixels to mToPixels, so the non-warped parts will be identical
		System.arraycopy( mFromPixels, 0, mToPixels, 0, mWidth*mHeight );
		if ( dist == 0 ) return;

			// warp northeast quadrant
		SetRect( r, mFromPoint.x - dist, mFromPoint.y - dist, mFromPoint.x, mFromPoint.y );
		ClipRect( r, mWidth, mHeight );
		SetPt( ne, r.x, r.y );
		SetPt( nw, r.x+r.width, r.y );
		SetPt( se, r.x, r.y+r.height );
		SetPt( sw, mToPoint.x, mToPoint.y );
		WarpRegion( r, nw, ne, sw, se );

			// warp nortwest quadrant
		SetRect( r, mFromPoint.x, mFromPoint.y - dist, mFromPoint.x + dist, mFromPoint.y );
		ClipRect( r, mWidth, mHeight );
		SetPt( ne, r.x, r.y );
		SetPt( nw, r.x+r.width, r.y );
		SetPt( se, mToPoint.x, mToPoint.y );
		SetPt( sw, r.x+r.width, r.y+r.height );
		WarpRegion( r, nw, ne, sw, se );
		
			// warp southeast quadrant
		SetRect( r, mFromPoint.x - dist, mFromPoint.y, mFromPoint.x, mFromPoint.y + dist );
		ClipRect( r, mWidth, mHeight );
		SetPt( ne, r.x, r.y );
		SetPt( nw, mToPoint.x, mToPoint.y );
		SetPt( se, r.x, r.y+r.height );
		SetPt( sw, r.x+r.width, r.y+r.height );
		WarpRegion( r, nw, ne, sw, se );
	
			// warp southwest quadrant
		SetRect( r, mFromPoint.x, mFromPoint.y, mFromPoint.x + dist, mFromPoint.y + dist );
		ClipRect( r, mWidth, mHeight );
		SetPt( ne, mToPoint.x, mToPoint.y );
		SetPt( nw, r.x+r.width, r.y );
		SetPt( se, r.x, r.y+r.height );
		SetPt( sw, r.x+r.width, r.y+r.height );
		WarpRegion( r, nw, ne, sw, se );
	}
	
		// warp a quadrilateral into a rectangle (magic!)
	void WarpRegion( Rectangle fromRect, Point nw, Point ne, Point sw, Point se )
	{
		int				dx = fromRect.width, dy = fromRect.height;
		double			invDX = 1.0/dx, invDY = 1.0/dy;

		for ( int a = 0; a < dx; a++ )
		{
			double 	aa = a * invDX;
			double 	x1 = ne.x + ( nw.x - ne.x ) * aa;
			double 	y1 = ne.y + ( nw.y - ne.y ) * aa;
			double 	x2 = se.x + ( sw.x - se.x ) * aa;
			double 	y2 = se.y + ( sw.y - se.y ) * aa;
	
			double 	xin = x1;
			double 	yin = y1;
			double 	dxin = ( x2 - x1 ) * invDY;
			double 	dyin = ( y2 - y1 ) * invDY;
			int 	toPixel = fromRect.x + a + fromRect.y * mWidth;
	
			for ( int b = 0; b < dy; b++ )
			{
				if ( xin < 0 ) 			xin = 0;
				if ( xin >= mWidth )	xin = mWidth - 1; 
				if ( yin < 0 ) 			yin = 0;
				if ( yin >= mHeight )	yin = mHeight - 1; 

				int pixelValue = mFromPixels[ (int)xin + (int)yin * mWidth ];
				mToPixels[ toPixel ] = pixelValue;

				xin += dxin;
				yin += dyin;
				toPixel += mWidth;
			}
		}	
	}

	void ClipRect( Rectangle r, int w, int h )
	{
		if ( r.x < 0 ) 				{ r.width += r.x; r.x = 0; }
		if ( r.y < 0 ) 				{ r.height += r.y; r.y = 0; }
		if ( r.x+r.width >= w ) 	r.width = w - r.x - 1;
		if ( r.y+r.height >= h ) 	r.height = h - r.y - 1;
	}
	
		// SetRect and SetPt are Mac OS functions. I wrote my own versions here
		// so I didn't have to rewrite too much of the code. 
	
	void SetRect( Rectangle r, int left, int top, int right, int bottom )
	{
		r.x = left; r.y = top; r.width = right-left; r.height = bottom-top;
	}
	
	void SetPt( Point pt, int x, int y )
	{
		pt.x = x; pt.y = y;
	}
	
}		
		
