The Source for Java Technology Collaboration
User: Password:



Simon Morris

Simon Morris's Blog

Getting Animated

Posted by javakiddy on March 08, 2007 at 07:13 AM | Comments (13)

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? :)


Bookmark blog post: del.icio.us del.icio.us Digg Digg DZone DZone Furl Furl Reddit Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment

  • WOW! Kewl, that is almost as good animation as on a Commodore 64 from 1980! I'm glad Java is catching up! ;)

    Of course, the C-64 animation was synced with the vertical blanking of the screen and thus the animation was perfectly smooth, but Java will soon be there. I've heard that javax.swing.Timer might get a higher than the current 10-15ms resolution sometime later. Maybe for Java 9.0?

    Sorry, about the sarcasm, but I couldn't resist. :)

    Cheers,
    Mikael Grev

    Posted by: mikaelgrev on March 08, 2007 at 08:20 AM

  • Coincidentally my next demo plays "Micheal Row the Boat Ashore" on a three voice sound chip (for those of you old enough to get the reference.)

    :)

    Posted by: javakiddy on March 08, 2007 at 08:44 AM

  • Great work as usual. I'm surprised though that you haven't admitted to yourself that so many people find GUI bling important. If GUI bling wasn't important, then Flash would have never have been so common place as it is today. Adobe Flex and WPF wouldn't be garnering any interest either. And good web designers would be non existent as there wouldn't be a need for them. Sadly though, the people who might place most importance on GUI bling are like my grandparents, who think the overall value of a website is site is based upon for how good the flash animations are + design. It reminds me of a discussion you and I had earlier; the same people who put the premium on GUI bling are the same ones who can't managed to clear their JWS cache...which is A LOT of people in this world. The reality is that it's not just GUI bling that is important, but bling in general. BMW's, Porsches, Ferrari's, model girlfriends, an Ipod as opposed to the cheap knockoff -- they all carry a lof of value to a certain type of person. And as such, GUI bling and bling will always exist b/c as long as the people who desire bling have disposable income.

    Posted by: javaguy44 on March 08, 2007 at 10:31 AM

  • I love where the wizard is headed, the falling file dialog actually made me giggle. Thanks!

    Posted by: pdoubleya on March 08, 2007 at 01:35 PM

  • You should just assume that we'll want to look at the source code :-)

    Posted by: mgbacke on March 08, 2007 at 01:35 PM

  • @mgbacke: I guess I live in eternal hope that I won't have to tidy it up and finish Javadoc'ing... :)

    Okay, I'll bundle the source somewhere in the next 24 hours or so. InterpolatedAnimationController is the only class which hasn't been doc'd properly, so it shouldn't take that long. I'll include the wizard source too, for those who are curious.

    Posted by: javakiddy on March 09, 2007 at 01:57 AM

  • Excellent work! Java technology needs strong tools for animation and multimedia. Sun must understand this for the survival of Java. Maybe someone would like to integrate SMIL,Video, SVG in a inspired with Java tool!! I know there are some project like x-smiles.org but we need something stronger!!!

    Posted by: dtrehas on March 09, 2007 at 03:00 AM

  • Download the source.

    Quickly knocked up some docs for the controller. The above zip contains the code for both WebStart demos.

    Incidentally, those of you who had problems with the original wizard when moving the window between display devices, try it now. I was playing fast-n-loose with VolatileImage buffers, which I suspect was rather naughty. The new wizard is a tad more respectful.

    Posted by: javakiddy on March 09, 2007 at 03:08 AM

  • @ javakiddy, you should stay away from VolatileImage as much as you can :)

    Posted by: gfx on March 09, 2007 at 05:42 AM

  • @gfx: I mainly use them as back buffers, in the hope they might give that little bit of extra performance. I figured if a single frame of a transition gets corrupted between rendering and paint(), it's no big deal. But to be honest I've never really tested the speed versus regular BufferedImage's for the type of ops I'm doing on the image.

    Posted by: javakiddy on March 09, 2007 at 06:08 AM

  • "the three constituent components, paths, velocities and frames, are all independent of each other."

    Ohh...that's awesome...and clean...I really like that. I tried doing a similar framework but my movement was dependent on keyframes inside the animation (ie: move this far between keyframe 1 and 2) and because of it I lost the precision and versatility you have with your velocities and paths.

    DB

    Posted by: dblair on March 09, 2007 at 11:15 AM

  • Throw in a few calls of Toolkit.sync() for good measure.

    Posted by: ewin on March 10, 2007 at 09:33 AM

  • Good start, but needs to "pimp your effect" ;-) You could add alpha blending fade-in. Improve damping, as actually is not consistent for neighbor component and is not smooth at al. Waiting for 2.0 ;-)

    Posted by: bjb on March 11, 2007 at 08:59 AM



Only logged in users may post comments. Login Here.


Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds