InterpolatingRect

Have you ever wanted to create a Dialog Bubble for a game but just couldn’t quite get the tip of the bubble to align it to where the head (or mouth) of the character appeared? What about re-positioning some floating boxes or UI Panels back into view that are located outside the browser window when a User relaunches your application on a smaller screen resolution? Wouldn’t it be nice if they came back into view in a way that makes sense rather than randomly scattering them on the Stage?

Let’s take a look at how this can be achieved using some good’old geometry calculations!

package com
{
	import flash.geom.Point;
	import flash.geom.Rectangle;
	/**
	 * ...
	 * @author Pierre Chamberlain
	 */
	public class InterpolatingRect
	{
		/**
		 * Gets the center point of a rectangle (taking it's x & y location in consideration).
		 * @param	pRect
		 * @param	pCandidate
		 * @return
		 */
		public static function rectCenter( pRect:Rectangle, pCandidate:Point = null ):Point {
			var x:Number =	round(pRect.x + pRect.width * .5);
			var y:Number =	round(pRect.y + pRect.height * .5);

			if (pCandidate) {
				pCandidate.x =	x;
				pCandidate.y =	y;
				return pCandidate;
			}

			return new Point( x, y );
		}

		/**
		 * Finds the top-left location of a small rectangle centered inside a larger rectangle.
		 * @param	pLarge
		 * @param	pSmall
		 * @param	pCandidate
		 * @return
		 */
		public static function rectAlignCenter( pLarge:Rectangle, pSmall:Rectangle, pCandidate:Point=null ):Point {
			var x:Number =	round(pLarge.x + (pLarge.width - pSmall.width) * .5);
			var y:Number =	round(pLarge.y + (pLarge.height - pSmall.height) * .5);

			if (pCandidate) {
				pCandidate.x =	x;
				pCandidate.y =	y;
				return pCandidate;
			}

			return new Point( x, y );
		}

		/**
		 * Restricts a small rectangle within a larger rectangle, with optional Margin and Interpolation parameters.
		 * @param	pLarge
		 * @param	pSmall
		 * @param	pMargin
		 * @param	pInterpolate
		 * @return
		 */
		public static function rectIntersect( pLarge:Rectangle, pSmall:Rectangle, pMargin:Number=0, pInterpolate:Number=0 ):Rectangle {
			if(pLarge.width				return null;
			}

			var smallCenter:Point =		rectCenter(pSmall, new Point);
			var	available:Rectangle =	pLarge.clone();
			available.inflate(
				-(pSmall.width * .5 + pMargin),
				-(pSmall.height * .5 + pMargin)
			);

			round(available);

			var intersection:Point =	pointSnapToRect( smallCenter, available, pInterpolate );
			var result:Rectangle =		pSmall.clone();

			if (!intersection) {
				return result;
			}

			result.x =	intersection.x - result.width * .5;
			result.y =	intersection.y - result.height * .5;

			return result;
		}

		/**
		 * Finds the intersection point between a set of four points (2 lines).
		 * @param	p1
		 * @param	p2
		 * @param	p3
		 * @param	p4
		 * @param	pCandidate	an Optional point object (reduces unnecessary instantiation).
		 * @return	The point of interception, or null if the segments don't touch.
		 */
		public static function lineIntersect(p1:Point, p2:Point, p3:Point, p4:Point, pCandidate:Point=null):Point {
			var x1:Number = p1.x,
				x2:Number = p2.x,
				x3:Number = p3.x,
				x4:Number = p4.x,
				y1:Number = p1.y,
				y2:Number = p2.y,
				y3:Number = p3.y,
				y4:Number = p4.y,
				z1:Number = (x1 -x2),
				z2:Number = (x3 - x4),
				z3:Number = (y1 - y2),
				z4:Number = (y3 - y4),
				d:Number =	z1 * z4 - z3 * z2;

			// If d is zero, there is no intersection
			if (d == 0) {
				return null;
			}

			// Get the x and y
			var pre:Number = (x1*y2 - x2*y1), post:Number = (x3*y4 - x4*y3);
			var x:Number = ( pre * z2 - z1 * post ) / d;
			var y:Number = ( pre * z4 - z3 * post ) / d;

			// Check if the x and y coordinates are within both lines
			if ( x < Math.min(x1, x2) || x > Math.max(x1, x2) ||
				x < Math.min(x3, x4) || x > Math.max(x3, x4) ) {
				return null;
			}

			if ( y < Math.min(y1, y2) || y > Math.max(y1, y2) ||
				y < Math.min(y3, y4) || y > Math.max(y3, y4) ) {
				return null;
			}

			if (pCandidate) {
				pCandidate.x =	x;
				pCandidate.y =	y;
				return pCandidate;
			}

			return new Point( x, y );
		}

		/**
		 * Snaps a point within a Rectangle (restricts it).
		 *
		 * @param	pPoint			The location
		 * @param	pRect			The boundaries
		 * @param	pInterpolate	The amount to interpolate (0-1, 0 being close to boundaries and 1 being center)
		 * @return
		 */
		public static function pointSnapToRect( pPoint:Point, pRect:Rectangle, pInterpolate:Number=0 ):Point
		{
			if (pRect.containsPoint(pPoint)) {
				return pPoint;
			}

			var center:Point =			rectCenter(pRect, new Point),
				intersection:Point,
				edgePoint1:Point = new Point,
				edgePoint2:Point = new Point;

			var attempts:int =	4;

			while (intersection == null && attempts > 0) {
				switch(attempts) {
					case 4:
						edgePoint1.x =	pRect.left;
						edgePoint1.y =	pRect.bottom;
						edgePoint2.x =	pRect.left;
						edgePoint2.y =	pRect.top;
						break;
					case 3:
						edgePoint1.x =	pRect.left;
						edgePoint1.y =	pRect.top;
						edgePoint2.x =	pRect.right;
						edgePoint2.y =	pRect.top;
						break;
					case 2:
						edgePoint1.x =	pRect.right;
						edgePoint1.y =	pRect.top;
						edgePoint2.x =	pRect.right;
						edgePoint2.y =	pRect.bottom;
						break;
					case 1:
						edgePoint1.x =	pRect.right;
						edgePoint1.y =	pRect.bottom;
						edgePoint2.x =	pRect.left;
						edgePoint2.y =	pRect.bottom;
						break;
				}

				round(edgePoint1);
				round(edgePoint2);

				intersection = lineIntersect(edgePoint1, edgePoint2, center, pPoint);

				attempts--;
			}

			if (!intersection) {
				throw new Error("No intersection? How!?");

				return null;
			}

			if(pInterpolate!=0) {
				intersection =	Point.interpolate(center, intersection, pInterpolate);
			}

			return intersection;
		}

		/**
		 * This is an important 'fix' for the lineIntersect calculations. Numbers with a huge
		 * trail of number after the decimal point cause it to misscalculate the line-to-line collision.
		 *
		 * Example: 108.12309831509 would FAIL, but this will round it to 108.
		 *
		 * This can also round the properties of Rectangles and Points for extra convenience.
		 *
		 * @param	value
		 * @return	A number, if one was given. Otherwise, it returns NaN.
		 */
		public static function round( value:* ):Number {
			if (value is Rectangle) {
				var rect:Rectangle =	value as Rectangle;
				rect.x =		round(rect.x);
				rect.y =		round(rect.y);
				rect.width =	round(rect.width);
				rect.height =	round(rect.height);
			} else if (value is Point) {
				var pnt:Point =	value as Point;
				pnt.x =			round(pnt.x);
				pnt.y =			round(pnt.y);
			} else if (value is Number) {
				return Math.round(value);
			}

			return Number.NaN;
		}
	}

}

And here’s a simple (yet intense! ) example of how to use this Class:

package
{
	import com.InterpolatingRect;
	import flash.display.Graphics;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.geom.Point;
	import flash.geom.Rectangle;

	/**
	 * A Demonstration of using Interpolating Rectangles.
	 *
	 * @author Pierre Chamberlain
	 */
	[SWF(width="640",height="480")]
	public class Main extends Sprite
	{
		private var rectLarge:Rectangle;
		private var smallWidth:int =	50;
		private var smallHeight:int =	30;

		public function Main():void
		{
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
		}

		private function init(e:Event = null):void
		{
			removeEventListener(Event.ADDED_TO_STAGE, init);

			var gap:int =					100;

			rectLarge =	new Rectangle( gap, gap,
				stage.stageWidth - gap * 2,
				stage.stageHeight - gap * 2
			);

			stage.addEventListener( MouseEvent.MOUSE_MOVE, onMouseHandler )
			stage.addEventListener( MouseEvent.MOUSE_DOWN, onMouseHandler )

			demonstraterectSmall();
		}

		////////////////////////////////////////////// EVENT CALLBACKS:

		private function onMouseHandler(e:MouseEvent=null):void
		{
			if (e.type == MouseEvent.MOUSE_DOWN) {
				smallWidth =	50 + Math.random() * 200;
				smallHeight =	30 + Math.random() * 200;
			}

			demonstraterectSmall();

			e && e.updateAfterEvent();
		}

		////////////////////////////////////////////// HELPER METHODS:

		private function drawRect( pRectangle:Rectangle, pColor:uint=0x880000, pThickness:int=1 ):void {
			var g:Graphics =	this.graphics;

			g.lineStyle( pThickness, pColor );
			g.drawRect(pRectangle.x, pRectangle.y, pRectangle.width, pRectangle.height);
		}

		private function demonstraterectSmall():void
		{
			this.graphics.clear();

			var rectSmall:Rectangle =
				new Rectangle( stage.mouseX, stage.mouseY, smallWidth, smallHeight );

			//Move the Small Rectangle to the center of the mouse-cursor:
			rectSmall.x -= int(rectSmall.width * .5);
			rectSmall.y -= int(rectSmall.height * .5);

			var result:Rectangle =	InterpolatingRect.rectIntersect(rectLarge, rectSmall, 10 );

			drawRect( rectLarge, 0x00ff00, 2 );
			drawRect( rectSmall, 0xff6666 );
			drawRect( result, 0xff0000, 2 );
		}
	}

}

Here is a live demo:

  • Mouse-Move = Updates position of small rectangle and it’s interpolated result.
  • Mouse-Click = Randomizes small rectangle’s size (and updates).

This area requires
Adobe FlashPlayer version 9.0.0 or above.
iOS Devices are not currently supported.

NOTE: Although it may not seem like there’s any Interpolation happening in this Demo ‘Per se’, notice there’s a 4th parameter in the InterpolatingRect.rectIntersect(...) method called pInterpolate:Number. It can be set from 0 to 1 (zero being flush to the given Margin amount, and 1 being centered in the Large Rectangle)

I’d like to thank Flassari.is for sharing the nice, compact line-intersection algorithm at: http://flassari.is/2009/04/line-line-intersection-in-as3/