|
|
Labs.byHook |
Scripts, Tools & Methods Developed at Hook |
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.
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}~}{]} delim{[}{~matrix{3}{3}{a c t_x b d t_y u v w}~}{]}](http://labs.byhook.com/wp-content/plugins/wpmathpub/phpmathpublisher/img/math_956.5_7b8d6d0418fb24dddf3886c063870ef4.png)
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}~}{]} delim{[}{~matrix{3}{3}{1 0 0 0 1 0 0 0 1}~}{]}](http://labs.byhook.com/wp-content/plugins/wpmathpub/phpmathpublisher/img/math_971.5_d67fb2fcb18eebaebdc543e1ee203469.png)
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,
;
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,
;
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,
;
Let’s look at all this nonsense in action to drive the point home.
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:
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:
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.
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.
3D, Actionscript, Actionscript 2, Actionscript 3, adobe, air, airforandroid, Alchemy, android, as2, as3, Audio, avm, ByteArray, C, Canvas, charts, CoffeeScript, Cygwin, data, Data Structures, Debugging, Decoder, Diagnostics, Encoder, Event, EventDispatcher, facebook, Flash, Flow, Framework, gcc, google, Graph API, hangouts, haXe, html5, Image Extraction, ios, iphone, java, Javascript, JSFL, library, Like Button, Linux, llvm, Menu, Meta Balls, Motion Blur, MVC, Nav, OAuth, Object Extraction, Ogg, Ogg Vorbis, performance, php, Physics, pie menu, Pointers, processing, radial menu, real-time, sheep, Siox, Sound, State Machine, statistics, three.js, tricks, tutorial, twitter, Utilities, Vectors, Verlet, video, video chat, visualization, Vorbis, WebGL, Windows, |
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.
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.