While developing a new feature for a product, I decided to create a splash screen that illustrated the metaphor of pieces falling into place. A little animation seemed the perfect trick for the job. To spice up your own splash screen, you can use the code presented in this article. Check Resources for the complete source.

Figure 1 illustrates the idea for the animation, using my company's slogan as the example. To create the desired effect, we want to chop up the slogan into its individual characters. These characters will fly in from all sides of the splash screen, ultimately falling into place to create the slogan. To keep the view realistic, the characters' trajectories should replicate something like a cannonball shot from a cannon. So the code needs to incorporate some basic physics—don't worry, the math is really simple.

Figure 1. Each character flies in from three sides

Figure 2 shows the final result after the animation completes. We'll also use a gradient paint for the background to add a little texture.

Figure 2. Animated splash screen

Calculate the trajectory

First things first—we need to decide how the characters, or sprites, will move about the screen. Animators use the term sprite to describe a small moving bitmap. We're going to randomly assign a starting position for each sprite along the top three sides of the view—left, top, right—in traditional x, y coordinates (i.e, not screen coordinates). We assign a final position based on the location of the sprite in the slogan text. The essence of our problem is this: we need to calculate an initial velocity for both the x and y directions for each sprite such that each sprite lands in the proper position after the allotted simulation time. Since we want a realistic trajectory, we'll want to include a "gravity field" in the y direction that pulls each sprite towards its final destination. Then we can launch our "cannonball" sprites toward their final targets.

The first equation we need comes from basic physics:

Equation 1 says that our sprite's final position is equal to velocity multiplied by time, plus the initial position. Since there isn't a gravity field along the horizontal axis, the velocity for each sprite along the x direction will be constant. Each sprite will start at a different x position, so the distance each sprite needs to cover will differ. Therefore, each sprite's velocity—though constant—will differ from the velocity of the other sprites.

The y direction is trickier since we want to include a gravity field, which will pull the cannonball (sprite) back towards ground (the final sprite position). In this case, velocity isn't going to be constant, so we need another equation from Physics 101:

Equation 2 says that our sprite's velocity equals acceleration multiplied by time, plus the sprite's initial velocity. Like gravity, our acceleration will be constant. So, we can plug Equation 2 into Equation 1 for the y direction and get:

Write the application

Now that we've figured out the basic physics, we're ready to start writing some code. We start by creating a new class, AnimationBanner, to serve as the entry point. Let's use a few static fields to capture the fundamental parameters for the simulation:

                        public class AnimationBanner {
   // Setup and tuning parameters...
   // Frame size
   private static int WIDTH = 700;
   private static int HEIGHT = 500;
   // Duration of the simulation in ms (default 1 sec)
   private static final int SIM_LEN = 1000;
   // The update frequency in Millisec
   private static final int DELAY = 20;
   // Acceleration parameter
   private static final double ACCEL = -0.0005;
   // Name of the file containing the slogan
   private static final String SLOGAN_FILE = "slogan.gif";
   // Number of chars in the slogan file
   private static final int NUM_SLOGAN_CHARS = 20;
   // Name of company logo file
   private static final String LOGO_FILE = "logo.gif";
   // Name of trademark file
   private static final String TRADEMARK_FILE = "tm.gif";
                   

The SIM_LEN is the simulation time, which is 1,000 milliseconds, or one second; DELAY is the update frequency; and ACCEL is the acceleration parameter. We just tune the ACCEL parameter to give a nice looking animation, which saves us the trouble of working with units of mass, gravity/acceleration, etc.

Most of the fields reference the gif files we place on our splash screen. The file slogan.gif captures the slogan "Software Made Simple." Our slogan has 20 characters, so we assign the value of 20 to the field NUM_SLOGAN_CHARS. The file logo.gif is the logo, which we place in the center of our splash screen. And tm.gif contains the trademark characters "TM." We could include the TM characters at the end of the slogan file, but since they're offset a bit and smaller, it's easier to just treat them as a separate sprite.

Now we need some code to launch our application:

                        public static void main(String[] args) {
   JFrame frame = new JFrame("Animation");
   frame.setLocation(100,100);
   frame.setSize(WIDTH, HEIGHT);
   frame.addWindowListener(new WindowAdapter(){
      public void windowClosing(WindowEvent e) {
         System.exit(0);
      }
   });
   AnimationBanner banner = new AnimationBanner();
   frame.setContentPane(banner.getAnimation());
   frame.setVisible(true);
   initAnimation(animation);
   banner.getAnimation().start();
}
                   

Our AnimationBanner class includes a JPanel to display the animation, so we use that as the content pane for our JFrame. Once the frame is visible, we need to initialize the animation and then start it.

Much of the work required for our application is in initializing the animation. We need to chop up our slogan.gif file into the proper number of sprites, randomly assign a starting position, and then calculate the initial velocity for each sprite such that it lands in the correct position after the simulation completes. One note of caution: Rather than use a bitmap for each sprite, we could have drawn each letter on the screen. Unfortunately, even with anti-aliasing turned on, the letters do not give a crisp simulation. Bitmaps are the way to go.

We use a little helper class to store the information we calculate for each sprite:

                        private static class SpriteImage {
   public Image image;
   public int x0;
   public int y0;
   public int xF;
   public int yF;
   public double vX0;
   public double vY0;
}
                   

Since we're all friends here, we won't worry about getters and setters for this class. Now we create the sprites:

                        // Location of the final text string
private static Point origin = new Point();
private static void initAnimation(Animation animation) {
   // Adjust width and height to parent frame size
   if (animation.getParent() != null) {
      WIDTH = animation.getParent().getWidth();
      HEIGHT = animation.getParent().getHeight();
   }
   // Origin in screen coords
   origin.x = WIDTH/2 - sloganImage.getWidth(null)/2;
   origin.y = (int)(HEIGHT * 0.75);
      
   // Chop up the slogan image to get a char for each sprite
   SpriteImage[] spriteImages = 
      createSpriteImages(sloganImage, NUM_SLOGAN_CHARS);
   for (SpriteImage spriteImage : spriteImages) {
      ProjectileSprite sprite = 
         new ProjectileSprite(spriteImage.image, 
         spriteImage.x0, spriteImage.y0, 0, spriteImage.vX0, 
         spriteImage.vY0, HEIGHT);
      animation.addSprite(sprite);
   }
      
   // Add the tm sprite adjusting to a raised position
   origin.x = origin.x + sloganImage.getWidth(null) - 10;
   Image tmImage = animation.getSpriteImage(TRADEMARK_FILE);
   spriteImages = createSpriteImages(tmImage, 1);
   for (SpriteImage spriteImage : spriteImages) {
      ProjectileSprite sprite = 
         new ProjectileSprite(spriteImage.image, 
         spriteImage.x0, spriteImage.y0, 0, spriteImage.vX0, 
         spriteImage.vY0, HEIGHT);
      animation.addSprite(sprite);
   }
}
                   

First, we reset the width and height based on the size of the parent frame—these are used to calculate a reference point for the slogan's final location, which is stored in a Point variable called origin. The createSpriteImages() method builds an array of SpriteImage. We'll look at that next.

Once we have the sprite images, we loop through them and create an instance of the class ProjectileSprite for each image. ProjectileSprite draws each sprite at each moment in the simulation.

We need one last sprite for the trademark gif, and that's created next. We offset it just a bit so it's raised above the slogan.

The real work begins in createSpriteImages(). Since this method is a bit long, I'll break it up a bit to make it easier to follow along.

First, we compute the width and height of each sprite image. The width of each sprite is calculated by dividing the length of the slogan by the number of characters. So, we pass in the desired number of characters to this method. For height we just use the height of the bitmap. Then we generate a random number based on the current time. The random number is used to randomly place the sprites about the edges of the animation at start time. That way, we receive a different animation each time it runs:

                        private static SpriteImage[] 
createSpriteImages(Image baseImage, int n) {
   n = n == 0 ? 1 : n;
   
   SpriteImage[] spriteImages = new SpriteImage[n];
   
   int h = baseImage.getHeight(null);
   int w = baseImage.getWidth(null)/n;
   
   Random rand = new Random(System.currentTimeMillis());
                   

Next, we use the random number to divide the sprites into two groups—those that start along the top edge (top) and those that fly in from the sides (left). Eventually, we'll divide up the sprites in the group left into two groups—one for the right side and one for the left side:

                        // Number of sprites along top edge
   int top = rand.nextInt(n);
   // Number of remaining sprites at x= 0
   int left = rand.nextInt(n - top);
   // Collection of sprites to pull from randomly
   List<SpriteImage> sprites = new ArrayList<SpriteImage>();
                   

Now we make a first pass through the sprites to set their final positions. We draw the sprites in an image buffer and then store the associated image with the sprite's SpriteImage:

                        // First pass to set the final positions
   for (int i = 0; i < n; i++) {
      spriteImages[i] = new SpriteImage();
      BufferedImage image = 
         new BufferedImage(w, h, BufferedImage.TRANSLUCENT);
      Graphics2D g2 = image.createGraphics();
      g2.translate(-(w * i), 0);
      g2.drawImage(baseImage, 0, 0, null);
      g2.translate((w * i), 0);
      spriteImages[i].image = image;
      // Final pos in projectile coords
      int xF = origin.x + w * i;
      int yF = HEIGHT - origin.y;
      spriteImages[i].xF = xF;
      spriteImages[i].yF = yF;
      sprites.add(spriteImages[i]);
   }
                   

Next, we translate the Graphics2D for the image based on the width of the sprite and its position index in the slogan, draw it, and then translate back. It's important to always translate the Graphics2D back to the starting position, because it retains the state of the last command. The key code:

                        g2.translate(-(w * i), 0);
      g2.drawImage(baseImage, 0, 0, null);
      g2.translate((w * i), 0);
                   

Finally, we calculate the final positions and store them with the SpriteImage. Since it's easiest for our equations to use traditional orthogonal x and y axes (i.e., y increasing upwards, x increasing to the right), we need to adjust for screen coordinates. Once we're finished, we store the sprite in an array for use in a second pass:

                        // Second pass to randomly set the initial positions
   for (int i = 0; i < n; i++) {
      // Grab a random sprite
      int size = sprites.size();
      SpriteImage spriteImage = null;
      if (size != 1) {
         int j = rand.nextInt(size - 1);
         spriteImage = sprites.remove(j);
      }
      else {
         spriteImage = sprites.get(0);
      }
      int y0 = 0; int x0 = 0;
      // Initial pos in projectile coords
      if (i < top) {
         y0 = HEIGHT + 30;
         x0 = rand.nextInt(WIDTH);
      }
      else {
         y0 = HEIGHT - rand.nextInt(HEIGHT);
         if (i - top < left) {
            x0 = 0;
         }
         else {
            x0 = WIDTH;
         }
      }
                   

In the second pass, we calculate the initial position for each sprite. On each pass through the list of sprites we previously saved, we grab one at random and then remove it from the list—we only want to process it once. Then we set the initial position for the sprite. All sprites along the top edge have constant height. Their positions along the x axis are computed using a new random number. That way, we get a random distribution of the sprites along the top edge.

Once we've processed the sprites for the top of the animation, we now assign sprites along the right and left edges. Again, a random number is used to set the height so we get a random distribution along both edges:

Page 2 of 2
                        double vX0 = (double)(spriteImage.xF - x0)/SIM_LEN;
   double vY0 = (double)(spriteImage.yF - y0 � 
      ACCEL * Math.pow(SIM_LEN, 2.0))/SIM_LEN;
   spriteImage.x0 = x0;
   spriteImage.y0 = y0;
   spriteImage.vX0 = vX0;
   spriteImage.vY0 = vY0;
}
   return spriteImages;
}
                   

Now we use our equations to set the initial velocity for both the x and y directions.

The projectile sprite

We copy all of the initialization data into an instance of ProjectileSprite. Its job is to calculate its current position based on time and to draw itself when asked. So it includes a couple of the equations we calculated previously for position:

                        public int getX() {
   int x = (int)(vX0 * t + x0);
   return x;
}
public int getY() {
   return (int)(a*Math.pow(t, 2) + vY0 * t + y0);
}
                   

To draw the ProjectileSprite we need to implement a paint(Graphics2D g2) method, where we pass in the Graphics2D associated with the real drawing area.

The animation loop

We need one more class before we're ready to run. We need an animation loop as well as somewhere to actually draw the animation, i.e., a JPanel. For that, we create a new class called Animation. The animation loop is fundamental to all animations—it's the engine that makes the whole thing go. We can choose from two basic approaches: create a new Thread or use a TimerTask. I prefer TimerTask because it provides more control over the timing loop. We include our animation loop in a new private class that extends TimerTask. We place the private class inside Animation:

                        private int time = 0;
private int duration = 0;
private class AnimationTask extends TimerTask {
   public void run() {
   time += delay;
   ProjectileSprite.setTime(time);
   if (!paintBackground(getBufferGraphics())) return;
   for (ISprite sprite : sprites) {
      sprite.paint(getBufferGraphics());
   }
   getGraphics().drawImage(imageBuffer, 0, 0, Animation.this);
   if (time >= duration) {
      timer.cancel();
   }
}
                   

Once we launch TimerTask, it will call the run() method at the specified interval (delay). A delay of 20 milliseconds gives a smooth simulation.

The logic for run() is simple: paint both the background and logo image into an off-screen buffer. We then copy the buffer to the screen. When the simulation time exceeds the desired duration, cancel the timer. We use a gradient paint to add a little texture to our simulation background. Since the logo image is static, we paint that when we paint the background:

                        private boolean paintBackground(Graphics2D g2) {
   if (g2 == null) return false;
   if (getHeight() <= 0 || getWidth() <= 0) return false;
   getBufferGraphics().setPaint(new GradientPaint(0, getHeight(),
      startColor, getWidth(), getHeight(), endColor));
   getBufferGraphics().fillRect(0, 0, getWidth(), getHeight());
   getBufferGraphics().drawImage(logoImage, logoX, logoY, null);
   return true;
}
                   

One more method and we're ready to run. To keep the simulation speedy, we must draw into an off-screen buffer:

                        private Graphics2D getBufferGraphics() {
   if (imageBuffer == null || width != getWidth() 
      || height != getHeight()) 
   {
      width = getWidth();
      height = getHeight();
      imageBuffer = new BufferedImage(getWidth(), 
         getHeight(), BufferedImage.TYPE_INT_RGB);
      logoX = getWidth()/2 - logoImage.getWidth(null)/2;
      logoY = (int)(getHeight() * 0.25);
      bufferGraphics = imageBuffer.createGraphics();
   }
   return bufferGraphics;
}
                   

Conclusion

Our work is done—time to have some fun. Launch the application and watch the letters fly. I included a MouseListener that reruns the simulation after a mouse click. You can play with the ACCEL parameter—more acceleration requires a higher launch velocity, and the characters travels farther.

In this article, you've learned how to liven up a boiler-plate splash screen with animation, learned some basic physics, and had some fun along the way. Java is perhaps not the first choice for animating a professional-quality game, but it's a great choice for fast and fun animations.

Michael Banghamis president of Mammoth Software. He has more than 20 years of programming experience and has been programming primarily in Java for the last five years. He holds bachelor degrees in both electrical and mechanical engineering, and holds a master's degree in mechanical engineering. In addition to writing lots of Java, he is also pretty good with an electric guitar.

Learn more about this topic