Domestic mouse attackIn the Twitterified Client, users can quickly perform some operations using mouse gestures.

When I implemented this, I googled for ActionScript mouse gesture classes but only found one and it was a commercial package.
I am sure that there are many ways to implement gestures, using rule engines, neural networks, etc.

But, curious and optimistic as usual, I decided to empirically build my own. Could it be done?
After some fumbling and several dead-ends, I came up with this solution which works as it should and that can easily be adapted to your own needs.
I believe that all the relevant source code is here; let me know if I’m wrong and do not hesitate to share your improvements!
 
 
Let’s begin with a few instance variables…

?View Code ACTIONSCRIPT
private var drawingCanvas:Canvas = new Canvas();
private var ctrlKeyIsDown:Boolean = false;
 
private var lastPoint:Point;
private var lastDrawPoint:Point;
private var gesturePath:Array;

drawingCanvas is going to be our, er, drawing canvas. We will use it to give the user visual feedback when doing a mouse gesture.
Note that lastDrawPoint will contain the coordinates of the last rendered point, whereas lastPoint will be used to track direction changes in the mouse’s course.
Similarly, gesturePath will only be updated when a significant update is detected.
 
 

?View Code ACTIONSCRIPT
public function main_init():void
{
...
	systemManager.addChild(drawingCanvas);
	systemManager.addEventListener(KeyboardEvent.KEY_DOWN, main_onKeyUpDown);
	systemManager.addEventListener(KeyboardEvent.KEY_UP, main_onKeyUpDown);
	systemManager.addEventListener(MouseEvent.MOUSE_MOVE, main_onMouseMove);
...
}

We are adding drawingCanvas to the top of our user interface, but not giving it a size, yet. We will wait for the user to actually initiate a mouse gesture to, then, detect our current real estate.
I am asking Flex to keep me updated of any keyboard change as I wish to use the [Ctrl] key as a hint that we wish to perform a mouse gesture. Obviously, you can go down a very different path.
 
 

?View Code ACTIONSCRIPT
private function main_onKeyUpDown(e:KeyboardEvent):void
{
	ctrlKeyIsDown = (e.ctrlKey);
	if(ctrlKeyIsDown)
	{
		drawingCanvas.x      = systemManager.stage.x;
		drawingCanvas.y      = systemManager.stage.y;
		drawingCanvas.width  = systemManager.stage.width;
		drawingCanvas.height = systemManager.stage.height;		
		drawingCanvas.graphics.lineStyle(2, 0x6EBAE5, 1);
		lastPoint = new Point(systemManager.stage.mouseX, systemManager.stage.mouseY);
		lastDrawPoint = lastPoint;
		gesturePath = new Array();
		gesturePath.push(new PathBit(lastPoint, lastPoint));
	}
	else
	{
		drawingCanvas.graphics.clear();
		// Gesture
		main_encodeGesture();
	}
}

Two possibilities: [Ctrl] is depressed and we initialize visual feedback and store the original mouse location, or it is up and we clear the feedback information and encode the gesture.
Note that this could be improved to not be disturbed by, say, the user depressing another key!
 
 

?View Code ACTIONSCRIPT
private function main_onMouseMove(e:MouseEvent):void
{
	if(!ctrlKeyIsDown)
		return;
	var x:int = systemManager.stage.mouseX;
	var y:int = systemManager.stage.mouseY;
	drawingCanvas.graphics.moveTo(x, y);
	drawingCanvas.graphics.lineTo(lastDrawPoint.x, lastDrawPoint.y);
	var newPoint:Point = new Point(x, y);
	var pathBit:PathBit = new PathBit(lastPoint, newPoint);
	// > 5% of stage?
	if(Math.abs(pathBit.horizontal) * 20 > systemManager.stage.width || Math.abs(pathBit.vertical) * 20 > systemManager.stage.height)
	{
		gesturePath.push(pathBit);
		lastPoint = newPoint;
	}
	lastDrawPoint = newPoint;
}

If [Ctrl] is depressed, store the mouse location. If significant motion has taken place, draw a new line and store the relevant waypoint.
 
 
What does a PathBit object look like? This is fairly simple: based on the latest mouse move angle, we determine a very rough direction.

?View Code ACTIONSCRIPT
package com.voilaweb.tfd {
 
	import flash.geom.Point;
 
	public class PathBit
	{
		static public const UP:int    = 0x01;
		static public const DOWN:int  = 0x02;
		static public const RIGHT:int = 0x04;
		static public const LEFT:int  = 0x08;
		static public const U:String  = "U";
		static public const D:String  = "D";
		static public const L:String  = "L";
		static public const R:String  = "R";
		static public const UL:String = "7";
		static public const UR:String = "9";
		static public const DL:String = "1";
		static public const DR:String = "3";
		public var point:Point = new Point();
		public var horizontal:int;
		public var vertical:int;
		public var direction:int;
 
		function PathBit(point1:Point, point2:Point)
		{
			horizontal = point2.x - point1.x;
			vertical   = point2.y - point1.y;			
			point.x = point2.x;
			point.y = point2.y;	
			direction = 0;
			if(0 == horizontal && 0 == vertical) return;
			// Compute angle, in degrees: horizontal == new x, vertical == new y
			var angle:Number = Math.atan2(horizontal, -vertical) * 180 / Math.PI;
			if(angle < 0) angle = 360 + angle;
			if(angle >= 337.5 || angle < 22.5)
			{
				direction = UP;
			} 
			else if(angle >= 22.5 && angle < 67.5)
			{
				direction = UP | RIGHT;
			}
			else if(angle >= 66.5 && angle < 112.5)
			{
				direction = RIGHT;
			}
			else if(angle >= 112.5 && angle < 157.5)
			{
				direction = RIGHT | DOWN;
			}
			else if(angle >= 157.5 && angle < 202.5)
			{
				direction = DOWN;
			}
			else if(angle >= 202.5 && angle < 247.5)
			{
				direction = DOWN | LEFT;
			}
			else if(angle >= 247.5 && angle < 292.5)
			{
				direction = LEFT;
			}
			else if(angle >= 292.5 && angle < 337.5)
			{
				direction = LEFT | UP;
			}
		} 		
	}
}

You may have noticed that our constant values are also very basic; I was not lying when talking about a naive implementation.
 
 

?View Code ACTIONSCRIPT
private function main_encodeGesture():void
{
	var container:Container = null;
 
	switch(showingTab)
	{
		case FRIENDS_TL:
			container = friends_timeline;
			break;
		case USER_TL:
			container = user_timeline;
			break;
		case PUBLIC_TL:
			container = public_timeline;
			break;
	}	
 
	if(!container)
		return;
 
	// Analyze path	
	var children:Array = container.getChildren();
	var statusRow:UIComponent;
	var multiComponent:Boolean = false;
	var curStatus:StatusRow;
	var lastStatus:StatusRow = null;
	var pathBit:PathBit;	
	var overallPathString:String = '';
 
	for each(pathBit in gesturePath)
	{
		curStatus = null;
		for each(statusRow in children)
		{
			if(statusRow is StatusRow)
			{
				if(StatusRow(statusRow).isBounding(pathBit.point))
				{
					curStatus = statusRow as StatusRow;
					break;
				}
			}
		}
		if(lastStatus && curStatus != lastStatus)
			multiComponent = true;		
		lastStatus = curStatus;
		if(pathBit.direction & PathBit.DOWN)
		{
			if(pathBit.direction & PathBit.LEFT)
				overallPathString += PathBit.DL;
			else if(pathBit.direction & PathBit.RIGHT)
				overallPathString += PathBit.DR;
			else
				overallPathString += PathBit.D;
		}
		else if(pathBit.direction & PathBit.UP)
		{
			if(pathBit.direction & PathBit.LEFT)
				overallPathString += PathBit.UL;
			else if(pathBit.direction & PathBit.RIGHT)
				overallPathString += PathBit.UR;
			else
				overallPathString += PathBit.U;
		}
		else if(pathBit.direction & PathBit.LEFT)
			overallPathString += PathBit.L;
		else if(pathBit.direction & PathBit.RIGHT)
			overallPathString += PathBit.R;
	}			
	overallPathString    = main_cleanupPath(overallPathString);
	main_parseGesture(multiComponent, overallPathString, lastStatus, container);
}

The switch statement is used to figure out which container is currently displayed. Because this container contains a list of Tweets, I can then check whether the mouse gesture entirely took place within the confines of a single Tweet, or multiple Tweets. This allows me, for instance, to mark a Tweet as ‘read’ if the mouse did not wander outside that Tweet’s display area.
I then build a path string, called ‘overallPathString‘, based on the content of gesturePath.
 
 
At this point, you may be wondering about the implementation of isBounding(). Here it is:

?View Code ACTIONSCRIPT
	public class StatusRow extends WindowShade
	{
	...
		public function isBounding(point:Point):Boolean
		{
			var pt:Point = this.globalToLocal(point);	
			return (pt.x >=0 && pt.y >= 0 && pt.x <= this.width && pt.y <= this.height);
		}
	...
	}

A simple helper.
 
 

?View Code ACTIONSCRIPT
private function main_cleanupPath(path:String):String
{
	var i:int, l:int = path.length;
	var c:String, prevC:String = null, ret:String = '';
	for (i=0; i<l; i++)
	{
		c = path.charAt(i);
		if(prevC)
		{
			if(c != prevC)
				ret += c;	
		}
		else
		{
			ret = c;
		}
		prevC = c;
	}
	return ret;	
}

As you can see, all this method does is collapse redundant information. Actually, without it, we would never be able to identify any gesture.
 
 

?View Code ACTIONSCRIPT
private function main_parseGesture(multi:Boolean, op:String, status:StatusRow,container:Container):void
{
	if(!multi)
	{
		// Single-status gesture!
		if(PathBit.L == op) // L
			main_toggleReadFlag(status, true);
		else if(PathBit.R == op) // R
			main_toggleReadFlag(status, false);
	}
	else
	{
		if("13" == op) // DL, DR
			main_toggleReadFlagForAll(container, true);
		else if("31" == op) // DR, DL
			main_toggleReadFlagForAll(container, false);
	}
}

I am sure that this could be rewritten to be more cleanly; potentially with no hard-coding at all. But it does the trick:
- If the gesture happened within the confines of a single Tweet, and is a single “right-to-left” move, then we mark this Tweet as ‘read’.
- If the gesture happened across multiple Tweets and goes “top-right-to-bottom-left-to-bottom-right” then mark all Tweets as read.

Well, there you have it. At least it’s easy to maintain :)

If you enjoyed this post, make sure you subscribe to my RSS feed!