Skip to main content

Getting Animated

Posted by javakiddy on March 8, 2007 at 7:13 AM PST

Well, given feedback from around the web it seems a lot of people liked my recent little foray into the realm of Swing eye candy, which is both very flattering, and also rather worrying. Do so many of you really value GUI bling?

I continued to play about with the code after I last posted it, and the final (for now!) bling'd up version can be WebStart'd from the link below. I must warn you, it takes eye candy to new depths of pointlessness! But these little experiments were never about testing the appropriateness or otherwise of various effects in a user facing GUI. Instead I was attempting to assess how easy it is to pull off such effects using bog-standard AWT/Swing API calls.

I found Swing highly compliant, which was reassuring. Instead the bulk of the head-scratching went not on getting Swing to 'swing', but on the custom PLaF and the movement mechanics behind the transitions. The movement mechanics in particular required a lot of boilerplate code.

In the end I wrote a mini API for tracking movement over time, and it was this utility API which proved to be far far more interesting than the main Swing code itself. So, despite the presence of rival animation solutions, I found myself drawn towards tinkering with this mini library, and eventually went back to the drawing board, rebuilding the code from scratch to be more universal and tighter in design.

The rest of this article will look at the results via a little trial animation I crafted.

Wizard_ocean.png Movement2.png

Creating the Frames

The rocket demo above is a simple little looped animation. A cartoon rocket takes off, hovers, the engine cuts out, and it comes tumbling back down to the ground. Key snippets of the code are presented below, in stages.

First off we need to set up some animation images for our rocket. I've already (elsewhere) loaded nine images into a BufferedImage array called cells, now we need to bind them into animations:

// Sequence in which the frames loop: 1,2,1,2...
ArrayFrames flyingFrames = _frames(1,2);
flyingFrames.setMode(ArrayFrames.Mode.LOOP_FORWARD);
flyingFrames.setGapBetweenFrames(150);
 
// Sequence in which the frames are random
ArrayFrames failingFrames = 
  _frames(0,0,0,0,0,1,2);
failingFrames.setMode(ArrayFrames.Mode.RANDOM);
failingFrames.setGapBetweenFrames(150);
 
// Sequence for tumbling rocket
ArrayFrames tumblingFrames = _frames(3,4,5,6);
tumblingFrames.setMode(ArrayFrames.Mode.LOOP_FORWARD);
tumblingFrames.setGapBetweenFrames(100);
 
// Sequence for crashed rocket
ArrayFrames crashedFrames = _frames(7,8);
crashedFrames.setMode(ArrayFrames.Mode.LOOP_FORWARD);
crashedFrames.setGapBetweenFrames(250);
 
:
:
 
private ArrayFrames _frames(int... f)
{   // Convert necessary cells into frame array
    ArrayList arr = new ArrayList();
    for(int i : f)  arr.add(cells[i]);
    // Make Frames object
    return new ArrayFrames(arr);
}

The first block creates the rocket with its engine thrusters running, specifically a loop of frames running 1,2,1,2,1,2... with a 150 millisecond gap between frames. The second has the engine cut out by showing a random frame from an array of possibilities. The third is another loop sequence, showing the rocket tumbling over and over, as is the fourth and final block, which shows a slow two cell animation of the crashed rocket.

You'll note that the ArrayFrames class has a type parameter, which determines what constitutes a 'frame'. Indeed so does the Frames interface which it implements. The idea is that Frames implementations should make few or no assumptions about what a frame is. A BufferedImage, a vector shape of some sort, an index into an array of data somewhere, a real-time generated bitmap, or even a wrapper object which contains bespoke animation data for your application — all of these are valid. The concrete class, ArrayFrames, takes a collection of such 'things' and feeds them to you one by one, in an order determined by its mode.

Creating the Movement

Now let us move on to the code which actually builds the animation:

iac = new InterpolatedAnimationController
  (ANIMATION_DURATION);
 
// Take off
double weight=0.30d;
iac.addPath(new Straight2DLinePath(0,250 , 0,0) ,weight);
iac.addVelocity(Velocities.SOFT_ACCELERATION_DECELERATION 
  ,weight);
iac.addFrames(flyingFrames ,weight);
// Hover, then failing engine
weight=0.30d;
iac.addPath(new Drunken2DLinePath(0,0 , 0,0 , 10,50,2,5) 
  ,weight);
iac.addVelocity(Velocities.CONSTANT ,weight);
iac.addFrames(flyingFrames ,0.10d);
iac.addFrames(failingFrames ,0.25d);
// Tumbling
weight=0.20d;
iac.addPath(new Straight2DLinePath(0,0 , 0,250) ,weight);
iac.addVelocity(new GravityBounceVelocity(0.70d,0.15d
  ,0.10d,0.05d) ,weight);
iac.addFrames(tumblingFrames ,0.15d);
// Crashed
weight=0.20d;
iac.addPath(new StaticPath(0,250) ,weight);
iac.addVelocity(Velocities.CONSTANT ,weight);
iac.addFrames(crashedFrames ,weight);

Firstly we create a new controller. This particular specimen will stretch our animation out over a given duration (in milliseconds.) Having glued all the bits of our animation together, it is this controller class we will use to access it. The type parameter matches the type of our Frames.

Into this controller we add paths (where to go), velocities (how to get there), and frames (what to show). Each of these has a weight, which determines how much of the overall animation it affects. In the Take off block the weights for all three types of data are the same, 0.30 (double.) Given that I've contrived the code such that all weights total 1, this will mean the opening part of the animation will occupy 30% of the total time.

Weights don't have to add up to 1. The controller scales each portion of the animation across the total of all weights for that type of data, so I could have used integer values instead (still represented by doubles) or whatever scheme I felt comfortable with.

In the Take off block of code you'll see we provide a straight 'up in the air' path, from (0,250) to (0,0), using a velocity which gradually increases then decreases over the course of the movement, while showing the frames we previously set up for the rocket booster animation.

Because several of the standard velocities do not require any configuration parameters and are re-entrant, I made them singletons in an abstract Velocities class for convenience.

Moving on to the next block: Hover... uses a drunken line which meanders around the (0,0) point, which is a useful method of getting our little rocket to sway and bob as it hangs in the air. The extra parameters in the constructor are to constrain the overall extent of the vertical and horizontal drift (how far) and the drift increment each frame (how fast.)

We don't need a velocity for this, so we use a uniform acceleration just to pad out that part of the animation, keeping the paths and velocities aligned.

Note something interesting about the frames: firstly we have two sets of frames, the first shows the booster as normal while the second shows it starting to fail, but the total weight of the frames is 0.05 over the weight of the other portions of the animation — more on this in a moment.

And so, on to the next block: Tumbling uses a straight line from (0,0) to (0,250) with a cute little velocity called GravityBounceVelocity. The relationship between paths and velocities is easily understood: think of a path as a track along which our animation can travel, and the velocity determines where on that track the animation is at, at any given interval during the duration of the animation (or rather, that potion of it.) (Note: Velocity probably isn't an appropriate label, but it was the best one I could come up with at the time :) The bounce velocity used here moves the point backwards and forwards along the line to fake a bouncing ball effect. The initial drop takes up 70% of the time, then the subsequent three bounces take up 15%, 10% and 5%.

You'll note that the tumbling animation is 0.05 short of the weight for this portion of the animation. This is because I've allowed the previous Frames object to bleed into this portion, so the rocket doesn't start to actually tumble until it's 25% of the way into its fall.

This demonstrates an important part of the API: the three constituent components, paths, velocities and frames, are all independent of each other. In the above example we grouped them into portions to make the code easier to read, but each of them are independently interpolated across the animation timeline. For example, if we commented out all the addFrames() lines after the first one, flyingFrames would continue to run throughout the duration of the animation — its 0.30 weight would simple stretch itself out across the whole timeline unchallenged.

And finally... the Crashed portion simply animates over two frames as the rocket lies prone at (0,250). Nice and simple.

Getting It On Screen

So, we've set up our animation. Now we need to actually run it:

// When we want to start the animation we need to seed 
// it with the current time, and reset any state data the 
// paths/velocities/frames may have.
iac.reset();
 
:
:
 
// This code is called on a timer, such as 
// java.util.TimerTask.run() or 
// javax.swing.Timer.actionPerformed()
if(!iac.isFinished())
{   // Get the current animation co-ords and frame image.
    Point p=iac.getCoordinates();
    BufferedImage im=iac.getFrame();
    // ... Draw your stuff here ...
}
else
{   // Animation has finished.  Normally we might call
    // Timer.stop() here to unschedule our timer task, but
    // instead we'll simply reset the animation to replay it.
    iac.reset();
}

The animation controller code should be easy to understand without having to walk through it. You'll see here why the controller needed a type parameter which matched the Frames implementation used, otherwise we'd have needed to cast our image as we fetched it.

Of course the InterpolatedAnimationController class is just one example of a controller. You could write your own if you needed some sort of animation which wasn't based in real-time — for example if you were writing an animation out to file and needed frames to be equi-spaced along the timeline, no matter how long it took to generate each individual frame.

Conclusion

Well, I had a lot of fun messing about with all that code, and the resulting API is functional and lightweight enough to employ in my own personal ongoing Swing Bling adventures. Beyond that... who knows? Is anyone bothered enough to want to see the source code? Is this idea strong enough to merit an Open Source project of its own? I don't know... I didn't build it as a serious rival to any other animation API, I just wanted to have a little fun. And, after all, isn't 'fun' what 'Swing Bling' is supposed to be all about? :)

Related Topics >>