Skip to main content

Time Again

March 15, 2006

{cs.r.title}







In the article "Timing is Everything," I covered the basics of how the core Java timers work, how more feature-rich timing systems work, and how my Timing Framework project works in particular. I have recently updated that project with several new interesting capabilities, and it is time once again to talk about time.

In the previous version of the Timing Framework project, there were several interesting but simple capabilities layered on top of the built-in timers in Java. Please read the article "Timing is Everything" for the complete scoop on this, but I'll try to cover the highlights here. Where the built-in timers simply allow you to set a timer resolution and then await callbacks to your actionPerformed() method, Timing Framework allows you to define a more capable timing situation, including having a timing cycle that defines a duration and resolution and a timing Envelope that allows you to define the behavior of one or more cycles in that envelope. Then during the actual timing, you receive callbacks not to a generic actionPerformed() method, but to a timingEvent() method that gives you the fraction of time elapsed in the cycle. These capabilities, while basic, allow you to define a much more powerful animation in a much simpler way than using the built-in timers directly.

At the end of "Timing is Everything," I called out several future tasks that I wanted to accomplish to increase the power and flexibility of Timing Framework. I've finally worked through a couple of these, plus some more along the way, so it was time to update the project and to describe how things work in the new system.

The major changes to Timing Framework boil down to the following:

  • Property setters: Let the framework handle the details for you.
  • Key frames: Much more complex and powerful animations.
  • Non-linear interpolation: More realistic timing models.
  • Acceleration: Quick and easy non-linear timing for some simple cases.

The changes are mostly backward-compatible, although I decided to make a real package for the project; the code now lives in org.jdesktop.animation.timing (instead of the old simple timing package). You'll need to update any existing code to use the new package, but otherwise the old capabilities remain untouched.

Property Setters

The way to alter the display of Swing components in general is to decide what value some property should have, such as its location, and then call an appropriate method on the component, such as setLocation(), with the new value. The way to animate these components, then, is to set a timer, vary the values over time in your callback method (timingEvent(), in the case of the Timing Framework), and call the appropriate property-setting method.

Given this canonical approach to setting component properties, could we just automate the process of setting properties? The new ObjectModifier class does just that.

Now, if you want to change the value of a property over time, you set up a PropertyRange object, which takes the name of a property and a range of values that you want the object to have. In the simplest case, this could be a simple from/to pair of values. The name of the property needs to be JavaBean-compliant, which means that there needs to be a method that the framework can call that is associated with the property name in the usual JavaBean way. For example, the property name location corresponds to the setter method setLocation.

A property range defines only the property and the range; this allows you to have one single PropertyRange for several different objects. To tell the system the object that you actually want to change the property on, you create an ObjectModifier object, which takes an object and a PropertyRange. ObjectModifier then uses reflection to set up the information it needs to call the property-setting method later. ObjectModifier is also a TimingTarget; you pass it into the constructor of TimingController to have the timing system automatically notify the ObjectModifier and to have it automatically modify the object in question.

For example, if you wanted to change the location of a JButton from (0, 0) to (500, 200) over the course of two seconds, you might do something like the following:

JButton button = new JButton("blah");
// ...
Point from = new Point(0, 0);
Point to = new Point(500, 200);
PropertyRange range =
    PropertyRange.createPropertyRangePoint("location", from, to);
ObjectModifier modifier = ObjectModifier(button, propertyRange);
TimingController controller = new TimingController(2000, modifier);
controller.start();

Note the use of a factory method for PropertyRange, namely createPropertyRangePoint(...). This approach allows us to create the appropriate KeyFrames object for PropertyRange based on the type that you want to use (more on KeyFrames below). There are several built-in factory methods and subclasses, for the types int, float, Point, Dimension, Rectangle, and Color, and more could be added in the future as appropriate. If you cannot use one of the existing factory methods, you can use the more powerful constructor for PropertyRange that takes a KeyFrames object; this constructor allows you to create a much more full-featured PropertyRange object that takes advantage of more of the new capabilities of the Timing Framework.

Key Frames

It is often enough to change an object over time in a simple linear progression, such as moving the button above from one location to another. But sometimes you want a timing sequence to consist of several distinct parts, with potentially very different values in those parts. For example, you might want to move the button first to the right, from (0, 0) to (500, 0), and then down, from (500, 0) to (500, 200).

You could do some amount of this by simply having several timing sequences strung together. In the button example, you could have one TimingController defined for the first leg and another defined for the second leg. You could start the second timer when the first timer called the end() method in your TimingTarget. But wouldn't it be nice if the timing framework handled this approach for you?

Now there is a concept of "key frames" in the framework.

"Key frame" is a term from animation. Each key frame in an animation defines an object's state at a particular point in time. At any other point in time between these key frames, the object's state can be interpolated between its state at surrounding key frame times. For example, suppose we were trying to animate Duke between two key frames defined for t=0 and t=1, where the position and rotation are defined for each key frame. Calculating the position and rotation for Duke for any time in between these key frames is straightforward. In Figure 1, we show a sample frame calculation at t=.5:

Figure 1
Figure 1. Interpolating between key frames

In the timing framework, each key frame defines a time (from 0 to 1, which represents the timing cycle) and a value associated with that time. There is a KeyTimes structure that holds all of the times and a KeyValues structure defined for an overall KeyFrames object. These structures work hand in hand with the property-setting capabilities outlined above; the values defined are for the property on the object that we wish to alter over time. At any time during the animation cycle, if the current fraction equals one of the times in the KeyTimes structure, then the object's property will be set to the appropriate value in KeyValues for that time. If the current fraction is between two times in the KeyTimes structure, the value will be interpolated between the two times that it lies between.

By default, KeyFrames will use linear interpolation (see more information below on the more interesting and complex non-linear case). So for any fraction f between 0 and 1, where f lies between the times t0 and t1 in keyTimes, and x0 and x1 are the values in keyValues that correspond to t0 and t1, we can calculate the appropriate value x(f) for the property at f as follows:

t = t0 + f * (t1 - t0);
x(f) = x0 + t * (x1 - x0);

Returning to our button-moving example, suppose we want to perform the two-leg movement defined above, where we want the button to start at (0, 0), to be at (500, 0) after half a second, and then to end up at (500, 200) after two seconds. We can do this as follows:

KeyTimes times = new KeyTimes(0, .25f, 1.0f);
Point start = new Point(0, 0);
Point middle = new Point(500, 0);
Point end = new Point(500, 200);
KeyValues values = createKeyValues(start, middle, end);
KeyFrames keyFrames = new KeyFrames(values, times);
PropertyRange range = new PropertyRange("location", keyFrames);
ObjectModifier modifier = ObjectModifier(button, propertyRange);
TimingController controller = new TimingController(2000, modifier);
controller.start();

Note that the code to create the ObjectModifier and TimingController is exactly the same as above; the thing that changed was how we defined the PropertyRange object. Now, instead of creating a simple range with single from/to values, we are creating a more complex path with three locations.

It is worth mentioning that there are simplified constructors for KeyFrames that can assume some of the details. For example, if we wanted both legs of the button's journey to take equal amounts of time (divide the total two seconds in half, in this case), then we could have KeyFrames define our KeyTimes structure implicitly and simply pass in the KeyValues object:

KeyFrames keyFrames = new KeyFrames(values);

Doing this would cause the button to spend half of the duration moving right toward (500, 0) and the other half moving down to (500, 200).

It is also worth mentioning that the framework makes liberal use of the varargs capabilities of J2SE 5.0, in the PropertyRange, KeyValues, and KeyTimes objects. In the first example above, where we created a property range with just from and to values, we could actually have passed in an arbitrary number of Point parameters instead. Internally, PropertyRange will create the necessary KeyValues and KeyTimes structures to hold these parameters. So in the latest example above where we move the button in two legs of equal times, we actually do not need to resort to the new KeyFrames or KeyValues structures at all; PropertyRange will handle those details for us:

Point start = new Point(0, 0);
Point middle = new Point(500, 0);
Point end = new Point(500, 200);
PropertyRange range = new PropertyRange("location", start, middle, end);
ObjectModifier modifier = ObjectModifier(button, propertyRange);
TimingController controller = new TimingController(2000, modifier);
controller.start();

There is an additional capability built into KeyFrames, called InterpolationType. By default, as shown above, values are linearly interpolated. But you can construct a KeyFrames object with InterpolationType.DISCRETE to specify that values are not interpolated at all. Instead, this interpolation type specifies that the property should take on only those values in the KeyValues structure. At any time between times in KeyTimes, the property will remain at the previous value until the next time is reached, at which time the value will jump to the next value in KeyValues.

In our original three-step animation of the button above, we could specify that the button "jump" between the from/middle/to values by using this alternate constructor for KeyFrames:

KeyFrames keyFrames = new KeyFrames(values, times,
        InterpolationType.DISCRETE);

Assuming we want animation that is either linearly or discretely interpolated, the KeyFrames/KeyTimes/KeyValues structures described above represent a relatively simple way of defining those types of animation.

But often we will want more interesting behavior than this system provides.

Non-Linear Interpolation

One of the things I discovered when looking into causes of choppy animations (see part one and part two for the analysis and potential solutions) was that linear animations suffer because we live in an inherently non-linear world. Vases do not fall with linear motion toward tile floors, people do not glide constantly to fail catching them, and shards of glass do not explode at a constant linear rate all over the room. Animations that use linearly-interpolation motion therefore tend to look fake in comparison to the real world.

One obvious thing to implement in an animation framework, then, is non-linear interpolation.

I mentioned the InterpolationType enum above; we saw it used when we defined a non-standard KeyFrame to use DISCRETE instead of LINEAR interpolation. There is an addition value that InterpolationType offers: NONLINEAR. This type of animation uses a different, more interesting, and more complex method of interpolating between the values in KeyValues; it uses splines.

Visuals sometimes help; here are some sample spline curves to give you a better feel for how different control points can control the behavior of the interpolation. When calculating the spline interpolation, the input is the fraction of time elapsed in the current time interval, which you can think of as the fraction of the length of the spline. With a timing fraction value of .1, we calculate out what point on the curve represents 10 percent of the total length of that curve. Given that value, we can find the value for the curve on the y axis. For example, in the "Quick start" curve below, you can see that a fractional input of 50 percent (halfway through the time interval) would be somewhere above 0.8 in y.

Figure 2
Linear: Control points at (0, 0) and (1, 1)

Figure 3
Ease-in: Control points at (0, .5) and (1, .5)

Figure 4
Ease-in, ease-out: Control points at (1, 0) and (0, 1)

Figure 5
Quick start: Control points at (0, 1) and (0, 1)

Rather than try to define splines further, I'll punt at this point and redirect you to the other resources. There are numerous resources for understanding the math behind Bezier splines, including books such as the canonical computer graphics text Computer Graphics Principles and Practice by Foley, van Dam, Feiner, and Hughes, and one I found more helpful in this situation, The Geometry Toolbox for Graphics and Modeling by Farin and Hansford. There are also numerous online resources for both explaining the concepts and math fundamentals, as well as playing with splines in applet form (just do a search with your favorite search engine on "spline applet"). Most of all, I'll defer to the SMIL specification. KeySplines (and KeyTimes/KeyValues, for that matter) in the timing framework were implemented specifically to be very compatible with SMIL. So, for example, the method of defining and calculating splines is the same for SMIL as it is for the timing framework. An important point to note: the SMIL spec assumes that the "anchor points" of each spline are (0, 0) and (1, 1); I found this crucial assumption poorly documented (read: completely skipped), so maybe it will save you some trouble if I cheat and just tell you this up front.

It is interesting to play around with the values of splines to see what results you get, and to try to get a handle on how the splines will affect the timing behavior in general. To help with this, Romain Guy wrote a great demo that is posted on the Timing Framework project site that allows you to modify the spline curve and see how that affects a particular animation.

There is another new structure in the timing framework that we use for this capability: KeySplines. This structure is used to define control points for Bezier splines that define the interpolation in all of the time intervals defined by KeyTimes. A KeySplines object consists of one or more Spline objects, which hold the actual control points for each individual spline. Note that since the splines define the behavior between times in KeyTimes, there needs to be one less spline than the number of times (for n times in KeyTimes, there are (n-1) time intervals defined).

Using splines in your application is just a simple addition to what we were doing above with KeyValues and KeyTimes. Let's go back to the original three-step button animation above. Suppose you still wanted linear interpolation during the first time interval, but more of an ease-in/ease-out interpolation during the second part. Linear interpolation can be represented by a spline with the control points (0, 0) and (1, 1), so our Spline for the first segment will use those points. We can get an ease-in/ease-out behavior by using a Spline with control points (1, 0) and (0, 1). The additional code for this animation (most of the code is exactly as it was in the linear case) would then be:

KeySplines splines = new KeySplines(
        new Spline(0, 0, 1, 1),
        new Spline(1, 0, 0, 1));           
KeyFrames keyFrames = new KeyFrames(values, times, splines,
                         InterpolationType.NONLINEAR);

Simple Non-Linear Behavior: Acceleration

The ability to define KeyFrames and have different types of interpolation is a powerful thing; you can create quite complex animations using these facilities.

But sometimes, you just want simpler non-linear behavior. In the case of a demo I was writing recently, I wanted a "bouncing" button; a component that falls down with gravity-like acceleration, hits the destination, and then returns to the starting point. Doesn't everyone need a bouncing component in their application?

While this behavior should be possible with the types of structures outlined above, I didn't find it very intuitive for such a simple task. What I wanted was a simple way to say "speed this thing up for the following amount of time."

Fortunately, the SMIL spec had just what I needed: acceleration and deceleration.

Acceleration and deceleration are simple fractional values that specify what portion of the timing cycle to spend speeding up (at the beginning of the cycle) and slowing down (at the end of the cycle). These values are set and stored in the TimingController object. By default, these values are 0 (no acceleration, no deceleration).

While the above KeyFrames functionality acts on property values, acceleration and deceleration act directly on the time value that the animations are based upon.

Normally, a callback to TimingTarget.timingEvent() includes the fraction variable, which is the portion of time elapsed between the beginning and end of a timing cycle. With the inclusion of the acceleration and deceleration variables, this timing value may have been accelerated or decelerated prior to calling timingEvent. The overall time cycle will still be exactly the same, but the rate at which the cycle proceeds at different times during that cycle depends upon the acceleration and deceleration parameters.

The process to use these parameters is simple; you just call the setAcceleration() and setDeceleration() methods of TimingController to set these properties.

In the simple example of my "bouncing button" demo, here is the code I used:

Cycle bouncingCycle = new Cycle(2000, 30);
Envelope bouncingEnvelope = new Envelope(TimingController.INFINITE, 0,
        Envelope.RepeatBehavior.REVERSE, Envelope.EndBehavior.RESET);
Point p0 = new Point(0, 0);
Point p1 = new Point(0, 500);
PropertyRangerange = PropertyRange.createPropertyRangePoint("location",
        p0, p1);
TimingController timer = new TimingController(bouncingCycle,
        bouncingEnvelope,
        new ObjectModifier(button, range));
timer.setAcceleration(1.0f);
timer.start();

Note that the button will rebound automatically (and get the right upward deceleration behavior) and continue bouncing forever due to the way that the Envelope is defined here.

Try It Out!

Check out the project site: read the Javadocs, look at the code, download it, run the demos, and use the framework.

I made some other minor changes to the framework, as well, such as the ability to have multiple TimingTargets for any given TimingController, but the features listed above are the main additions. The Timing Framework API is still relatively small, but I think the combination of property setting, key frames, and non-linear motion takes the framework to a very interesting place. There are still lots of future directions to explore and implement, and I expect to keep updating the framework as I get the chance, but in the meantime I hope the framework is useful to both learn from and use.

References

  • "Timing is Everything:" This is the first article on the Timing Framework project; it explains the fundamentals behind timers, the built-in Java timers, and the Timing Framework itself.
  • timingframework.dev.java.net: This project on java.net has the source code and library for understanding and using the Timing Framework.
  • SMIL specification: Much of the Timing Framework supports the SMIL model of timing; understanding SMIL might help in understanding how to use the Timing Framework effectively.
  • "Make Your Animations Less Ch-Ch-Choppy:" This blog entry investigated and analyzed some of the problems with choppy animations.
  • "Smooth Moves:" This article demonstrated possible solutions and workarounds for the problems investigated in the above "Ch-Ch-Choppy" blog entry.

Acknowledgements

I'd like to thank Hans Muller, Vincent Hardy, Chris Campbell, Romain Guy, and others I bothered for their ongoing assistance with the details of the framework design and implementation. Designing an API in a vacuum is difficult and error-prone; having people actually using and reviewing the work is critical.

width="1" height="1" border="0" alt=" " />
Chet Haase I'm a graphics geek. I worked at Sun, Adobe, and Google, always on the UI side of things.
Related Topics >> GUI   |