IBM Skip to main content
Search for:   within 
      Search help  
     IBM home  |  Products & services  |  Support & downloads   |  My account

developerWorks > Java technology
2D animation with image-based paths
code389 KBe-mail it!
What's not to lose?
It's all a matter of control
Let's have a hot time tonight!
Download the code
Color my world
Variety is the spice of life
Run for your lives!
Everyone gets a turn
Painting the scene
About the authors
Rate this article
Related content:
IBM Developer Kits for the Java platform (downloads)
Creating Java 2D composites
Introduction to Java 2D
dW newsletters
dW Subscription
(CDs and downloads)
Take the heavy coding out of fixed-object animation

Level: Intermediate

Barry A. Feigenbaum, Ph.D. ( animation with image-based paths), Senior Consulting IT Architect, IBM
Tom Brunet ( animation with image-based paths), Ph.D. candidate, University of Wisconsin

9 January 2004

Why code your animated sequences when you can draw what you want and let a program do the rest? In this article, Barry Feigenbaum and Tom Brunet show you how to combine lossless images, Swing technology, and the authors' own Java-based animation engine to generate movement sequences for fixed objects in 2D animation.

In two-dimensional (2D) animation it is often necessary to move an object or objects around the 2D area in predetermined patterns, sometimes called control paths. This type of animation requires that you solve two problems:

  • How to specify the control paths for the objects to follow.
  • How to move the objects along the selected paths.

In this article we'll show you how to solve these problems using lossless images, Swing technology, and a Java-based animation engine. We'll start by drawing the desired trajectory for our animated objects, and then use the animation engine to drive the objects along the defined control paths.

While lossless images (described below) are both easy to create and easy to process, the technique of using them can be as finely tuned as you want to make it. Using an example animation sequence, you'll learn how different color sets can be used to create more and less sophisticated movement sequences. You'll also learn how to process your image to extract out the desired control paths, layer the control paths against a background image, create the objects (Swing GUI components) for your animation sequence, and drive them along the defined control paths to complete the animation process. See Resources for more information.

Note: This article assumes you have a working knowledge of Java programming in general and Swing GUI construction in particular. Some additional experience manipulating images on the Java platform using Java 2D will be helpful.

What's not to lose?
A lossless image is one in which all the image pixels are permanently retained. The image must be of a type that can be either stored exactly or recovered to an exact replica of the original.

You can use a variety of applications programs to create lossless images, including Microsoft Paint, Jasc Paint Shop Pro, and some custom applications. You can store the images in files or create them only in RAM. The images must either be stored uncompressed or compressed using a lossless compression scheme, such as zip compression. Typical lossless image formats include Microsoft's Bitmap (BMP) and the Portable Network Graphics (PNG) format. Lossy compression schemes such as those often used for GIF (Graphics Interchange Format) and JPEG (Joint Photographic Experts Group) files will not work for the animation techniques described in this article.

It's all a matter of control
In its most general form a control path represents the behavior to be taken at a particular position and time through any n-dimensional space. We define a control path as the path taken by one or more objects through a 2D space. You represent a control path by mapping an object's position to a behavior at that position. A program then iterates through the defined objects, looks up the behavior for the position of the object in the map, and performs the instructed action on the object. For all but the simplest of control paths, creating such a mapping in code can be both time consuming and error prone; thus a drawing program is more appropriate.

Control paths can be time invariant, in which case they are static, or time variable, in which case they are dynamic. If your lossless image is contained in an image file, it will be time invariant, or static. If your lossless image is contained in RAM and used directly, it will be time variable, or dynamic. In this article we'll focus on static control paths. With the right editing program, static images can be much easier to generate, although the types of behavior defined will also affect the process to some degree.

Let's have a hot time tonight!
A good way to learn about animation is to do it yourself. We'll use an animation example to illustrate the concepts discussed throughout the remainder of the article. Our example is an animated fire escape sequence, so we'll generate control paths to represent the escape trajectory for several figures. We'll use the partial floor plan in Figure 1 as a background image. You can see the full-size background image in Figure 6.

Download the code
Get the complete source code for the animation engine and the example animation sequence from Resources.

Figure 1. A portion of the background image
Image of a partial floor plan showing hallways and offices

We can generate the control mapping from any array of values. Using an image for the array (as shown in Figure 2) allows us to use a color value to represent the behavior at each location. The size (number of color bits) of each color value will depend on the image format. Figure 2 illustrates some of the control paths for our fire escape sequence.

Figure 2. Some of the control paths
Image of some of the control paths used to represent escape routes

To see how the control path image corresponds to the animation background, we can overlay the control image on top of the background image, as shown in Figure 3.

Figure 3. The images combined using a layered transparency
The two images combined using a layered transparency

Color my world
After an image is generated, it can be easily converted to the needed mapping. We simply iterate through the colors of the image and assign a behavior for each color value. So, for example we could use white, which is typically an all-ones value, to indicate no mapping or a default behavior. Black, which is typically a zero value, could be used to represent a custom behavior. If mapped according to our image, when the object encounters a position that shares the same behavior (that is, the same color, say black) it will continue in the direction defined by that position. If the position does not share the same behavior, it will find an adjacent position that shares its behavior without backtracking.

We can represent other behaviors with different colors. Those color values not defined will be ignored. Thus, pixels in the background (such as the light-gray pixels in Figure 3) can be ignored.

Listing 1 shows how the mapping is accomplished. The image is first scanned for specific color values, then the position of each color pixel is used to define a control state at that location in the map of control states. In the Escape example, six behaviors are defined by the various STATE_xxx constants.

Listing 1. Processing a control path image

Map map = new HashMap();
public final static int STATE_UNKNOWN = -1;
public final static int STATE_NONE = 0;
public final static int STATE_HALLWAY = 1;
public final static int STATE_INTERSECTION = 2;
public final static int STATE_HINT = 3;
public final static int STATE_START = 4;
public final static int STATE_EXIT = 5;
/** Process the control image */
void processControl(Image img, int x, int y, int w, int h) 
    int pmap[] = new int[w * h];
    PixelGrabber pg = new PixelGrabber(img, x, y, w, h, pmap, 0, w);
    try {
        if ((pg.getStatus() & ImageObserver.ABORT) != 0) {
            System.err.println("image fetch error");
        else {
            Integer none = new Integer(STATE_NONE);
            Integer hall = new Integer(STATE_HALLWAY);
            Integer start = new Integer(STATE_START);
            Integer exit = new Integer(STATE_EXIT);
            Integer hint = new Integer(STATE_HINT);
            Integer inter = new Integer(STATE_INTERSECTION);

            // for each position
            for (int i = 0; i < pmap.length; i++) {
                int red   = (pmap[i] >> 16) & 0xff;
                int green = (pmap[i] >>  8) & 0xff;
                int blue  = (pmap[i]      ) & 0xff;
                if      (red == 255 && green == 255 && blue == 255) 
                    ; // don't bother to add NONE to map
                else if (red == 0 && green == 0 && blue == 0)
                    map.put(new Integer(i), hall);
                else if (red == 0 && green == 255 && blue == 0)
                    map.put(new Integer(i), start);
                else if (red == 200 && green == 0 && blue == 0)
                    map.put(new Integer(i), exit);
                else if (red == 255 && green == 0 && blue == 0)
                    map.put(new Integer(i), hint);
                else if (red == 0 && green == 0 && blue == 255)
                    map.put(new Integer(i), inter);
    catch (InterruptedException e) {
        System.err.println("image processing interrupted");

Variety is the spice of life
Although specific color values are used in Listing 1 (that is, red is 200, green is 0, and blue is 0) we could easily enhance the code to support color ranges. Using color ranges reduces the precision of the color selection used in the drawing program, thus making it easier to create a path image.

Using a wider selection of colors allows you to define many more states, and also to describe much more complicated behaviors. For example, you could use different color bands in an RGB scheme to create overlapping control paths. If each of the above states were encoded by different intensities of a single color rather than by different colors, three independent control paths could be overlaid on top of each other. Of course, using different intensities of a single color makes it harder to discern the subtle color differences between behaviors. Most image editor programs display exact color values of the selected pixel, mitigating this problem.

It's also possible to define more than three control paths. If you accessed each color value through a bitmask, you would be limited only by the number of bits in your image format (typically 24; 32 if you used alpha values). Using single bits for a path is more complex than using color bands but it can be done. You would either need to have a program merge together separate image control paths or use a single image and additive painting. If you did not need to support overlapping paths (that is, have multiple states exist at one position), you could have 2^24 (or 2^32) states at each position. You could also mix these two approaches. For example, using the red band via a bitmask and the green and blue bands for other states.

Figure 4 shows the complete control path image used for our Escape simulation. Notice the use of multiple colors, and how the colors are used to represent different behaviors at different locations.

Figure 4. The whole control path
Image of the whole control path

Figure 5 magnifies a particular section of the control path for greater clarity.

Figure 5. Partial control path detail
An image of a section of the control path magnified several times

Run for your lives!
After we've defined a state map, we can begin to move around objects in the 2D space. The example Escape application models moveable objects as instances of the Entity class. Two major subclasses have been defined: a Person and an Alarm. Persons can move while Alarms are stationary. The Entity interface is defined in Listing 2.

Listing 2. The Entity interface

interface Entity {
    void addToPanel(JPanel panel, boolean shared);
    void updateTick();

The addToPanel() method creates one or more Swing components to represent the objects and adds them to the provided panel. The components are typically JLabels with an icon set. The panel is typically the implementation of the 2D space. Its background displays the animation background.

The updateTick() method causes the object to animate itself for each cycle of the animation. Alarm objects change their color to create a flashing effect. Person objects move.

Alarm objects are simple components that blink, as implemented in Listing 3.

Listing 3. Alarm.updateTick: Flash

public void updateTick() {
    if (++tick % CYCLE == 0) {
        opaque = !opaque;

Person objects are more complex than Alarms. They move about along the defined control paths as shown in Listing 4.

Listing 4. Person.updateTick: Move along the path

/** Move one step along the path */
public synchronized void updateTick() {
    Integer tock = stops.get(new Integer(tick));
    if (tock != null) {   // adjust startTime if requested
        startTick = tick + tock.intValue();
    if (tick < startTick) return;    // not my time yet
    if (isAtExit()) return;

    // Process individual movement
    Point2D location = getPosition();
    int x = (int)location.getX();
    int y = (int)location.getY();
    switch (manager.stateAt(x, y)) {
        case BuildingManager.STATE_EXIT:
            atExit = true;

        case BuildingManager.STATE_START:
        case BuildingManager.STATE_INTERSECTION:
            // process any hints
            if      (manager.stateAt(x - 1, y) == 
            else if (manager.stateAt(x + 1, y) == 
            else if (manager.stateAt(x, y + 1) == 
            else if (manager.stateAt(x, y - 1) == 

            // no hints, select a direction
            if (getDirection() == DIR_NONE) {
                if      (manager.stateAt(x - 1, y) != 
                else if (manager.stateAt(x + 1, y) != 
                else if (manager.stateAt(x, y + 1) != 
                else if (manager.stateAt(x, y - 1) != 

        case BuildingManager.STATE_HALLWAY:
        case BuildingManager.STATE_HINT:
            // effect motion in selected direction
            int tempX = x;
            int tempY = y;
            switch (getDirection()) {
                case DIR_EAST:  x += 1; break;
                case DIR_WEST:  x -= 1; break;
                case DIR_NORTH: y -= 1; break;
                case DIR_SOUTH: y += 1; break;
            int check = manager.stateAt(x, y);
            if (check == manager.STATE_UNKNOWN ||
                check == manager.STATE_NONE) {
                // went off the path, backup
                x = tempX;
                y = tempY;
                if (getDirection() == DIR_EAST || 
                    getDirection() == DIR_WEST) {
                    if (manager.stateAt(x, y + 1) != 
                        BuildingManager.STATE_NONE && 
                        manager.stateAt(x, y + 1) != 
                        BuildingManager.STATE_UNKNOWN) {
                        y += 1;
                    else {
                        // Only direction not checked is north
                        y -= 1;
                else {
                    if (manager.stateAt(x + 1, y) != 
                        BuildingManager.STATE_NONE && 
                        manager.stateAt(x + 1, y) !=
                        BuildingManager.STATE_UNKNOWN) {
                        x += 1;
                    else {
                        // Only direction not checked is south
                        x -= 1;
            setNextPoint(new Point(x, y));

This fairly complex method basically examines the map around the current position. It then selects the best new position to go to. It works by attempting to go in the same direction as much as possible. Note that hints are markers that provide a preferred direction. They are typically used at starting locations or intersections.

A Person can stop prematurely (that is, before reaching the end of a path), or be set to delay the start of motion for a fixed time. Person objects are also capable of leaving a fading trail of images (or history) to depict their motion, as shown in Figure 6.

Figure 6. A person moving with history shadows
Image of a person moving with history shadows

Everyone gets a turn
Listing 5 shows the logic for moving entities. This process is performed once per animation cycle.

Listing 5. Move all entities

/** Move the entities around the pattern */
public void moveEntities() {
    // update (move) the people 
    for (Iterator iter = people.iterator(); iter.hasNext();) {
        Object next =;
        if (next instanceof Person) {
    // update the other entities
    for (Iterator iter = entities.iterator(); iter.hasNext();) {
        Object next =;
        if (next instanceof Entity) {

Each new frame in the animation is created by the process shown in Listing 6. Note that the entities add themselves to the frame.

Listing 6. Add entities at current positions

/** Advance the animation */
public void prepareNextFrame(boolean update, boolean shared) {
    if (update) {
    // add the people 
    for (Iterator iter = manager.getPeople().iterator(); 
         iter.hasNext();) {
        Person person = (Person);
        person.addToPanel(mainPanel, shared);
    // add the entities
    for (Iterator iter = manager.getEntities().iterator(); 
         iter.hasNext();) {
        Entity entity = (Entity);
        entity.addToPanel(mainPanel, shared);

We achieve continuous animation with the code in Listing 7. The delay value controls how fast the animation runs, and also how much CPU time it takes.

Listing 7. For the life of the animation

public void runUpdates(int delay) {
    TimerTask task = new TimerTask() {
        public void run() {
            SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        if (manager.getPeople().size() == 0) {
    (new Timer(true)).scheduleAtFixedRate(task, 0, delay);

Below are several snapshots of our animated sequence. Figure 7 shows the escape simulation just before the action heats up (approximately 10 percent complete).

Figure 7. Escape sequence near the start
Image of the escape sequence animation near the beginning

Figure 8 shows the animated sequence at approximately 50 percent complete.

Figure 8. The escape sequence following some updates
Image of the escape sequence animation in the middle

Figure 9 shows the sequence near its completion (although the sequence actually runs forever).

Figure 9. Near the end of the escape sequence
Image of the escape sequence animation near the end

In Figure 10 you can see how the entities move along the control paths, which should give you a greater idea of how the motion is actually achieved.

Figure 10. Entities in motion along the control paths
Image of an escape sequence animation showing the control paths instead of the normal background

Painting the scene
Class BuildingViewer creates the container in which the objects are moved. The paintChldren() method first draws the background image, then any alert message, and finally the subcomponents representing the various entities.

Listing 8. BuildingViewer.paintChildren

private int paintCount;
private static final Color evacColor = new Color(255, 0, 0, 128);
/** draw the background, massage and entities */
public void paintChildren(Graphics g) {

    if (background != null) {
        Graphics g2 = g.create();
        try {
            // draw the background
                         (int)getLocation().getX() + getWidth(), 
                         (int)getLocation().getY() + getHeight(),
                         0, 0, 
               , null);

            // draw the alert message (if any)
            if (alertMessage != null) {
                if (paintCount % alertPeriod >= (alertPeriod / 2)) { 
                    Font f = g2.getFont();
                    Font f2 = f.deriveFont((float)alertSize);
                    FontMetrics fm = Toolkit.getDefaultToolkit().
                    int fHeight = fm.getHeight(), 
                        fAscent = fm.getAscent();
                    int sWidth = fm.stringWidth(alertMessage);
                    Graphics2D g2d = (Graphics2D)g2;
                    g2d.setStroke(new BasicStroke(10));
                                  (getWidth() - sWidth) / 2, 
                                  (getHeight() - fHeight) / 2 + 
        } finally {

To create an effective animation, you need objects to animate. Listing 9 shows the code to create a series of Person entities of the same type (that is, disabled, non-disabled, firefighters, etc.) based on the provided inputs.

Listing 9. Create entities of the same type

/** initialize paths */
protected static void initLoop(BuildingManager manager, ImageIcon icon,
                               int[] locs, String[] names, 
                               int[] starts, int[] appear, int[][][] stops)
    LinkedList startPts = (LinkedList)manager.
    // for all specified locations - create a Person 
    for (int i = 0; i < locs.length; i++) {
        JLabel label = new JLabel(names[i], icon, JLabel.CENTER);
        label.setFont(new Font(label.getFont().getName(),
                               label.getFont().getStyle(), 20));
        Person person = new Person(manager, label,
                                     Math.min(startPts.size() - 1, locs[i])),

        // defines stop locations for each Person
        for (int j = 0; j < stops[i].length; j++) {
            person.addStop(stops[i][j][0], stops[i][j][1]);

Listing 10 defines a set of Person entities using the initLoop code from Listing 9. This code uses several parallel arrays (based on the length of the locs array) to provide information about the objects to be created. The locs array provides an index into the set of defined starting locations, as provide by the control path. The starts value specifies at what timer tick the Person is to begin to move. The appear value defines the timer tick when the Person should become visible (often before it starts to move). The stops values specify the (possibly multiple) stop points each Person can have.

Although shown as hand-typed values below, it is possible to get most of these input values from the control path by adding new colors that represent states that position entities. This enhancement can simplify the input of these values and make them less subject to error when the control paths change.

Listing 10. Create all Person entities

/** Make some demo people */
static public void createPeople(BuildingManager manager, 
                                ImageIcon employIcon,
                                ImageIcon fireIcon,
                                ImageIcon disabledIcon)
    // Main character - ALEX
    int locs[] = new int[] {42};
    String names[] = new String[] {"Alex"};
    int starts[] = new int[] { 300 };
    int appear[] = new int[] { 0 };
    int stops[][][] = new int[][][] {{}};
    initLoop(manager, employIcon, locs, names, starts, appear, stops);

    // Some disabled people
    locs = new int[] { 39, 45 };
    names = new String[] { "Karen", "Mike" };
    starts = new int[] { 0, 0};
    appear = new int[] { 0, 0 };
    stops = new int[][][] {{{1, 164}, {560, 20}}, 
                           {{1, 141}, {460, 30}}};
    initLoop(manager, disabledIcon, locs, names, starts, appear, stops);

    // Some Assisters
    locs = new int[] {44, 49, 37, 46};
    names = new String[] { "Tom", "Joe", "Cathy", "Larry" };
    starts = new int[] { 0, 0, 0, 0};
    appear = new int[] { 0, 0, 0, 0 };
    stops = new int [][][] {{{120, 52}, {560, 20}},
                            {{155, 24}, {560, 20}}, 
                            {{122, 27}, {460, 30}}, 
                            {{100, 59}, {460, 30}}};
    initLoop(manager, employIcon, locs, names, starts, appear, stops);

    // A firemen
    locs  = new int[] { 25 };
    names = new String[] { "FD", "FD 2", "FD 3" };
    starts = new int[] { 400, 400, 400};
    appear = new int[] { 400, 400, 400 };
    stops = new int [][][] {{},{}, {}};
    initLoop(manager, fireIcon, locs, names, starts, appear, stops);

      :  **** many additional definition sets omitted ****

And, finally, Listing 11 shows how to create an Alarm entity. Obviously, we can easily add more alarms as we need them.

Listing 11. Create alarms

/** Make some demo alarms */
static public void createAlarms(BuildingManager manager) {
    final int alarms[] = { 12 };
    LinkedList startPts = (LinkedList)manager.
    for (int i = 0; i < alarms.length; i++) {
            new Alarm(manager, (Point2D)startPts.get(alarms[i])));

In this article, we have shown you how to use lossless images, Swing technology, and a custom animation engine for motion-path generation in 2D animation. This method allows us to visualize the animation as we create it via control paths in a quick and predictable way. Some of the advantages of this technique are as follows:

Ease of use
Most image editors have a number of ways to generate straight lines, rounded arcs, and other shapes. These options allow us to generate some paths by hand quickly while reducing error. For some behaviors, this is very useful.

Reference images
When the animation is moving relative to a background image (such as in Figure 1), we may wish to move objects within the confines of items in that image, for example keeping objects within the confines of hallways. Many image editors will allow us to use a semi-transparent layer to generate our control image over our background image. We then can easily create our control paths to match our background image, as we can see both images lined up while we generate the control image.

Additive painting
By blending colors, we are able to encode multiple behaviors at a position. For example, using RGB colors we can use red (0xFF0000)to represent the path to follow for one object and green (0x00FF00) to encode the path to follow for another object. Using additive painting, a point at which the paths intersect will be yellow (0xFFFF00).

With this additive model, when using, say, a 32-bit color model, up to 32 different behaviors can be easily extracted from a given position like a bitmask. Although we have only described one simple behavior, the number of behaviors that can be encoded is limited only by the number of bits assigned to each color in the image format.

We have also shown and described a simple animation engine for moving objects around the sets of paths. Each object, called an Entity and implemented as a JLabel in the example, is driven periodically to update its position and/or appearance. A long-running timer thread is assigned to drive this process. A JPanel is used as the container of the objects and also as the means of painting the background. See Resources for the complete code for the animation example and the Java-based animation engine introduced in this article.


  • Download the source code for the animation engine and the example animation sequence used in this article.

  • Visit to learn more about JFC and Swing.

  • While you're at it, you might also want to read up on the Java 2D API and the Java Advanced Imaging API.

  • Mitch Goldstein's tutorial, Introduction to Java 2D offers a step-by-step guide to the advantages of advanced drawing, text layout, and image manipulation that Java 2D brings to GUI programming.

  • Learn more about image creation and manipulation using the Java 2D API with "Creating Java2D composites for rollover effects" (developerWorks, September 2002).

  • The UK-based Technical Advisory Service for Images (TASI) has provided a useful overview of the various file formats and compression techniques for digital images, including a section on lossless image compression techniques.

  • Yakov Nekrich has compiled a good set of links about lossless image compression.

  • Learn more about some of the image types mentioned in this article -- namely JPEGs, PNGs, and GIFs.

  • Barry Feigenbaum has also written "Coding for accessibility" (developerWorks, October 2002), which shows you how to use Swing/JFC and a unique Accessibility Toolkit to build more accessible Java applications.

  • You'll find hundreds of articles about every aspect of Java programming in the developerWorks Java technology zone.

  • For a listing of free Java-based programming tutorials, see the developerWorks Java tutorials page.

About the authors
Dr. Barry Feigenbaum is a member of the IBM Worldwide Accessibility Center, where he is part of a team that helps IBM make its products accessible to people with disabilities. Dr. Feigenbaum has published several books and articles, holds several patents, and has spoken at industry conferences such as JavaOne. He serves as an Adjunct Assistant Professor of Computer Science at the University of Texas, Austin. You can contact Dr. Feigenbaum at

Tom Brunet is a graduate student at the University of Wisconsin-Madison. He received his B.S. in Computer Science from the University of Texas at Austin, where he was named as a Dean's Honored Graduate. During his undergraduate studies, he worked for IBM Research for four years, spending a summer with the Data Abstraction Research group, and the remainder with the Accessibility Center. He also worked concurrently as an undergraduate research assistant for Dr. Nina Amenta. You can reach Tom at

code389 KBe-mail it!

What do you think of this document?
Killer! (5) Good stuff (4) So-so; not bad (3) Needs work (2) Lame! (1)


developerWorks > Java technology
  About IBM  |  Privacy  |  Terms of use  |  Contact