Friday, September 30, 2005

The OS X Dock, Javascripted

I've created the OS X "dock" in javascript, magnification, bounce and all. And here's how I did it. For those who want to peek before the explanation: The OS X-esque dock.

Note: This only works in firefox for right now!


Preface. It was 3:30 AM and, as usual, my insomnia was full steam ahead. My brain wouldn't shut off and I was a hundred miles from anywhere resembling slumber. Not wanting to work on actual contract work anymore, I figured it would be prudent to do nothing other than re-create the OS X dock, magnification, bounces, and all. Without using some half-assed onmouseover "I almost did it" hack. Well, anyway, it seemed like a good idea at the time.

Plug. See, I've been working on a web application toolkit that is more "javascript window manager" than "hey, it's another friggin template system", if they could even be considered opposites. None the less, I figured this would be a fine component to add to my arsenal, which already includes dialog boxes, cute little pop-up balloons, XP-esque sidebar navigation panes, animated tabbed content blocks, media players, slide panels, toolbars, a gallery selector, and several other bits of interactive goodness. All of which I hope to inject a bit more life, color, and responsiveness into web applications some day soon; also all part of my yet unnamed web application toolkit (How does "Fluid" sound?).

Context. So, back to the point, my rendition of the OS X dock. How about a visual context:


My screen capture fails to show that the mouse is indeed over the, erm, gem. I used whatever icons were somewhat aqua-like, and handy. So the gem, bomb, and fish somehow made their way in there. Wherever the mouse may roam, the icons do grow. Upon clicking any given icon, I added a little bounce action, true to form:


The "Satellite and Computer" icon is indeed hopping up and out of its usual domain. He'll bounce a few times, or until a bit of code commands it to stop.

The basic mechanics behind my javascript rendition are fairly straightforward, though I should warn you in advance: The code that I've concocted for this little demo is not straightforward, nor is it elegant, nor is it consistent, pretty or comfortable to interact with in any way except for via it's rendered, active form. It was just for fun, just for something to do, and I did indeed get my giggles out of it (however, it did not cure my insomnia). I apologize for the combination of underscore and java-style naming throughout this code. You're right, there really is no rhyme or reason to it. There is also a lot of dead, or inconsequential, code stuffed in this demo as well. Sometimes that's because this is ripped from a bigger prototype I'm developing, and sometimes it's because I let it rot and take up space because I'm a bad person. That being said, let's move on to how it works!

Icons. This dock needs to be fed icons of some type that have a transparent background. PNG was my choice because they were everywhere, and aquaesque, and of appropriate dimensions. 128x128 px seemed to scale well in this case, and I've not really tried any other dimensions. Anything smaller seemed to pixellate quite badly, and anything that is not uniform on both axes will not work right. My code makes an assumption that the icons are all of the same size in both dimensions. To feed the dock, you have two options. You can either declare your images using very shady IMG tags with extra attributes, as I did:

<img src="http://www.blogger.com/icon.png" is_a="dock_icon">

I also added an opacity setting for 75%, and a z-index of 355. As long as it's a higher z-index than your background and the semi-transparent bar behind the icons, you're set. I chose 355 because I ripped this demo out of a larger prototype I'm building with other components.

Another way to feed the dock is to add the icons programmatically, via the addDockItem(item) method (which I've forgotten to include in the demo). The method basically adds a node to the DOM and sets my shady attribute, then calls compute_dock(). compute_dock zips through the DOM, looking for anything with an attribute of "is_a" that has a value of "dock_icon", and rebuilds an array that I've aptly named dockItems. Also, the icon can also contain a value for the attribute "dock_click". I did this anticipating that at some point I will need to do other things on the click, like trigger a bounce!

Rendering. The entire method to render the dock is contained in babysit_dock() and arrange_dock(). babysit_dock ostensibly did something at some point in my [very short, 2 hour] development cycle, but now it simply calls arrange_dock. arrange_dock, in a nutshell, takes all the icons, and moves from left to right (according to the icons position in the dockItems array) and one by one positions the icons on the screen, absolutely, according to several factors. First, I'll outline the configuration values and safe assumptions for the dock mechanics explanation, even though right now they may not make a lot of sense. We'll get there.

Configuration & Assumptions. You can assume throughout the explanation and derived values that l is the number of items in the dockItems array.

icon_margin
: The distance in pixels to separate the icons by
icon_width: The width of each icon when not magnified. I actually bank on the fact that most browsers also scale the height commensurate with the complimentary dimension, but Safari does not. It's an easy fix, but I've just not gotten to it.
scale_coeff: A factor to scale the icons by, assuming the mouse is directly over the icon.
distance_coeff: The distance from the mouse pointer's current position to begin scaling icons, measured in icons (px derived as a function of icon_width).
cluster_coeff: A factor to shrink the distance between the x-coordinates of icons which are currently magnified, as to move them a bit closer together when they're bigger. This creates a little better illusion of magnification, and makes the dock look a little more uniform.
accrued_width: This is the distance, in pixels, that is gained by way of magnifying the items in the dock, above and beyond what is expected if all icons were of icon_width. This is computed so falloff_distance, and other factors, can be derived correctly. This defaults to 1 if a previous render cycle does not exist. If one does exist, it is carried over from the previous call to arrange_dock.
falloff_distance
: A derived distance in pixels as ((icon_width + (accrued_width/l)) * distance_coeff). This is the distance in pixels along the x-axis that should any icon exist be magnified given the previous render cycle.
spacing_interval: A derived distance equal to icon_margin + (accrued_width / l). This correctly computes the actual distance between each icon based on the magnifications determined in the previous render cycle.
item_pos: Position of the first item, before iterating through the dockItems array. This is computed as (bwidth/2) - ((l * (icon_width+spacing_interval))/2), or plainly put, half the distance of the actual width of the current (or previously rendered) dock shifted to the left of the absolute center of the browser window, so the icons lined up horizontally are centered.

Mechanics. If the mouse is within the "falloff_distance" of any given icon, I compute a modified scale_coeff that approaches zero as you travel further from the mouse pointer. That is to say, any icons within the falloff_distance are magnified based on how far they are from the pointer's position. An icon exactly centered under the mouse pointer will receive the greatest magnification, and those further away less so, in a linear fashion, depending on margins and clustering. If the icon is clicked, the bounce() function is called, the event is digested to determine the icon that was clicked, and the attributes "bounces" is read from the image element (if one does not exist when the dock is computed, a default value is assigned). The bounce function then kicks off a self-sustaining chain of function calls (via setTimeout) that bounces the icon up, then down (repeat ad bounces), modifying the last bounce or two to be partial bounce distances (to fake physics, and for fun). Overall, if you look in the code, I do a lot of really pointless rounding and even/odd meddling with some of the values, and slows everything down (among other things). These were almost all cheap attempts at reducing "jitter".

Problems. Jitter is one of the problems with this prototype that is probably the most annoying of them all. It's the phenomenon where, because the render cycle scales the icons so predictably, in one cycle it will scale certain items up based on their distance, which modifies their distance from the pointer on the next render cycle (because of the icon scale change); the next render cycle then computes new values because their positions have changed, which sends the icons back to the previous cycles position (or another that requires readjustment). I do have a solution for this, but I've not tested it nor implemented it yet, so any suggestions as to how to solve this are welcome. Another issue is attaching the mousedown event with a handler that lets me ignore the click as a trigger for a bounce, and initiate some kind of drag and drop so I can pull the icon out, shrink the space it previously occupied, and not have all hell break loose in the process. I've a good "down and dirty" way to do it, but I'm trying to play nice with script.aculo.us, so I'll just have to experiment more.

Comments. Like I said, it's not pretty, or comfortable code, or even structure, but it was a bundle of fun to write and play with. And besides, who doesn't like the dock? Maybe I shouldn't have asked that. If for some insane reason you want to use this code, or base something off of it, I'd appreciate a shout-out, or a link, or both. I'm working on a nice, clean, "I'm a good coder, really" version of this, and I'll post that when I reach it in the pipeline.

Demo. Until then I hope you enjoy the demo: Enough talk, without further ado, The OS X-esque dock. Any and all comments, ideas, and improvements are welcome, along with any requests for other components or widgets you would like to see!

The future. I have been meaning to start a blog for ages and have plenty of topics mapped out, some of which include my experiments, prototypes and thoughts on combining web services (a la weather, commerce, and such) and many of the new handy-dandy client-side libraries out there (see my links on the right), [applied] asynchronous eventing and "cohabitating component interaction" for lack of a better term, novel methods for gathering site and page usage statistics in an age where clicking things doesn't mean you're making usable log crumbs (either because statistics packages don't aggregate asynchronous requests, or because we don't send them usable trails of data), a handful of topics on affiliate marketing and e-commerce including ideas for a consolidated, standard cart web-service interface (what a can of worms that is), thoughts and ideas on how to manage data feeds and product definition web services, consolidated catalogs and meta-search functionality (hello kayak, goodbye mom-and-pop-affiliates), and the wide, wide world of tagging, and why I totally and utterly love it (with sidebars on contextual advertising, where it needs to go, and why cross-selling could be oh so much better).

Take care,
-john.

Thursday, September 29, 2005

test post

tap tap, is this thing on?