Email Updates RSS Subscribe
Line

This blog is created and maintained by the technical team at Hook in an effort to preserve and share the insights and experience gained during the research and testing phases of our development process. Often, much of this information is lost or hidden once a project is completed. These articles aim to revisit, expand and/or review the concepts that seem worth exploring further. The site also serves as a platform for releasing tools developed internally to help streamline ad development.

Launch
Line

Hook is a digital production company that develops interactive content for industry leading agencies and their brands. For more information visit www.byhook.com.

Line

Go Go Gadget Flow

Line
Posted on May 25th, 2011 by Parker
Line

A cover flow is a great way to display and browse through discrete content. It feels like flipping through a magazine: easy and fast, but informative. Its a natural addition to many user interfaces, but many existing implementations are like black boxes with strict display rules. You add in your elements, and you end up with iTunes 7. We needed something more generic and reusable; those were the priorities. We take a look at the built in transform property of a DisplayObject, and how that can be used to build an flexible flow.

Get Adobe Flash player

Download the files here.

Breaking It Down

The flow can be broken into several pieces. The main class, called a Flow, handles things like user input, positioning of the elements, and z-sorting. Inside a Flow is an array of FlowContainer‘s, which is really where the magic happens. The FlowContainer actually encapsulates various transformations, and applies a percentage of those transformations to itself based on the position of the mouse within the Flow. Both the Flow and FlowContainer extend MovieClip, and this means that any DisplayObject can be added as a child to the FlowContainer and it will inherit its various transformations.

Wow, that was abstract. What do I mean when I say FlowContainer encapsulates transformations? Every DisplayObject has a property transform which holds various information about the object’s position and color. I’ll make an example with a transformation matrix, which is a three by three matrix. The template of the matrix is:

delim{[}{~matrix{3}{3}{a c t_x b d t_y u v w}~}{]}

The elements a, b, c, and d control the scaling of the DisplayObject, while tx and ty control the translation of the DisplayObject. The u, v, and w usually provide additional information if its needed, but in a standard two dimensional transformation they are unnecessary. The most basic transfomation matrix is the identity matrix. The identity matrix, when applied to the object, does absolutely nothing.

delim{[}{~matrix{3}{3}{1 0 0 0 1 0 0 0 1}~}{]}

Now, lets say we want to translate, or move, the object 56 pixels to the right and 32 pixels down. The transformation matrix we would use is,

delim{[}{~matrix{3}{3}{1 0 56 0 1 32 0 0 1}~}{]};

Easy. Scaling is similar, and uses a to scale horizontally and d to scale vertically. So to make our object 4.4 times wider and 2.6 times taller, we would use,

delim{[}{~matrix{3}{3}{4.4 0 56 0 2.6 32 0 0 1}~}{]};

Note that the translation is still there, now its just scaling at the same time. Rotation is the strangest, because its actually a compound operation. Without getting too mathy, the elements a and d of the array control scale and the elements c and b control skew. If you do both operations at the same time, you can rotate the object. I’m going to remove the scaling from the previous matrix for clarity. If we wanted to rotate the DisplayObject by 70 degrees (about 1.2 radians) we would use,

delim{[}{~matrix{3}{3}{{cos(1.2)} {-sin(1.2)} 56 {sin(1.2)} {cos(1.2)} 32 0 0 1}~}{]};

Let’s look at all this nonsense in action to drive the point home.

Get Adobe Flash player

There, that’s a little more interesting. Try manipulating a, b, c, and d to make the box kind of rotate. Also, note that a, b, c, and d are all relative to the blue box’s registration point in the top left, while tx and ty are the translation from the parent’s registration point to the child’s registration point. Why does this happen? If tx is 56 and ty is 32, why isn’t the blue box at pixel (56, 32)? This is because the when the display object is rendering, it doesn’t stop at its own transformation matrix. It actually traverses up the display list and concatenates (multiplies) each and every transformation matrix above it to find an absolute transformation matrix relative to the stage. The inverse of this is that if we change the transformation matrix of a DisplayObject, all of the children of the object inherit that transformation.

Go with the Flow

Back to flows. How can we use all these transformations to make a cool effect like the cover flow? We can think of each FlowContainer in the Flow as having two appearances. One is when its out of focus, or off to the side. The other is when it is in focus, either in the center of the screen (like in the classic cover flow) or directly underneath the mouse (like in our flow example). If we define those two appearances as matrices, we can easily interpolate between the appearances based on whatever input we’re using (the mouse, a slider, arrow keys, etc.). For basic transformations, our FlowContainer class keeps track of four variables.

private var lerp:Number; // the percentage of the effect currently being applied
 
private var positioning:Matrix; // the original position of the object, letting our transformations be relative
 
private var transformNone:Matrix; // the relative transformation when lerp is 0
private var transformFull:Matrix; // the relative transformation when lerp is 1

To set the values of these, we use a single method with some constants for context.

/**
 * Bind a transformation matrix to either the near or far appearence of the 
 * container. To specify whether the transformation happens near or away from 
 * the mouse, set the distance parameter to either Flow.NEAR_EFFECT or Flow.FAR_EFFECT.
 * @param	value		the transformation matrix to be bound
 * @param	distance	the distance constant at which the transformation is applied
 */
public function bindTransformEffect(value:Matrix, distance:String):void
{
	if (distance == Flow.NEAR_EFFECT)
		transformFull = value.clone();
	else if (distance == Flow.FAR_EFFECT)
		transformNone = value.clone();
	else
		throw new TypeError("The distance must be either Flow.NEAR_EFFECT or Flow.FAR_EFFECT");
 
	synchronize();
}

After these are set, we use synchronize() to match the appearance of the container with whatever matrices were set.

public function synchronize():void
{
	var lerpedTransform:Matrix;
 
	if (lerp != 0.0) {
		lerpedTransform = Util.lerpMatrix(lerp, transformNone, transformFull);
		lerpedTransform.concat(positioning); // make the transformation relative to the original position
	}
	else {
		// transformNone is cloned because otherwise the concat operation would compound
		lerpedTransform = transformNone.clone();
		lerpedTransform.concat(positioning);
	}
 
	transform.matrix = lerpedTransform;
}

There are two things to especially take note of in that code block. The first is at Util.lerpMatrix(), where we use linear interpolation between transformNone and transformFull. This is kind of bad, because linear interpolation doesn’t handle rotation correctly. But, it is a heck of a lot faster and easier than the alternatives: quaternions, slerping, or matrix decomposition/recomposition. If you’re like us and only need to use scaling and translation, here is the linear interpolation algorithm.

public static function lerpMatrix(value:Number, low:Matrix, high:Matrix):Matrix
{
	if (value < 0.0 || value > 1.0)
		throw new RangeError("The lerp value must be a percentage between 0.0 and 1.0.");
 
	var result:Matrix = new Matrix();
 
	result.a = value * (high.a - low.a) + low.a;
	result.b = value * (high.b - low.b) + low.b;
	result.c = value * (high.c - low.c) + low.c;
	result.d = value * (high.d - low.d) + low.d;
	result.tx = value * (high.tx - low.tx) + low.tx;
	result.ty = value * (high.ty - low.ty) + low.ty;
 
	return result;
}

The second thing to take note of is the concat() function, where we concatenate (multiply) the positioning variable onto the transformation. This is because its way easier for whoever is interfacing with this code to define the translations relative to wherever they’ve placed the object. The tx and ty value can look like 50 and -25, instead of random nonsense like 480 and 312. But, the transform property of the DisplayObject stores transformations relative to its parent, not itself, so we have to take care of that step ourselves.

In addition to transformations, we also store ColorTransformation objects and BlurFilter objects to lerp between and synchronize in our FlowContainer. After seeing how the regular transformations work, hopefully these are pretty easy to understand. Using only the FlowContainer object, we can make something like this:

Get Adobe Flash player

A Little Organization

The final step is organizing these little nuggets of transformation juju into a usable UI component. Its a lot simpler than you might think; the Flow object only has about three substantial functions. The first step though, is keeping track of the FlowContainers so that we can iterate through them quickly. After that, adding DisplayObjects is easy.

/**
 * Add a component to the flow. The component can be anything; if it is not already 
 * a flow container, it will be wrapped in one automatically.
 * @param	item	the component to be added to the flow
 */
public function addComponent(item:DisplayObject):void
{
	var fc:FlowContainer
 
	if ( !(item is FlowContainer) ) {
		fc = new FlowContainer();
		fc.addChild(item);
	}
	else
		fc = FlowContainer(item);
 
	items.push(fc);
	addChild(fc);
}

After all the items are added, we need to put them in their place. We use the variable track here; its just a invisible movie clip that lets us separate the Flow‘s area for interaction from its area for display. A simple spacing algorithm, followed by the z-sorting algorithm:

/**
 * Organizes the flowable items into an even spread along the track.
 */
public function organize():void
{
	var item:FlowContainer;
 
	for (var i:int = 0; i < items.length; ++i) {
		item = (items[i] as FlowContainer);
 
		item.x = track.x + ( Number(i + 1) / Number(items.length + 1) ) * track.width;
		item.x -= item.width / 2.0; // center the component in its slot
		item.y = track.y;
 
		item.freezePositioning(); // stores the flow containers current position as the start point for any relative transformations
	}
}
 
/**
 * Sorts the z-index of the items based on their distance to the mouse.
 */
private function zsort():void
{
	var i:int = 0;
	var j:int = numChildren - 1;
 
	// make i the index of the component which should have the highest z-index
	while (i < (items.length - 1) && items[i].effectPercent <= items[i + 1].effectPercent) { 		++i; 	} 	 	/* 	 Move this component to the top of the z-stack. Note that j will then 	 point to the next highest spot on the z-stack because of the post-decrement 	 operator. 	 */ 	swapChildren(items[i], getChildAt(j--)); 	 	// sort the items left of the middle item 	for (var k_left = i - 1; k_left >= 0; --k_left) {
		swapChildren(items[k_left], getChildAt(j--));
	}
 
	// sort the items right of the middle item
	for (var k_right = i + 1; k_right < items.length; ++k_right) {
		swapChildren(items[k_right], getChildAt(j--));
	}
}

Then, we just listen to the mouse and update the FlowContainers. The input could easily be replaced by a slider or arrow buttons for something more infinite.

private function moveListener(e:MouseEvent):void
{
	// relative to the envelope of the flow
	var mouseX:Number = e.stageX - this.x;
 
	for (var i:int = 0; i < items.length; ++i) {
		var distance = Math.abs(items[i].x + items[i].width / 2.0 - mouseX);
 
		// here, effectNone and effectFull are numbers which indicate the distance from
		// the mouse where there is no effect, and full effect. then it just maps the distance
		// between those and winds up with a percentage.
		var percent:Number = Util.map( distance, effectNone, effectFull, 0.0, 1.0);
 
		items[i].effectPercent = Util.clamp(percent, 0.0, 1.0);
	}
 
	zsort(); // if a new flow container is in focus, it needs to be on top!
}

Custimization

That’s the last of it really. Put all the pieces together, and you end up with something like this:

Get Adobe Flash player

To drive home how simple this framework makes it, here is all of the code used to configure the above flow.

var flow:Flow;
 
flow.farEffectDistance  = 150.0;
flow.nearEffectDistance = 0.0;
flow.maintainEffect = false;
 
for (var i:int = 0; i < 12; ++i)
	flow.addComponent(new SampleContainer());
 
flow.organize();
 
flow.transformEffect(new Matrix(1.5, 0, 0, 1.5, -30.0, -40.0), Flow.NEAR_EFFECT);
 
flow.blurEffect(new BlurFilter(4, 4, 1), Flow.FAR_EFFECT);

And to make it obvious how easy it is to customize it, here are some wacky settings that you might never want, but if you do, its possible. And that’s the important part.

Get Adobe Flash player

 

var flow:Flow;
 
flow.farEffectDistance  = 150.0;
flow.nearEffectDistance = 0.0;
flow.maintainEffect = false;
 
for (var i:int = 0; i < 8; ++i)
	flow.addComponent(new SampleContainer());
 
flow.organize();
 
flow.transformEffect(new Matrix(0.2, 1, 1, 0.2, 2.0, -2.0), Flow.FAR_EFFECT);
flow.transformEffect(new Matrix(1.0, 0, 0, 1.0, 25.0, -25.0), Flow.NEAR_EFFECT);
 
flow.colorEffect(new ColorTransform(1, 1, 1, 0, 0, 0, 0, 0), Flow.FAR_EFFECT);

And guess what? Those buttons will always work.

Download the files here.

Line
2 Responses to “Go Go Gadget Flow”
  1. You can apply many 3D transformations at once using a Matrix3D object. For example if you wanted to rotate, scale, and then move a cube, you could apply three separate transformations to each point of the cube. However it is much more efficient to precalculate multiple transformations in one Matrix3D object and then perform one matrix transformation on each of the points.

  2. Ben says:

    Nice, configurable code on this, but – not to pop your bubble – this is the Genie Effect, not Cover Flow. The Genie Effect is how the OS X toolbar menu works, the Cover Flow is the angled images in iTunes.


Leave a Reply

*

Line
Line
Pony