Pretty Buttons
Tonight's sample is for animating buttons in a generic way. As with all the samples on my blog, this is just one way you can approach things in iHD; nothing says it is the only way (or even the best way). The animation will be a simple "roll-over" effect whereby buttons highlight when the mouse is over them, highlight even more when they have the focus, and fade nicely back to their original appearance when they lose the mouse / focus.
Download the Code
You can download the ZIP file that goes with this sample; as with before, you will need to copy-and-paste a font into the ADV_OBJ folder in order to get the sample to work.
Here's a screenshot of what it looks like when the Scenes button has the focus, and the mouse is over the Pause button:
The General Approach
Three constraints I placed on building this sample:
1) Multiple buttons must animate at once if the user moves the mouse quickly; and
2) The sample must scale easily to an arbitrary number of buttons; and
3) The sample must make it easy to re-use graphics and dynamically generate buttons if needed
The first constraint means I can't use a generic catch-call cue (with associated markup-based animation) due to the 'a cue must end before it can begin again' rule. The second constraint means that I can't solve the first problem simply by having multiple id-coded cues to match the number of buttons; I will need script animation to be 100% generic. The last means that I will use dynamic text to create the buttons; whilst this isn't necessarily the best choice for creating your UI (you will always get better-looking text by rendering it as a graphic in a high-end program like Adobe Photoshop or Microsoft Expressions, where you can do anti-aliasing, drop shadows, etc.) it makes it easy to write a downloadable sample and it is still useful. It also saves on pixel buffer and makes it possible to do dynamic buttons based on (eg) content you download from the network.
So, the basic approach is to fire an event to script when a button gets / loses the mouse / focus, and to let script perform the got / lost focus animation for us. This means we can have multiple, overlapping animations (ie, several buttons fading out as the user moves the mouse over them all) which looks a lot cooler than only a single animation.
The Markup
Here's the salient piece of markup:
<
div style="btnBackground"
style:x="0px" style:y="100px"
style:backgroundImage="url('file:///dvddisc/ADV_OBJ/images/red.png')">
<div style="btnTextHolder">
<input style="text" mode="display" state:value="Play"/>
</div>
<button id="play" style="btnOverlay" state:focused="true" />
</div>
Here we have the four elements of the pretty button:
1) The div that serves as the background. In this case, it has a backgroundImage set to be a red-ish graphic. The style attribute is set to btnBackground, which specifies things like height and width for us, since they are constant (at least in this example)
2) The child div that is used to hold the text (this is simply because text can't be positioned by itself; it needs to be placed into a positioned container
3) The input box, which is used to display the text of the button (in this case, Play). Again, the style attribute is used to fill in all the default stuff like font name, size, etc.
4) Finally, the "overlay" button that is shown over the top of the background and the text. This is a solid black button that will have its opacity changed to simulate glowing / fading of the content underneath.
This basic block is repeated many times in the sample, just updating the button's x & y co-ordinates, backgroundImage, value (text), and id. All the other aspects are controlled by the referential style attribute, which sets things like width, height, and so on.
The Animation
The animation is simple:
<
cue begin="//button[state:pointer() and $Pointer = '']" dur="1f">
<event name="GotPointer" />
</cue>
<cue begin="//button[state:pointer() = false() and @id = $Pointer]" dur="1f">
<event name="LostPointer" />
</cue>
<cue begin="//button[state:focused() and $Focus = '']" dur="1f">
<event name="GotFocus" />
</cue>
<cue begin="//button[state:focused() = false() and @id = $Focus]" dur="1f">
<event name="LostFocus" />
</cue>
We use two XPath variables $Focus and $Pointer to track who (if anyone) most recently had the focus / pointer, and that makes the cues very simple, viz:
·When a button gets the mouse pointer (and no button previously had the pointer), fire the GotPointer event
·When the button that previously had the mouse pointer loses it, fire the LostPointer event
·Same for getting / losing focus
The Script
The script is pretty basic, too. A slightly edited version of startup code:
setMarkupLoadedHandler(Startup);
addEventListener("GotFocus", OnGotFocus, false);
addEventListener("LostFocus", OnLostFocus, false);
addEventListener("GotPointer", OnGotPointer, false);
addEventListener("LostPointer", OnLostPointer, false);
var
elementWithFocus = "";
var elementWithPointer = "";
var pendingUnset = {};
function
Startup()
{
SetXPaths();
}
function
SetXPaths()
{
document.setXPathVariable("Focus", elementWithFocus.toString());
document.setXPathVariable("Pointer", elementWithPointer.toString());
}
As with the last time, we set a "markup loaded" event handler in order to set the XPath variables so that the timing engine will work. We also add event listeners for the four events that the markup fires to script. Then we have three globals that help manage the state of the animations -- elementWithFocus, elementWithPointer, and pendingUnset. The purpose of the first two should be pretty obvious -- they hold the id of the element with the focus / mouse, respectively. The third one is used to control the cancelling of animations (via unsetProperty), and you'll see how it works in a little bit.
The event handler for getting focus looks like this:
function
OnGotFocus(evt)
{
var id = evt.target.core.id;
evt.target.style.opacity = "0";
pendingUnset[id] = false;
elementWithFocus = id;
SetXPaths();
}
When a button gets the focus, its opacity is set to zero, i.e. completely transparent. Since the button was originally a translucent solid black object, this gives the appearance of the content underneath "glowing". We then clear any pending unset operations for this element, update the element with the focus, and re-set the XPath variables.
The event handler for losing focus looks like this:
function
OnLostFocus(evt)
{
var id = evt.target.core.id;
evt.target.style.animateProperty("opacity", "0;0.6", 0.5);
var t = createTimer("00:00:00:12", 1, cleanup);
t.autoReset = false;
t.enabled = true;
pendingUnset[id] = true;
if (elementWithPointer == elementWithFocus)
elementWithPointer = "";
elementWithFocus = "";
SetXPaths();
function cleanup()
{
if (pendingUnset[id] == false)
return;
evt.target.style.unsetProperty("opacity");
}
}
First, we animate the opacity of the button from 0 to 0.6 over a period of half a second. This makes the underlying content appear to "fade out" as the black overlay increases in opacity. Next we kick off a timer for 12 frames (half a second at 24fps) to remove the script animation over-ride blocks (I don't know if I've talked about that in the past or not... basically any script-based animations have priority over markup-based animations, so you must "release" the property from script back to markup if you want to let the markup engine animate things again. We don't need to do that in this small sample -- there are no markup-based animations in this sample -- but in general it is a good thing to do). Then we enable the pending unset operations for this element so that the cleanup actually happens (I'll get to how it works in a sec -- promise!).
Then there's a little bit where we check to see if the element losing the focus is the same element as the one that currently has the mouse pointer. This check is done so we can "fake out" the animation engine into thinking that this element just got the mouse pointer on the next tick, thereby maintaining its "got pointer" appearance. (There are other ways to accomplish this, but this seemed like the quickest and easiest). If you comment out these two lines, you will notice that if you click a button with the mouse then move off the button with the keyboard, the button will appear "dull" rather than the normal semi-glowing appearance you would expect from a mouse-overed button.
Finally we update the XPaths. Woo-hoo.
OK, so the cleanup function (which is called from the 12-frame timer) is used to "unset" the opacity property so it can be re-animated by markup if need be. B-U-T... we use the pendingUnset object to make sure we don't accidentally unset an element that has been re-activated. Imagine this flow:
· time t: User moves focus to button A; it glows
· t + 1 frame: User moves focus to button B; it glows and button A starts to fade. We also create a timer to unset A in 12 frames
· t + 10 frames: User moves focus back to A; it glows again and button B starts fading (and kicks off its own timer)
· t + 13 frames: The first timer (from A) triggers and unsets A's opacity, making it not glow any more... But A still has the focus!
In order to stop this from happening, whenever a button gets the focus we clear its 'pending unset' state, and whenever it loses the focus we set the 'pending unset' state. When the timer comes around to firing, it makes sure the button is still a candidate for unsetting before doing any work.
The pointer code is very similar to the focus code, so I won't show it here.
Enjoy!