A Music Shelf in Java2D

You can run the WebStart demo or, as usual, download its source code.

The demo I'm about to present today was built using other demos I showed before on this blog: Jewel Case with Java2D, StackLayout and Swing Glint. Before going any further, I advice you to open the files CDShelf.java and CrystalCaseFactory.java from the sources archive. The CD Shelf (or Music Shelf or whatever) component has originally been created to design a login dialog. I cannot show you this dialog because of copyrights issues with the pictures I used but, since we used it at Sun Microsystems to build a chat client, you'll probably see it if we decide to release the code of this client. Anyway, this is why you will see a lot of mentions to avatars in the code.

If you haven't tried the demo yet, you must first take a look at the result to better understand the explanations:

You can browse among the albums by clicking on them or by using the arrow keys on your keyboard (left, right, up, down, page up and page down) and also home and end to go directly to the first or last item. The code does support the selection of an item when you press enter or click the one in the center. You can insert your own code into the inner classes KeyAvatarSelector and MouseAvatarSelector to handle the selection. In the case of the login dialog, selecting an item displays the password field:

If I recall correctly, the implemention we used in the chat client allows to register an action listener on the component. But the interesting part here is the 3D effect. You can see it better when browsing the items: as they scroll, their size and opacity changes.

Drawing the layout of the final result helps to understand how to implement the final effect:

As you can see on this poorly drawn schema, the further the items are from the center, the smaller. By doing this we give the impression the items are also further in the distance, on the Z axis. To reinforce this impression, we also want to change the opacity, making the items more and more transparent as they are supposed to be further. To achieve this we need to find a relation between the size/opacity and the location of the item on screen. Unfortunately the answer is mathematics (hate'em #@!).

In this demo, the albums seem to evolve along a bump centered on screen but you can hack the code to make it work with any path. The idea here is to use a simple two-dimensional plot and to project it on the Z axis. Given the position of an item on the X axis, we compute its position on the Y axis of the plot and use the result as the Z position. Consider this picture:

This is the plot of the equation used in the demo. Now imagine the albums move along the curve. The item in the middle will be the highest whereas the items on the edges will lie below. If we put each album on the curve and project the Y axis on the Z axis we know the depth of each album in our virtual world.

Now we can define the position of each album we need to define our world. To make it easier to implement we consider that the center of the screen is the origin of the X axis. Thus the album in the middle is at position x=0. Both left and right egdes of the screen are at positions x=-1.0 and x=1.0. Mapping the positions from the virtual world to the screen space is done during the painting and proves to be really easy with these numbers.

The case of the Z axis (depth) is more interesting. The idea is to make our items disappear in the distance. Since we change both size and opacity, the best thing to do is to get a Z position in the range [0.0, 1.0], 1.0 being meaning full-sized, fully opaque. To sum up, we want a mathematical function complying to these two constraints:

  • f[-1.0, 1.0] -> [0.0, 1.0]
  • f(0.0)~=1.0

The function must also give a curve shape you like for the scrolling effect. As I explained during my talk, I remembered one of my teachers showing us how he was using a Gaussian distribution curve to average our grades. And this curve just has the shape I wanted to use for this demo. After playing with it a while I came up with slightly modified version of the equation:

I used this equation to draw the previous plot. You can see two parameters, σ and ρ. The first parameter, σ, controles the shape of the curve. The following two graphs use respectively σ=0.43 and σ=1.0:

This parameter is really important since it can help you define how fast the items disappear in the distance. If you study closely these diagrams, you can see the curves is not at all in the range [0.0, 1.0] on the Y axis. This is why I introduced the second parameter, ρ. Its role is simply to constrain the results in the appropriate range. To compute ρ solve the equation with ρ=1.0. The result of this computation is the ρ you want to use. You can change σ anytime you want from your code by calling cdShelf.setSigma(0.5);. And here is what happens:

private double sigma;
private double rho;
private double exp_multiplier;
private double exp_member;

public void setSigma(double sigma) {
  this.sigma = sigma;
  this.rho = 1.0;
  this.rho = computeModifierUnprotected(0.0);
  this.damaged = true;

private void computeEquationParts() {
  exp_multiplier = Math.sqrt(2.0 * Math.PI) / sigma / rho;
  exp_member = 4.0 * sigma * sigma;

private double computeModifier(double x) {
  double result = computeModifierUnprotected(x);
  if (result > 1.0) {
      result = 1.0;
  } else if (result 

The methode computeEquationParts() is reponsible for caching the parts of the equation which remains constant with a given σ. Getting the position of an item on the curve is done by calling computeModifier(positionX). This method protects the values to ensure the result is in the expected range. Given the poor precision of our ρ parameter we might get results close to 1.0 and 0.0 but sometimes greater or lower. Note the value 0.0 can be a problem when you compute the size of the album with it, Java2D doesn't like zero-sized pictures :) Well, enough mathematics for now. If you take a look at the code, you'll see the component keeps track of the position of each item on the screen and use it to change their size and opacity.

Now let's take a look at how the albums are loaded and drawn on the screen. The component uses two collections, List<Image> avatars for the pictures and List<String> avatarsText for the label of each item. These collections are meant to be filled in by the inner class PictureFinderThread. In this demo, all the pictures and labels are statically loaded but you can easily change this behavior. For each picture to be loaded, we need to create the CD case and the reflection:


We use aninstance of CrystalCaseFactory called fx to perform this rendering operation. I won't explain how it works since I already did it in Swing Glint. Creating the CD case is just a matter of stacking a few pictures together. Finally, when at least avatarAmount items have been loaded, the component selects the one in the middle and starts drawing items:

if (i++ == avatarAmount) {
  setAvatarIndex(avatarAmount / 2);

The fader is just a nicety which makes the items appear progressively. Calling setAvatarIndex() selects the currently selected item, that is the one at the center of the world. This is really important to understand the difference between the selected state and the position. When we scroll through the items, the selected one might be at another position than the center. But let's see how everything is drawn.

In paintComponent() you can see the most important lines of code:

if (damaged) {
  drawableAvatars = sortAvatarsByDepth(x, y, width, height);
  damaged = false;

drawAvatars(g2, drawableAvatars);

if (drawableAvatars.length > 0) {

To increase performances, the component uses to tricks. First, it knows when it has been damaged, that is resized or hidden. In such a case it must rebuild its layout, that is to say it needs to compute the position and state (size/opacity) of each item. But it doesn't need to do this for all the items. We just want to know the state of each drawable item, the ones which are actually on screen. This is the purpose of the array drawableAvatars created by sortAvatarsByDepth():

private DrawableAvatar[] sortAvatarsByDepth(int x, int y,
                                            int width, int height) {
  List drawables = new LinkedList();
  for (int i = 0; i 

This method goes through all the available items and attempts to promote them into the drawable list by calling promoteAvatarToDrawable():

private void promoteAvatarToDrawable(List drawables,
                                     int x, int y, int width, int height,
                                     int offset) {

  double spacing = offset * avatarSpacing;
  double avatarPosition = this.avatarPosition + spacing;

  if (avatarIndex + offset = avatars.size()) {

  Image avatar = avatars.get(avatarIndex + offset);

  int avatarWidth = displayWidth;//avatar.getWidth(null);
  int avatarHeight = displayHeight;//avatar.getHeight(null);

  double result = computeModifier(avatarPosition);
  int newWidth = (int) (avatarWidth * result);
  if (newWidth == 0) {
  int newHeight = (int) (avatarHeight * result);
  if (newHeight == 0) {

  double avatar_x = x + (width - newWidth) / 2.0;
  double avatar_y = y + (height - newHeight / 2.0) / 2.0;

  double semiWidth = width / 2.0;
  avatar_x += avatarPosition * semiWidth;

  if (avatar_x >= width || avatar_x 

Ouch! Well this code is actually pretty simple. Given an item, it computes its position in the world, taking into account the desired spacing between the items. Once the position in the world is known, the new size of the item is computed using the result of the equation we discovered earlier. Finally, the code translates the world position into pixel position and checks whether it falls between the bounds of the component. Speaking of this, there is small bug in the test avatar_x >= width || avatar_x which doesn't take into account the x and y parameters (defined by the size of the border, if any, set on the component). Nothing really serious though :) Finally, the drawable item is built and added to the list. A drawable item contains the position (on screen and in the world) of the item, its size in pixel and its position on the Z axis, called zOrder in the code. The class DrawableAvatar implements Comparable to sort the items according to their depth (zOrder).

And that's pretty much it! The final step is to draw the promoted items:

private void drawAvatars(Graphics2D g2, DrawableAvatar[] drawableAvatars) {
  for (DrawableAvatar avatar: drawableAvatars) {
    AlphaComposite composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 
                                                          (float) avatar.getAlpha());
                 (int) avatar.getX(), (int) avatar.getY(),
                 avatar.getWidth(), avatar.getHeight(), null);

Since all the information are available from DrawableAvatar, this method doesn't do much. It just honors the opacity before drawing. You can also see we resize the picture at drawing time since it has proven to be the fastest way to resize a picture. If you wondered why the loading of the albums takes so long, it is because of resizing operations happening in memory instead of on screen.

I invite you to take a look at the code of the numerous inner classes to see how to animate the scrolling and how to handle the click on the items. Just know that setPosition() changes the position of the selected item (identified by avatarIndex). The basic idea is to change the position until the newly selected item is in place.

Overall the code is not very complicated but quite long. It is also far from perfect given the time I took to write it and the fact it went through major changes from the first mockup to this demo. I now have 4 versions of this component, each slightly different from the others. This demo is a mix of the first 3 so I know the code is sometimes awkward :)

Finally, if you want to do something when enter is pressed or when the selected item is clicked, consider uncommenting removeInputListeners(). For instance, you could display a white veil over the component and show the cover art and some information about the selected album. When you do that, you might want to disable the listeners responsible for scrolling through the albums. Simply remove the listeners and when the user dismisses the information, call addInputListeners(). We used this technique in the chat client and I hope we'll be able to show you the code very soon.

If you have any question regarding this piece of code, just ask and I'll do my best to give you an answer. Oh and while I'm at it, here is a little gift taken from Joplin. It is a nice picture you can use when you show an album for which you don't have the cover art (click onto the CD case to get the picture you can use with JewelCaseFactory):

4 Responses to “A Music Shelf in Java2D”

  1. David Hansen says:

    For some reason this code seems to crsh when having loaded more thn 113 pics…any idea why?

  2. David Hansen says:

    Ehhh…nevermind tht, turns out I ran out of memory :s

  3. Garth says:

    Under what license can the Music Shelf in Java2D code be used?

  4. interesting blog. It would be great if you can provide more details about it. Thanks you