Showing posts with label animation. Show all posts
Showing posts with label animation. Show all posts

Monday, March 02, 2009

Super Easy Fast-Forward Reverse Carousel with YUI 3!

I've been mucking with YUI 3 and playing around with carousels using their Animation Utility. If you don't know what a carousel is, here's a suggested reading. Our carousel is a little different. Ours rewinds when we reach the end or fast-forwards when we're on the first and go to the last element. We come up with something like this --



Play with it. It'll work on all the big browsers -- IE, FF, Safari and Opera. Think of the carousel as a queue with a start and an end. So, all we're doing is traversing the queue from the front to the end and back again. It's a simple exercise which we probably did in Data Structures 101!

Go ahead and view the source. The code is easy to understand and it'll just be another YUI 3 example.


There are several important points to remember. We'll use an HTML template to represent the carousel. Our JavaScript will reference this for the animation --
            <div class="view-port">
<ul class="carousel">

<li>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</li>

<li>
<div>5</div>
<div>6</div>
<div>7</div>
<div>8</div>
</li>

<li>
<div>9</div>
<div>10</div>
<div>11</div>
<div>12</div>
</li>

<li>
<div>13</div>
<div>14</div>
<div>15</div>
<div>16</div>
</li>

<li>
<div>17</div>
<div>18</div>
<div>19</div>
<div>20</div>
</li>

<li>
<div>21</div>
<div>22</div>
<div>23</div>
<div>24</div>
</li>

</ul>
</div>
We've grouped our items -- this is the unordered list -- and each group represents the "viewable" area and each list contains content. Content of course can be anything -- a div, another list, an image, a table or a collection of all those things. It's the unordered list with the class name "carousel" that we'll be animating. The important thing to remember is that we can easily calculate the width of each list by using the element's "offsetWidth" property. There's no need to hardcode anything. This is the width of the viewable area and this is also the distance which we'll move by. If we view the source, this is the variable WINDOW_WIDTH.

Another thing that we're doing is that we never set the animation object's "from" attribute. This is because "from" is implicit. Once we move to a position, we know where we're at ( obviously! ). We just need to specify where we want to move and so throughout the animation, we just set the "to" attribute. In reality, we can always get our current position by calling the method getX() on the element that we're moving.
                function scrollLeft() {

if (_currentIndex === _startIndex) {

_currentXPosition = -1 * WINDOW_WIDTH * (TOTAL_PAGES - 1) + _carousel.getX();
_currentIndex = TOTAL_PAGES - 1;

} else {

_currentXPosition = _carousel.getX() + WINDOW_WIDTH;
--_currentIndex;

}

}

where _carousel is the unordered list that we're moving.

That's it. It's pretty straightforward and easy. Feel free to use the code and have fun!

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!

Friday, June 15, 2007

Bindows 3.0 :: 3 Animations in a Dashboard

Early this month, Bindows 3.0 was released.

One of it's main features is the animation library. I'd written about it before.

To celebrate 3.0's release, I've put the animation library to good use by using their three animators - BiSizeAnimator, BiOpacityAnimator, and BiLocationAnimator - and created this dashboard.


It's an imitation of the cool looking IconDB Dashboard ( IconDB never had animated "transitions"; so, I've added these in various places of my example ).

Here's some functionality from the example.

Observe change in opacity by selecting a background from the Select Background container. Watch the background fade out and then watch the new one fade in.

Click on the expand button in the Welcome container and watch as the container transitions to an expanded container. Then, collapse and watch it shrink.

Drag one of the containers. Then, drag the other. Do this as many times as you'd like and then click on Undo Move. Watch as containers move back to their previous positions.

There's a lot in this example, but rather than talk about the design of the dashboard or how it's constructed ( I'll blog about that in a later entry ), for now, I'll focus on the animation library.

The
animation tutorials on the Bindows site are excellent, but they focus on the declarative ( XML ) piece. With the dashboard example, we'll see how to use it with the JavaScript API. This will supplement the tutorials.

One of the nice things about the animators is that they're separate from the components. They animate BiComponents. So, if you've built a custom component from an earlier version of Bindows, you don't need to change your component to use the animator. You just simply switch to Bindows 3.0, construct an animator by passing in the component and then start the animation.

General Pattern
In general, the constructor for the three animators follow this pattern --

BiAnimator([parameters for the type of animation], nSpeed, bLoop, nAccType, oComp, nFrameRate, bAutoStart)

Each animation type -- Size, Opacity and Location -- requires specific parameters to be passed and so, they differ depending on the type of animation that we want to do.

nSpeed is how fast you want your animation to occur in milliseconds. You can set it directly as a number or a constant ( i.e. BiSizeAnimator.SPEED1, BiOpacityAnimator.SPEED1, BiLocationAnimator.SPEED1, etc. Note that the speed constants differ depending on the type of animation ). You can even use strings - "slowest", "slow", "normal", "fast" and "fastest."

You can repeatedly do the animation by passing bLoop as true. In the dashboard example, all the animators have this set to false.

You can set acceleration, nAccType, for your animation. Typically, the animation is run at "constant speed" meaning that throughout the entire animation cycle, the animation is done in the same speed. You can accelerate, decelerate or "go slow then fast then slow" by using these constants - BiComponentAnimation.SLOW_TO_FAST, BiComponentAnimation.FAST_TO_SLOW, BiComponentAnimation.SLOW_TO_SLOW. Constant speed is BiComponentAnimation.CONSTANT_SPEED.

oComp is simply the component that the animator acts upon. Only BiComponents can be animated.

nFrameRate is the number of frames per second for the animation. We'll use the default -- BiFpsGenerator.DEFAULT_FRAME_RATE. Just pass in a natural number. We can play with this setting to get a "smooth" animation. In the dashboard example, using the default frame rate results in a smooth animation.

bAutoStart allows you to auto start the animation. We typically set this as false because we want to control when we want to start the animation.

It's Event Driven
So far, we've looked at what properties we can set on an animator. Though this is useful, it doesn't tell us how to interact with the animator. An animation is useless if you can't control it.

Starting an animation is easy -

  1. Construct an BiComponent
  2. Construct an animator ( by passing in the parameters to the constructor as described above )
  3. Call the start() method

The animation starts and then ends., but how do we know when it ends?

We'll listen for the "animationend" event. When that occurs, we can perform our next set of operations in the event handler. In the next section, we'll see in the dashboard example how we do this.

Note that during the animation, the animators constantly fire the "frameprogression" event. If we want, we can check for that, but in our example, we don't do that.

Opacity
The dashboard's background opacity is controlled by the BiOpacityAnimator. Setting opacity allows you to fade components. Components can gradually fade in or fade out.

The fade out occurs when we select a background image ( ex. Olympia, etc. ). The current background fades out and once that animation ends, the fade in of the Olympia background begins.



Notice how the fadeOut animator listens for the "animationend" event. When that event is fired, the handleAnimationEnd method is invoked, which does three things --

  • Starts the fade in animation
  • Disposes the fade out animator

When the fade in completes, we dispose the fade out animator.

It's important to remember that the "animationend" event drives everything. We'll see this pattern in the other animations as well.

Change Size
You can change the size of the container by controlling the BiSizeAnimator. In the Welcome container, click on the collapse button at the upper right. You'll see that the container acts like a drawer closing. Click on the expand button and the animation goes the other way.



There's not much difference between this example and the opacity one. The opacity animator does have an extra last boolean parameter, bForward, which allows the animation to progress forward or in reverse ( fade out ).

Changing the Location
Changing the location of the container is done by using BiLocationAnimator. Move the containers around the dashboard. Then, click on Undo Move. The container moves back to their previous position.



You can review the opacity and the location animators here. The size animator is found here.

The example is here.

Have fun!

Saturday, May 19, 2007

Simple Fun Faders

If you're using pre-Bindows 3.0 Beta, here's a simple way to do component fading in Bindows.

When Bindows 3.0 is released, it'll do a lot more including resize and relocation animations. Hopefully, this little fader will wet your appetite for that release.

For a preview of the animation capabilities found in Bindows 3.0, check out my earlier writeup.

Here's an example of what the fader object, MyFader, can do. We're cycling through a six photo collection of 1/24 model cars from Tamiya --




Fading is achieved by dynamically changing a component's opacity.
One of the great things about using a toolkit like Bindows is that you don't have to worry about browser specifics like setting filter ( IE ) or -moz-opacity ( Firefox ).

All of that is abstracted for you.
With Bindows, this is easy to do by repeatedly calling a BiComponent's setOpacity() method.

You basically setup a timer and then at intervals call setOpacity() incrementing by 0.1 until you reach 1 ( "fade in" ) or decrementing by 0.1 until you reach 0 ( "fade out" ).

There's not much coding behind it. Here's what the reusable object, MyFader looks like --



MyFader isn't a component. It doesn't need to be because MyFader doesn't need to be rendered, but it does need to dispatch events.

So, we make it a BiEventTarget. When a fade starts, MyFader dispatches the "fadestart" event and when it ends, MyFader dispatches the "fadeend" event.

Use these two events to coordinate fading. This is what we're doing when we're swapping the six model cars. You can view that source here.

Not all fading is aesthetic. We can also use fading to provide visual feedback. Here's an example of a cascading menu with fade-ins when a list is updated. To see it in action, select an item --



Note that the cascading menus are lists and each item is a BiListItem, but we don't fade each item. Instead we fade the entire list. It's always better to animate the container/parent rather than each child ( much like event delegation where you handle events at the parent level ).

It's much easier to coordinate by fading one component rather than many. JavaScript after all uses a single threaded model.

You can get all the code here including the MyFader object.

To see the model car example, go here.
The cascading list example is here.

Have fun!

Sunday, March 04, 2007

Bindows 3.0 Beta :: Animating Yahoo's Expanding and Collapsing Tabs

I was fortunate enough to be one of the lucky ones to test drive the Bindows 3.0 Beta. The 3.0 version has some significant new features --

  • An animation library
  • Vector graphics support for all browsers
  • Advanced gauge support for all browsers
  • Enhanced charting capabilities
  • An improved grid panel component for better layout performance
You can read more about it here.

I was most interested in the animation library. I'm a big fan of the UI Design Patterns from Yahoo! and a lot of their patterns involve relocating, resizing and fading control elements. All this required the ability to animate which until now, Bindows lacked.

The example we'll imitate is taken from Yahoo! (they do practice what they preach!). It's the expanding/collapsing grid menu tabs found on the Yahoo! home page. Here's our imitation which I'm sure you'll recognize --



Mouseover the tabs and you'll see the animated expanding and collapseing tabs at work. Note that the tab pane content is static. They're just images, but in reality, they can be any component.

You can find the real version here.

There are three types of component animation found in Bindows 3.0. You can --

  • Resize components
  • Change component location
  • Visually fade components
These animation types correspond to three animator objects --

  • BiSizeAnimator
  • BiLocationAnimator
  • BiOpacityAnimator
All three animators are derived from BiComponentAnimation. Since the grid menu tabs only use the BiSizeAnimator and the BiLocationAnimator. So, we'll only talk about those.

Note that animators aren't components. They animate BiComponents or their derivatives. So, think of animators as objects that provide resizing, relocating and fading behavior to components. We'll see how we use these animators in a minute.

From our implementation, you'll notice that there are three components that are resized and one component that is relocated.

The resized ones include the container for the grid tab menu and the "middle body", the area between the first and the bottom tabs --

It also includes the area below the bottom tabs ("bottom body") --

With that in mind, here's how the grid tab menu behaves.

Mouseover the top menu buttons and the grid tab menu container and the middle body increase in size, while at the same time, the bottom menu buttons move down making way for the middle body.

Mouseover the bottom menu buttons and if the top menu buttons are collapsed, the bottom body and the grid tab menu container increase in size.

If the top menu buttons are expanded, mouseover the bottom menu buttons and the middle body collapses, the bottom menu buttons move up, the bottom body and the grid tab menu container expand.

All of the animation is synchronous meaning that the components move together. This is important because it just wouldn't look right if the animations occurred at different times and rates.

For the three resizeable components ( grid tab menu container, middle and bottom body ), we'll use three BiSizeAnimators. For the bottom menu tab buttons, which we move up and down, we'll use a BiLocationAnimator. Here is the bottom menu tab highlighted within a dashed box --

Here's the code for creating a BiSizeAnimator for the grid tab menu container --



Basically, you pass in the "from" and "to" widths and heights. These make up the first four parameters. The fifth parameter is the speed of the animation. We've chosen to use BiSizeAnimator.SPEED4 which is pretty fast. SPEED1 is the slowest and SPEED5 is the fastest.

The sixth parameter determines whether we want the resizing to autostart. In our case, we don't want that. We'll start the resizing ourselves. So, we set that to false.

The seventh parameter deals with acceleration. We want our acceleration to be constant all the way. So, we've chosen BiComponentAnimation.CONSTANT_SPEED. We could have chosen FAST_TO_SLOW, SLOW_TO_FAST or SLOW_TO_SLOW.

The eighth parameter associates the component with the animation. In this case, we're animating the BiComponent tabMenuContainer.

The final parameter sets frames per second. For this example, setting this value to 100 works nicely.

More information on the BiSizeAnimator can be found here.

We initiate the the grid tab menu container resize when we call the method start() --



It's important that we call start() in the right place and for us, we call it when we expand the middle or the bottom body. Here, for example is the code for expanding the middle body --



The bottom body has something similar (sizeAnimator2) --



The constructor for the BiLocationAnimator is identical to the BiSizeAnimator. They differ only in the value of the fifth parameter. In the BiLocationAnimator, we use the string "fastest" rather than the BiSizeAnimator.SPEEDX primitive constant.



More information on BiLocationAnimator is found here.

I noted earlier that all "the components move together." To do that, we start the animations one after another --



Where sizeAnimator resizes the middle body and as noted earlier, sizeAnimator3 resizes the grid menu tab container. The locationAnimator moves the bottom tab menu up or down.

If you review the BiLocationAnimator constructor for the locationAnimator, you'll notice that it's set to move from top to bottom. However, we don't always want to go in that direction.

We can change when we need to by calling the method setToFrom to go from bottom to top before calling start() --



Notice also that the locationAnimator listens for the "animationend" event. "animationend" is an important animation event because many times, you want to immediately perform an action after an animation.

In our case, we'll display the image in the tab pane using the showContentHandler event handler.



You can review the entire code here. The implementation is found here.

Note that we're using a modified theme based on the "Beige" theme provided in the toolkit. You can review that here. I added the three selector rules at the bottom of the theme.

Also, remember that this example requires Bindows 3.0 Beta. It will not work with earlier versions of Bindows.

As always, feel free to modify, use, comment and improve on the example. If you do use, just let them know where you got it.

Have fun!