Sunday, February 24, 2008

How to Build and Fly a Spaceship

Well, not exactly. We'll build and fly a spaceship built with the YUI library ( I'm using the 2.3.0 library; I typically don't upgrade that frequently especially when things aren't broken ). Once again we'll explore event handling and extensively use the animation library.

For part 1, We'll focus on listening for keyboard events and then using the YAHOO.util.Motion object we'll fly the spaceship and fire our laser gun into virtual space. In part 2, we'll focus on listening and subscribing to "hits". After all, if you fire a gun there has to be a target!

Without further adieu, here's the flying spaceship --



The beautiful spaceship is from Everaldo Coelho
. I found it on iconfinder. To fly Everaldo's spaceship, use the ARROW keys to move in the direction that you want to go. If you want to change directions, hold down the SHIFT key and use the LEFT or RIGHT ARROW keys. We've made this example simple by only supporting directions and movement at 45 degrees ( 0, 45, 90, 135, 180, 225, 270, 315 and 360 ).

To fire the gun, just press the SPACE BAR. Bullets spew out and then disappear at a distance.

You'll notice that when the spaceship moves, it has two streams of light adding a propulsion effect which appear behind the wings. These, like the spaceship and the bullets, use the YAHOO.util.Motion object.

In addition, we should note a few important concepts.

First, the spaceship is a PNG sprite consisting of eight spaceships rotated at 45 degree intervals. The images are rotated with a bit map editor and then using the Web Performance CSS Sprite Generator the sprite image along with their selector rules are created.

When we change the spaceship's direction, what we're really doing is flipping through the eight spaceships in the sprite. Each spaceship is used as a background image ( not an image element i.e. <img> ). Sprites greatly benefit performance in that they're loaded once ( reduce the number of HTTPRequests ) and are then cached. We've discussed that before.

Second, it's important to understand how we determine where we want to move. Because we know the angle and the distance of each movement ( for each key press ), we just need to calculate the x and y coordinates of where we want to go. Fortunately, that's pretty easy to do by recalling the basic geometry of a circle.

We'll assume that the center of origin is always at (0,0) and if we do that, then we can use these two equations to determine the "to" point (x, y) --

x = a + rcos(t)
y = b + r sin(t)

a and b are the "from" point (a, b). t is the angle ( theta ) which in our case is always increments of 45 degrees. A good refresher about the circle is found on Wikipedia.

Third, when we fire a bullet or add a propulsion effect, we dynamically add the div element to either the body or the spaceship. In both cases, we add these elements to the DOM with the display set as "none". It's important that we do this to prevent reflow or the redrawing of the elements on the page. Reflows make the elements on the page flicker. Once we've added the element, then we change the display to "block".

Likewise, when we remove an element, we set the display to "none" and then call removeChild(). Removing an element while it's displayed also causes reflow.

Fourth, we listen for the key events through YAHOO.util.KeyListener. We listen for the ARROW keys, the SHIFT and ARROW keys, the SPACE key and the SHIFT and SPACE key. Then, we call the appropriate event handler. So, all the interaction begins with the KeyListener.

You can see the minutae by viewing the source. Feel free to use and improve on my effort. If you do use it, let them know that I was the original author and send me a note on what you did to improve it.

Have fun!

Sunday, February 17, 2008

Animated Horizontal MenuBar

The animated horizontal menubar isn't the most practical or useful menubar around, but it does allow us to explore events with complex workflows. We'll also explore how we fire events programatically.

Here's our horizontal menubar --



The menubar is based on the YUI MenuBar ( in fact, I've kept some of the original comments from the YUI examples ). There are many examples and it's well documented. So, it's a good choice to add to it's behavior. The workflow that we want is simple --

Mouseover a menu item and the "cap" moves toward the item. Once the cap arrives, the menu item changes background and the submenu appears. Mouse to another menu item and the current submenu hides. The new menu item behaves as before.

Having users explore by mouseover is easier than having users click on each item. Animation allows us to "control the pace" at which our users explore.

Our workflow involves a sequence of events --
  1. The container containing the menubar listens for the mouseover event
  2. When a mouseover event is fired, the "cap" moves towards the menu item
  3. With the animation, we subscribe and handle the onStart and onComplete events for the animated object
  4. When onComplete occurs, we fire a custom event to the menubar which then displays the submenu
Note that when we use the term "listen" we mean that we listen for non-custom or "normal" events. An example of these are mouseover, mouseout, click, etc. They're fired when the user interacts with our application. Essentially, these are the basic events that you deal with in an web environment.

When we use the term "subscribe" we mean custom events or events that are fired programmatically. We're using three of these --
  • Both onStart and onComplete are found in YAHOO.util.Motion, the object that moves
  • A "showmenu" custom event attached to the MenuBar, fired once the animation completes
Here's the mouseover event handler, moveThing. It's invoked when you mouseover the container containing the menubar --
 var moveThing = function(e) {
clearID>-1?clearInterval(clearID):"";
var target = YAHOO.util.Event.getTarget(e);
var id = target.id;
var menuBar = (id == "Com" || id =="Shop" || id == "Ent" || id == "Inf");
if (menuBar) {
(doingMenu && doingMenu !== target)?hideMenu(doingMenu):"";
doingMenu = target;
oMenuBar.myCustomEvent.unsubscribe(handleCustomEvent, oMenuBar);
oMenuBar.cfg.setProperty("autosubmenudisplay", false);
var x = YAHOO.util.Dom.getX(target);
var y = YAHOO.util.Dom.getY(target)-4;
!w?w = parseInt(YAHOO.util.Dom.getStyle(target, "width")) + parseInt(YAHOO.util.Dom.getStyle(target, "padding-right")) + parseInt(YAHOO.util.Dom.getStyle(target, "padding-left")) + "px":"";
YAHOO.util.Dom.setStyle(thingToMove, "width", w);

var attributes = { points: { to: [x, y] }};
var anim = new YAHOO.util.Motion(thingToMove.id, attributes, 1, YAHOO.util.Easing.easeOut);

var handleOnStart = function(e) {
var el = anim.getEl();
YAHOO.util.Dom.setStyle(el, "display", "block");
}

// BEGIN :: Create/subscribe custom event
var handleCustomEvent = function(type, args, me) {
if (args.length>0) {
var current = parseInt(args[0].getAttribute("index"));
var item = me.getItem(current);
item.cfg.getProperty("submenu").show();
}
}
oMenuBar.myCustomEvent.subscribe(handleCustomEvent, oMenuBar);
// END :: Create/subscribe custom event

var handleOnComplete = function(e) {
anim.onStart.unsubscribe(handleOnStart);
anim.onComplete.unsubscribe(handleOnComplete);
anim=null;
if (doingMenu === target) {
var parentNode = doingMenu.parentNode;
parentNode.className.indexOf(" showing-menu")==-1?parentNode.className += " showing-menu":"";
oMenuBar.myCustomEvent.fire(target);
}
}

anim.onStart.subscribe(handleOnStart);
anim.onComplete.subscribe(handleOnComplete);
anim.animate();
} else if (id == "Container") {
doingMenu?hideMenu(doingMenu, 1000):"";
}
}

Everytime you mouseover the container, we create a new animation object. We do this so that the animation doesn't stop when we consecutively mouseover other menubar items. So, we intentionally don't reuse the YAHOO.util.Motion object.

To prevent memory leaks and to "clean up" our event handling we unsubscribe our events in the onComplete handler --
var handleOnComplete = function(e) {
anim.onStart.unsubscribe(handleOnStart);
anim.onComplete.unsubscribe(handleOnComplete);
anim=null;
if (doingMenu === target) {
var parentNode = doingMenu.parentNode;
parentNode.className.indexOf(" showing-menu")==-1?parentNode.className += " showing-menu":"";
oMenuBar.myCustomEvent.fire(target);
}
}

The onComplete handler changes the background image of the MenuItem ( by appending the class "showing-menu" ) and fires the "showmenu" custom event. This only occurs if the menu item is the same one that was the target of the mouseover in which case the menubar object subscribes and handles the event by displaying the submenu --
 var handleCustomEvent = function(type, args, me) {
if (args.length>0) {
var current = parseInt(args[0].getAttribute("index"));
var item = me.getItem(current);
item.cfg.getProperty("submenu").show();
}
}

The attribute "index" is a custom attribute added to each link in the HTML. It's used to get the menu item. Every YUI MenuItem ( or for that matter every Menu ) has a configuration property ( cfg ). For menu items, we've set the "submenu" property and so, in the event handler, we'll call "show()" to explicitly display that.

There's also an equivalent hide() that we call when we need to hide the submenu. Once again, we see symmetry of operations --
 var hideMenu = function(menu,t) {
// t is the time that it waits to hide it
var current = parseInt(menu.getAttribute("index"));
var item = oMenuBar.getItem(current);
var parentNode = menu.parentNode;

var hideIt = function() {
parentNode.className = parentNode.className.indexOf(" showing-menu")>-1?parentNode.className.replace(" showing-menu",""):parentNode.className;
item.cfg?item.cfg.getProperty("submenu").hide():"";
}
clearID=(arguments.length==2)?setTimeout(hideIt,t):hideIt();
}

You can find the example here. Feel free to view, use and improve it.
Have fun!