Wednesday, July 15, 2009

Penner for your Thoughts

Knock knock

Who's there?

Ease

Ease who?

Ease anyone going to open the dang door?

For anyone new to the term "Easing", it refers in Flex (and in Flash) to altering the motion of an animation, to create non-linear motion for more realistic, enjoyable, or interesting behavior. I've shown some examples of this behavior in other blog posts and videos like this one, so you might check those out if you're interested and clueless about what I'm blathering on about. For anyone mystified by the title of this blog, the easing functions in Flex 3 came from similar easing functions in Flash that were created by Robert Penner, and are described in his book and on his website; the functions are also sometimes called the "Penner" easing functions. (Pretty cool to get your name associated with something so widely known and used. I wonder how I could do that? Although I should be careful what I wish for; I'd probably end up with something like "Chetastrophic Failure" or "Haasehang". Maybe anonymity ain't so bad).

Easing into Flex 4

In Flex 4, the new effects use a different approach to easing. These changes came under the heading of "as long as I'm in there mucking about, can I improve the API a tad to make it more flexible and extensible?" Also, the changes were related to introducing "interpolators" (via the IInterpolator interface) which allow arbitrary type interpolation in the new effects.

In Flex 3, you supply an "easing function" (literally, a Function reference) that the effect will call into on every animation frame to get the desired eased behavior. In Flex 4, you instead supply an instance of an easing class (one which implements the new IEaser interface) that the effect calls into in much the same way as before (although the functions in the two cases are very different, with the Flex 3 version taking 4 parameters and the Flex 4 version taking just one).

I like the change from an API standpoint (well, I would, wouldn't I?). I'm into simplicity, so the fact that the new Power class expresses the same functionality as four previous classes (Quadratic, Cubic, Quartic, and Quintic) and 12 functions (easeIn, easeout, and easeInOut in each of those classes) while allowing more flexibility in terms of handling arbitrary and fractional powers and arbitrary ease-in/out points is pretty nifty.

But as with any new API that takes the place of existing API, I had some reservations about introducing the new classes without offering complete parity with the old easing functions. Sure, the new easing classes and interface offer more power and flexibility, and developers should be able to easily create their own easing classes. But what if they miss the old Bounce and I simply haven't gotten around to porting it yet? Will there be great gnashing of teeth in developer cubicles? Will flags with my cartoon visage be burned in effigy at tech conferences worldwide? Will programmers spew into the output-only blogosophere "Flex 4 IEaser? Flex 4 IHarder, is more like it!"?

To be clear, the easing classes we offer in Flex 4 should cover the bases for all standard effects and most cases that developers would really care about. We have the same Sine-in-out behavior for effects by default that we used to have, and we offer ease in/out/in-out variants of Sine and Power (where Power covers multiple old easing function behaviors, as explained above). We also offer Linear (no-ease), with additional acceleration/deceleration phases. And with the new RepeatBehavior capability in Flex 4 effects, the old Bounce easing class is perhaps a bit less necessary than it might have been before.

But still, developers might have had some favorite easing classes that have no equivalent in Flex 4 (at least not yet), such as Elastic and Exponential.

So what to do?

Well, I took a few minutes this week (far less time than it took me to write this article) and put together a simple wrapper class that does it all. You can now create an instance of this class (which implements IEaser) with any of the old easing functions and supply the instance to any Flex 4 effect and it'll do exactly what you want - play the new effect with the old easing function behavior.

The Wrapper Class

I wrote this class mostly as a utility to offer to anyone pining for the old Penner easing classes in Flex 4. But I also thought it would be a good demo of how easy it is to write custom easing classes with the new easing API of Flex 4. In fact, it's so easy, I'll just put the complete implementation (minus some awesome comments) right here inline to prove my point:

public class EasingFunctionWrapper implements IEaser
{
 public var easingFunction:Function = Linear.easeNone;

 public function EasingFunctionWrapper(easingFunction:Function = null)
 {
     if (easingFunction != null)
         this.easingFunction = easingFunction;
 }

 public function ease(fraction:Number):Number
 {
     return easingFunction(fraction, 0, 1, 1);
 }
}

That's right - a whopping one-liner function plus a constructor and a property and we get all of the old Flex 3 easing functions in Flex 4.

Now, let's look at how and why it works.

The constructor simply takes a reference to the easing function that you want to use. This function must, of course, be one of the functions from the old easing classes (or any other function that has the same function signature, taking 4 Number arguments and returning a Number), but there is otherwise no restriction. You can pass in Linear.easeNone or Cubic.easeIn or whatever else you want here that works. The constructor then saves a reference to that function for use later. Note that the parameter has a default value, which is a handy way to enable this class to be created in MXML (since MXML tags will only work with no-arg constructors).

The ease() function takes a fraction and returns a fraction. Specifically, it takes a number (generated by the Animation running inside of the effects) that represents the elapsed fraction of the animation. It then calls into the easing function specified and returns the result, which will also be an elapsed fraction. This return value is generally a number from 0 to 1, although some of the easing functions (Back and Elastic) can produce numbers toward the start and end of the animation that go outside the 0-1 boundaries (this is how the object ends up springing outside of the endpoints when used with a Move effect with these easing functions).

The reason that the wrapper works (and, gosh, so easily!) is that we use the given easing function generically, supplying fictional values to it, but ones which will produce exactly the value we need to return from our ease() function. Inside our ease() function, it does not matter what the target values are, what time we started, what the total duration is, or anything else specific to the animation: we only care about the elapsed fraction of the animation. We can feed numbers to the Flex 3 easing function to get back exactly the eased fraction that we need.

The Flex 3 easing functions take four numerical arguments:

  • t: the elapsed time in the animation
  • b: the starting value of the animation
  • c: the total change in value during the animation
  • d: the duration of the animation

The return value of the function is a number representing the eased value of the animation.

Typically, you would call (or, rather, the Flex 3 Tween class would internally call) this function with real time and target values and would get back the actual eased value for the object. It would then set the target property to this new value.

But in the case of Flex 4 easing, we can deal with a simpler fractional value and derive the real value from the result. In particular, we can treat both the times and values as fractional (values between 0 and 1), and use the result to calculate the real values. That is:

  • t: this can be the elapsed fraction sent into our ease() function - it represents how much time has elapsed in an animation with duration 1.
  • b: this value can be set to 0, representing the starting value for the object (whatever it is). We don't actually need to use the real starting value here, as long as we can translate the result of our easing call into a real value later on.
  • c: just like our time and duration values, we can use an end value of 1, which represents the total change in value of the target object.
  • d: we can send in a duration of 1, which represents 100% of the total real duration (whatever it is in real time values).

The easing function will return an eased value from these inputs which we can then use to calculate a real value for the target object.

Let's run through an example. We will assume that the developer wants a Linear ease (just to make the math easier to explain), so the eased value will be exactly the same as the pre-eased value. The developer's code would create the wrapper like this:

    var wrapper:IEaser = new EasingFunctionWrapper(Linear.easeNone);

or, in MXML:

    <s:EasingFunctionWrapper id="wrapper" easingFunction="Linear.easeNone"/>

(In fact, since the wrapper defaults to Linear.easeNone, we don't have to specify the easing function in the code above, but I'm doing so to make the code clearer for the purposes of explaining how it all works).

The developer would then supply the wrapper as the easer property to any of the Flex 4 effects, like this:

    var mover:Move = new Move();
    mover.xFrom = 50;
    mover.xTo = 70;
    mover.easer = wrapper;

or, in MXML:

    <s:Move id="mover" easer="wrapper" xFrom="50" xTo="70"/>

At any point during the animation, the wrapper's ease() function would be called with some fraction f, between 0 and 1 (representing, again, the elapsed fraction of the animation). The ease() function would then call the wrapper's easing function as follows:

    return easingFunction(f, 0, 1, 1);

That is, we're calling the easing function with our elapsed fraction, a duration of 1, and object starting/ending values of 0 and 1. We then get back some Number which is then used (by the Animation class running inside our Move effect) to calculate the true object value. For example, suppose we are at the halfway point in the animation, where f==.5. We call the easing function with .5, and we get back another value of .5 (because it is a Linear ease). The Animation class then takes that value and calculates the animated x value using a simple linear parametric calculation:

    var newValue:Number = startValue + easedFraction * (endValue - startValue);

For our animation above from x==50 to x==70, this give us an animated value of 60 at this half-way point in the animation.

Some Summaries

What I've attempted to show here are two related things:

  • Flex 4 easing: The new approach to easing in Flex 4 is different from Flex 3, but hopefully simpler to use and to customize than the older easing functions which took four parameters and dealt strictly with numeric values. (I didn't get into the IInterpolator aspect of Flex 4 effects, but suffice it to say that the new easing system allows Flex 4 effects to deal with arbitrary objects and types, not just Numbers).
  • Flex 3 easing functions: Several of the old easing functions still exist in Flex 4 effects, albeit in a different form (they are instances of type IEaser instead of easing functions). Some of the Flex 3 easing functions, however, do not currently exist in Flex 4 (such as Elastic and Exponential). But through the EasingFunctionWrapper class above, you can access any of the old easing functions for the new Flex 4 effects.

Demolicious

Now that we're done, it's demo time. Here's a demo that shows the new wrapper in operation. You can specify an arbitrary duration (in milliseconds), any of the Flex 3 easing classes, and an easing type (ease in/out/in-out - note that Linear automatically disables the type ComboBox since no matter what you pick you get the same Linear.easeNone behavior).

Of course, no demo would be complete without the source code. Check it out, play with it, and feel free to use the wrapper if you want to play with Flex 3 effects in your Flex 4 code.

By the way, do you use non-default easing in your code? If so, are there any Flex 3 easing functions that you've been missing in Flex 4? Any you think we should try to get in before we release, or for some follow-up release? Please comment and tell me what you think.

3 comments:

Robert Penner said...

Nice work and thorough explanation. I really like the IEaser interface.

Anonymous said...

Great work Chet. Really liked your MAX session.

I was wondering is there any way to use Animate to call a method on an object rather than a property?

So for e.g I'd like to animate a child object in the updateDisplayList method of a layout class, but instead of animating the "x" property, would like to call the "setLayoutBoundsPosition" method instead over time.

madRIAman said...

Thanks for posting this. You've saved my lazy programmer butt!!