/* 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.applet.Applet;
import java.awt.Button;
import java.awt.Color;
import java.awt.Event;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.ImageObserver;
import java.awt.image.MemoryImageSource;
import java.awt.image.PixelGrabber;

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.setEnabled(false);

		// 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.setEnabled(true);
		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.setEnabled(false);
			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.setEnabled(false);
				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;
	}

}


