Hexagonal Animation Effect

This is going to be a short post. It is the progress I made in converting some code that I found at wonderfl. The link to the demo where I got the original source from, is in the code below. I have also included a link to the image I am using. Use the arrow keys to control the effect.

Essentially I took the algorithm and tried to make some sense of it. The other agenda was to provide a generic method which would apply the effect to any display object passed to it. Seeing as I spent most of the time trying to figure out the code and have not refactored it yet, it is still quite a bit messy and poorly designed. Lastly, minor pre-calculation performance enhancements were made to the algorithm.

The algorithm is relatively simple.

  • It starts at the center point provided and then chooses neighbours at random.
  • If there is a neighbour at the location then it will traverse to that neighbour and repeat.
  • If there is no neighbour, it is then assumed it is at the edge of the structure and thus creates a new hexagon
  • It then links it accordingly to keep the animation fluid
  • The animation speed is currently tied to the fps of the flash object. There is a plan to allow for variable speed control, using a timer, when the class is refactored.
  • There are two drawing constructions
    • One for the mask (plain hexagons)
    • Another for the hexagonal border (currently attached to the container provided). An enhancement would be to [attach / remove] the border [to / from] the parent instead.

Code

With the enhancements suggested above, the stage will not have to be passed in. It is currently used only to tie the animation to the frame-rate. Secondly the container will not have to be passed in, as the parent can be used instead of the container.

package avdw.generate.effect {
	import avdw.generate.effect.shape.Hexagon;
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.DisplayObject;
	import flash.display.Graphics;
	import flash.display.Shape;
	import flash.display.Sprite;
	import flash.display.Stage;
	import flash.events.Event;
	import flash.geom.Point;

	/**
	 * TODO: add in a speed parameter to control the animation speed
	 *
	 * This class will apply an animated mask to an image.
	 * The animation is a collection of hexagons being built around a point.
	 * Where the hexagons are built the mask is applied to reveal the image.
	 *
	 * Reference: http://wonderfl.net/c/jLM1
	 * ...
	 * @author Andrew van der Westhuizen
	 */
	public class HexagonalMask {
		private const angles:Vector.<Number> = new Vector.<Number>();

		private var stage:Stage;
		private var container:Sprite;
		private var displayObject:DisplayObject;
		private var hexagons:int;
		private var diameter:int;
		private var firstHex:Hexagon;

		private const V:Number = 30;

		private var sprite:Sprite = new Sprite(); // hexigons
		private var shape:Shape = new Shape(); // hexigon borders

		public function HexagonalMask(stage:Stage, container:Sprite, displayObject:DisplayObject):void {
			this.stage = stage;
			this.container = container;
			this.displayObject = displayObject;
		}

		public function remove():void {
			if (container.contains(sprite))
				container.removeChild(sprite);
			if (container.contains(shape))
				container.removeChild(shape);
			displayObject.mask = null;
		}

		/**
		 *
		 * @param	start
		 * @param	hexagons
		 * @param	diameter
		 */
		public function apply(start:Point, hexagons:int = 300, diameter:int = 20):void {
			var count:int;
			this.diameter = diameter;
			this.hexagons = hexagons;

			displayObject.mask = sprite;
			container.addChild(sprite);
			container.addChild(shape);

			for (count = 0; count < 6; count++) {
				angles.push(Math.PI / 180 * (60 * count));
			}

			firstHex = new Hexagon();
			firstHex.origin = start;
			firstHex.v = 0;
			var currHex:Hexagon = firstHex;

			for (count = 0; count < hexagons; count++) {
				currHex.next = new Hexagon();
				var tmpHex:Hexagon = firstHex;
				// traverse from origin to outside hex randomly
				// create new hex on border and link it as next for animation
				while (true) {
					var idx:uint = Math.floor(Math.random() * 6); // choose random neighbour
					if (!tmpHex.link[idx]) { // if it does not exist
						// create it and link it
						currHex.next.origin.x = tmpHex.origin.x + diameter * Math.cos(angles[idx]);
						currHex.next.origin.y = tmpHex.origin.y + diameter * Math.sin(angles[idx]);
						checkLink(currHex.next);

						break; // and break, thus will have correct # hexagons
					} else { // if it exists
						tmpHex = tmpHex.link[idx]; // traverse it
					}
				}
				currHex.next.v = Math.random() * -20 + (-10 * count);
				currHex = currHex.next;
			}

			stage.addEventListener(Event.ENTER_FRAME, animate);
		}

		/**
		 * This method assigns a new link on the correct hexigon.
		 *
		 * @param	cp
		 */
		private function checkLink(cp:Hexagon):void {
			for (var i:uint = 0; i < 6; i++) {
				var cx:Number = cp.origin.x + diameter * Math.cos(angles[i]);
				var cy:Number = cp.origin.y + diameter * Math.sin(angles[i]);
				var p:Hexagon = firstHex;
				while (p.next) {
					if (cx < p.origin.x + 2 && cx > p.origin.x - 2 && cy < p.origin.y + 2 && cy > p.origin.y - 2) {
						cp.link[i] = p;
						p.link[(i + 3) % 6] = cp;
					}
					p = p.next;
				}
			}
		}

		private function animate(e:Event = null):void {
			var g:Graphics = shape.graphics; // border graphics
			var sg:Graphics = sprite.graphics; // hexigon graphics
			var p:Hexagon = firstHex;
			var cnt:uint = 0;

			sg.clear();
			g.clear();
			g.lineStyle(1, 0x000000);
			while (p) {
				if (p.v < 610)
					p.v += V;
				if (p.v >= 0) {
					if (p.v >= 600) {
						g.beginFill(0xFFFFFF, p.a / 100 - (Math.random()));
						if (p.a > 0)
							p.a -= 5;
						sg.beginFill(0xAAFFAA, 0.1);
					}
					var bx:Number = 0;
					var by:Number = 0;
					for (var i:Number = 0; i <= 6; i++) {
						var ang:Number = Math.PI / 180 * (60 * i - 90);
						var lx:Number = p.origin.x + diameter / 2 * Math.cos(ang);
						var ly:Number = p.origin.y + diameter / 2 * Math.sin(ang);
						if (i == 0) {
							g.moveTo(lx, ly);
							sg.moveTo(lx, ly);
							bx = lx;
							by = ly;
						} else {
							if (i * 100 <= p.v) {
								g.lineTo(lx, ly);
								if (p.v >= 600)
									sg.lineTo(lx, ly);
								bx = lx;
								by = ly;
							}
							if (i * 100 > p.v && (i - 1) * 100 <= p.v) {
								g.lineTo(bx + (lx - bx) * (p.v % 100) / 100, by + (ly - by) * (p.v % 100) / 100);
							}
						}
					}
					g.endFill();
				}
				p = p.next;
				cnt++;
			}
		}

	}
}