/** * Class ClusterEngine * * The mechanics to control the cluster effect */ package cluster { import flash.display.Stage; import flash.events.Event; import flash.events.MouseEvent; import flash.geom.Point; import flash.utils.Dictionary; public class ClusterEngine { public static const MIN_INNER:Number = 20; //The closest a sibling can be public static const MAX_INNER:Number = 50; //The furthest a sibling can be public static const MIN_OUTER:Number = 100; //The closest a non-sibling can be protected var _stage:Stage; protected var _nodes:Array = new Array(); protected var _activeNode:Node; protected var _siblingPositions:Dictionary; public function ClusterEngine( stage:Stage ) { _stage = stage; } public function get stage():Stage { return _stage; } public function set stage( value:Stage ):void { if (_stage != value) disableEvents(); _stage = value; } /** * Returns a clone of the node tree */ public function get nodeTree():Array { var arr:Array = new Array(); while (arr.length < _nodes.length ) { arr[arr.length] = _nodes[arr.length]; } return arr; } /** * add a node to the node tree, if already a member nothing occurs */ public function addNodeToTree( node:Node ):void { if (_nodes.indexOf( node ) >= 0) return; listenToNode( node ); _nodes.push( node ); } /** * Remove a node from the node tree, if it isn't a member nothing occurs */ public function removeNodeFromTree( node:Node ):void { if (_nodes.indexOf( node ) < 0) return; stopListeningToNode( node ); _nodes.splice( _nodes.indexOf( node ), 1 ); } /** * Start the Cluster Engine */ public function start():void { enableEvents(); } /** * Stop the Cluster Engine */ public function stop():void { disableEvents(); } /** * * LISTENER METHODS FOR THE ENGINE * */ /** * Set the activeNode to the recently clicked node so it can be dragged */ protected function grabNewNode(e:MouseEvent):void { var node:Node = e.currentTarget as Node; if (node) { if (node.parent) node.parent.addChild( node ); _activeNode = node; _siblingPositions = createNewClusterPoints( node ); } else { _activeNode = null; _siblingPositions = null; } } /** * Remove any reference to an active node to stop all dragging */ protected function dropCurrentNode(e:MouseEvent):void { _activeNode = null; _siblingPositions = null; } /** * Update the positions of all nodes with respect to the actively dragged node */ protected function updateCurrentNodePosition(e:Event):void { if (!_activeNode) return; //set up variables for repeat use var pnt:Point; var node:Node; //get the position of the mouse with respect to the active node pnt = new Point( _stage.mouseX, _stage.mouseY ); if ( _activeNode.parent ) pnt = _activeNode.parent.globalToLocal( pnt ); //update the active node's position updatePositionOfNode( _activeNode, pnt ); var sibs:Array = _activeNode.siblings; var nonSibs:Array = filterSiblingsFromNodeTree( sibs ); //bring the sibling nodes close to the active node while(sibs.length) { node = sibs.pop(); //get a clone of the position with respect to the active node pnt = _siblingPositions[ node ].clone(); pnt.x += _activeNode.x; pnt.y += _activeNode.y; //bring the position to the sibling nodes coordinate space if (_activeNode.parent) pnt = _activeNode.parent.localToGlobal( pnt ); if (node.parent) pnt = node.parent.globalToLocal( pnt ); //update siblings position updatePositionOfNode( node, pnt ); } while( nonSibs.length ) { node = nonSibs.pop(); //get the siblings nodes position pnt = new Point( node.x, node.y ); //bring the position to the active node's coordinate space if (node.parent) pnt = node.parent.localToGlobal( pnt ); if (_activeNode.parent) pnt = _activeNode.parent.globalToLocal( pnt ); //check if the position is close to the active node pnt.x -= _activeNode.x; pnt.y -= _activeNode.y; if (pnt.length < ClusterEngine.MIN_OUTER) { //if the position is to close to the active node, push it away pnt.normalize( ClusterEngine.MIN_OUTER ); pnt.x += _activeNode.x; pnt.y += _activeNode.y; //don't forget to put it back in the non-siblings coordinate space first! if (_activeNode.parent) pnt = _activeNode.parent.localToGlobal( pnt ); if (node.parent) pnt = node.parent.globalToLocal( pnt ); updatePositionOfNode( node, pnt ); } } } /** * Internal methods for repetetive actions */ /** * Return a clone of the node tree without any of the nodes supplied in the param array "sibs" */ protected function filterSiblingsFromNodeTree( sibs:Array ):Array { var arr:Array = new Array(); for each( var node:Node in _nodes ) { if (sibs.indexOf( node ) < 0) arr.push( node ); } return arr; } /** * Lerp the position of the node in param "node" to the position of param "pnt". * pnt should be in the same coordinate space as node */ protected function updatePositionOfNode( node:Node, pnt:Point ):void { pnt = pnt.clone(); pnt.x -= node.x; pnt.y -= node.y; var len:Number = pnt.length; if (len > 1) pnt.normalize( len / 4 ); node.x += pnt.x; node.y += pnt.y; } /** * Create a dictionary of positions for each of the siblings to param "node" for the cluster effect */ protected function createNewClusterPoints( node:Node ):Dictionary { var dict:Dictionary = new Dictionary( true ); var arr:Array = node.siblings; var segA:Number = Math.PI * 2 / arr.length; while (arr.length) { var sib:Node = arr.pop(); var a:Number = segA * arr.length; var pnt:Point = new Point( Math.cos(a), Math.sin(a) ); var rad:Number = ClusterEngine.MIN_INNER + Math.random() * (ClusterEngine.MAX_INNER - ClusterEngine.MIN_INNER); pnt.normalize( rad ); dict[ sib ] = pnt; } return dict; } /** * * EVENT LISTENER SETTING METHODS * */ /** * Add listener to a node to check when it was clicked on and should be grabbed */ protected function listenToNode( node:Node ):void { if (_nodes.indexOf( node ) < 0) return; node.addEventListener( MouseEvent.MOUSE_DOWN, grabNewNode, false, 0, true ); } /** * Remove the listener from a node that checks when it was clicked on and should be grabbed */ protected function stopListeningToNode( node:Node ):void { node.removeEventListener( MouseEvent.MOUSE_DOWN, grabNewNode ); } /** * Add all event listeners to the referenced stage */ protected function enableEvents():void { for each( var node:Node in _nodes ) { listenToNode( node ); } if (!_stage) return; _stage.addEventListener( MouseEvent.MOUSE_UP, dropCurrentNode, false, 0, true ); _stage.addEventListener( Event.ENTER_FRAME, updateCurrentNodePosition, false, 0, true ); } /** * Remove all event listeners from the referenced stage */ protected function disableEvents():void { for each( var node:Node in _nodes ) { stopListeningToNode( node ); } if (!_stage) return; _stage.removeEventListener( MouseEvent.MOUSE_UP, dropCurrentNode ); _stage.removeEventListener( Event.ENTER_FRAME, updateCurrentNodePosition ); } } }