Thursday, October 4, 2012

Using Timelines to Manage Animations with TweenLite

Yesterday, I was writing about how to manage multiple animation sequences in jQuery. The issue I was attempting to solve was how to organize all the animations so you can clearly see the sequence, easily reuse sections, and, in general, just make you life easier. My conclusion after making the attempt to build my SVG-based impulse wave animation was that I may have hit the point where jQuery wasn't going to be the best tool to solve my problem.

So I decided to rewrite the whole thing using GreeenSock's JS Animation Platform. Included in this library is the handy TimelineLite (or TimelineMax depending on your needs) which can organize and control a collection of animation sequences. Its a rather robust little tool that can manage nested timelines, add labels, arbitrary functions, and then let you control the whole animation from one convenient object. I figure an example is better than a bunch of words so let's look at the new and improved version of the code.

First, TweenLite has an SVG plugin which depends on Raphael. That meant I needed to swap out the SVG creation I was doing and start using the Raphael library. That support alone made this an easy sell to switch over and we're not even at the timeline yet:


var MAX_R = 25,

paper = Raphael(0, 0, $(window).width(), $(window).height()),
$svg = $('svg'),

wave = paper.circle(-1, -1, 1)
.attr('id', 'wave')
.attr('stroke', 'black')
.attr('fill-opacity', 0.4)
.attr('stroke-opacity', 0.3)
.hide(),

charge = paper.circle(-1, -1, 1)
.attr('id', 'charge')
.attr('fill', 'red')
.attr('stroke', 'none')
.attr('fill-opacity', 0.4)
.hide(),

pulse_x, pulse_y, intensity,
timeline, pulse;

timeline = new TimelineLite({
paused: true
});



Now we have the two SVG circles that will represent the wave charging and pulse burst created as Raphael objects. Additionally, we'll create a paused timeline since nothing should happen until the mouse is clicked somewhere on the page.

Now, let's define the animations using timeline as the container to manage everything:


/*
Setup the timeline. There are 4 distinct phases to
out pulse animation:
1 - MouseDown Initial charging - wave expands out to MAX_R
2 - Mouse still down and fully charged, show pulsing red circle
3 - Mouse up - collapse wave back to 0 radius
4 - Release the wave expanding it until it animates off the screen

Each part has a label so we can move the play head to that
part of the animation. Additionally, functions are added to
the timeline to setup, repeat, or complete various stages of the
animation.
*/
timeline.insertMultiple(
[

/*
Part 1: Charge the wave.
* Add the label for playback control
* Add function to reset wave attributes and show
* Add the animation
* Add a function to show the pulsing circle
*/

'charging',
function ()
{
wave
.attr('fill', 'white')
.attr('fill-opacity', 0.4)
.attr('stroke-opacity', 0.3)
.show();
},

TweenLite.fromTo(wave, 0.5, {
raphael: {
r: 1,
'stroke-width': 1
}
}, {
ease: Linear.easeNone,
raphael: {
r: MAX_R,
'stroke-width': 5
}
}),

function () {charge.show();},

/*
Part 2: Charged and waiting.
* Add the label for playback control
* Add the animation
* Add a function to repeat the animation
*/

'charged',
TweenLite.fromTo(charge, 0.5, {
raphael: {
r: 1,
'fill-opacity': 0.8
}
}, {
ease: Linear.easeNone,
raphael: {
r: MAX_R,
'fill-opacity': 0.05
}
}),

function () {timeline.play('charged')},

/*
Part 3: Start discharge process.
* Add the label for playback control
* Add function to hide the pulsing circle
* Add the animation
*/

'discharge',
function () {charge.hide();},

TweenLite.to(wave, 0.25, {
ease: Linear.easeNone,
raphael: {
r: 1,
'fill-opacity': 0,
'stroke-width': 1
}
}),

/*
Part 4: Emit the wave.
* Add the label for playback control
* Add the animation - save a reference for later
* Add function to hide everything
*/

'pulse',
(pulse = TweenLite.fromTo(wave, 0.15, {
raphael: {
r: 1,
'stroke-width': 1,
'stroke-opacity': 0.3
},
}, {
ease: Linear.easeNone,
raphael: {
r: 1000,
'stroke-width': 100,
'stroke-opacity': 0.1
}
})),

function ()
{
charge.hide();
wave.hide();

pulse_x = -1;
pulse_y = -1;
}

], 0, 'sequence', 0.001);


Ok, that's a fairly large chunk code so let's break it down a bit. First, there are several ways to add elements to a timeline. I chose to use insertMultiple() since I already knew how everything was being sequenced ahead of time. I highly recommend reading up on the documentation to see all the different alternatives available. Next, if you look at the first three elements added to the timeline, you can see how much power this tool has for building complex animations:



/* Add a label so we can refer to it in timeline.play() */
'charging',

/* Add a function that will ensure the SVG element is
reset to starting values and visible */

function ()
{
wave
.attr('fill', 'white')
.attr('fill-opacity', 0.4)
.attr('stroke-opacity', 0.3)
.show();
},

/* Now add the actual animation of growing the circle outward
to the MAX_R defined at the beginning of the code. Since
this is an SVG object, we need to pass a raphael object to
TweenLite so the plugin is used to do the animation */

TweenLite.fromTo(wave, 0.5, {
raphael: {
r: 1,
'stroke-width': 1
}
}, {
ease: Linear.easeNone,
raphael: {
r: MAX_R,
'stroke-width': 5
}
}),


Using the timeline, I can add labels to define important points in the animation that I can later refer to in other functions like play(). Additionally, I can add functions that perform various tasks as the playback passes through that point in the timeline. These have a duration of zero so will not affect the timing of the animation but can be quite helpful to execute tasks that are not animated. In this case, I ensure the SVG element is properly styled before showing it on the page. Later on in the timeline, I use a function to loop the animation to restart the charged pulsing animation until the user releases the mouse button. I could have put these functions in the onStart and onComplete callbacks in the actual TweenLite animations. However, I like seeing them inline with the rest of the timeline because its clear what order everything is running.

There's one minor detail I'd like to point out in the insertMultiple call - the last parameter is used to stagger the individual elements in the timeline. I passed 0.001 here because I was having an issue with the function immediately after the 'discharge' label not getting called. Without it, there's a pileup of zero duration elements all together and it seems that trying to move the play head there was causing an issue getting the function called. A small staggering did the trick and is something to keep in mind building timelines with an assortment of labels and functions.

Now that my timeline is setup, I just need to control it from a few mouse events:


$svg
.on('mousedown', function (e)
{
var pos = $(document.body).offset();

pulse_x = e.pageX - pos.left;
pulse_y = e.pageY - pos.top;

wave
.attr('cx', pulse_x)
.attr('cy', pulse_y);

charge
.attr('cx', pulse_x)
.attr('cy', pulse_y);

timeline.play('charging');

})
.on('mouseup', function (e)
{
intensity = wave.attr('r') / MAX_R;

/*
Update target values based on run-time
intensity calculation. Need to use invalidate
to clean out any caching and force recalculation
*/

pulse.invalidate();
pulse.vars.raphael['stroke-width'] = 100*intensity;
pulse.vars.raphael['stroke-opacity'] = 0.3*intensity;

timeline.play('discharge');
});


When the user holds the mouse down, the code moves the SVG elements to that point they clicked and starts the charging phase of the animation using the predefined label in the timeline. The timeline will run the first function, the animation, the second function (after the 'charged' label, the next animation, and then the third function which replays the second animation by calling timeline.play('charged'). This will repeat over and over again as long as the user holds the mouse down. Once they release the mouse button, the playback is moved to the 'discharge' label to finish the animation.

You'll notice I added an intensity calculation in the mouseup event. I did this so if the user does not hold the mouse down for the full half second required to charge the wave to full strength (25 pixels), the resulting pulse will have a thinner, less visible line to create an effect that the pulse is less intense. Since I already setup the animation for the effect at full strength in the timeline (or from a previous animation cycle), I need to invalidate it and then reset values using the reference I saved when I created the timeline.

So that was my little voyage into learning how to use the TimelineLite object. I just barely scratched the surface of what can be achieved with this part of the animation library. I think you can clearly see the advantages it provides over trying to do the equivalent functionality in jQuery. If your planning a project that has a large dependency on sequencing and controlling the animation of multiple elements, these feature will not only simplify the construction of the animation, but your code will be more readable and reusable making your life, as a developer, just a little bit easier.