The Source for Java Technology Collaboration
User: Password:



Kirill Grouchnikov

Kirill Grouchnikov's Blog

How to create your own icons

Posted by kirillcool on February 25, 2005 at 05:50 AM | Comments (16)

Almost every application with GUI needs icons. And they better be sexy. And stylish. And consistent. And small. Here are few tips for programatically creating icons using Java 2D features

First, here is a screenshot of a demo that shows icons created using the techniques described in this entry. As you can see, it can paint any letter on any background, creating an optional plus sign and optional arrow that can point either to the right or to the left (note that the actual quality of the icons in GUI is better then saved JPEG version):
.
Here is an enlarged shot of a letter G:

The important visual details in the icons:
  • The letter itself is anti-aliased
  • A whitish highlight spot is in the upper left corner of the icon to make it look 3-D
  • This spot does not influence the black border of the icon, which is also anti-aliased
  • The plus sign has a semi-transparent white halo. Without halo, the red color is too close to the half-black pixels of the border. With completely opaque white color, the sign stands out too much
  • The arrow has semi-transparent white halo. Around horizontal part, the halo is more opaque, around the arrow head the halo is more transparent

And now for the interesting part - the code itself. First we create a completely transparent image. This way, when the icon will be drawn on non-white background, it will blend in nicely:
   BufferedImage image = new BufferedImage(ICON_DIMENSION, ICON_DIMENSION,
      BufferedImage.TYPE_INT_ARGB);
   // set completely transparent
   for (int col = 0; col < ICON_DIMENSION; col++) {
      for (int row = 0; row < ICON_DIMENSION; row++) {
         image.setRGB(col, row, 0x0);
      }
   }
Now, retrieve the graphics context of this image and set rendering hints for antialiasing
   Graphics2D graphics = (Graphics2D) image.getGraphics();
   graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
      RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
   graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON);
Now, we fill a circle with specified background color
     
   graphics.setColor(backgroundColor);
   graphics.fillOval(0, 0, ICON_DIMENSION - 1, ICON_DIMENSION - 1);
Now, before we draw the border, we create a whitish spot. This one is not particularly tricky - based on the distance from a pixel to the spotlight location, make the color of the current pixel closer to white color (or the color of the spotlight). Important - don't forget to preserve the transparency (or opacity) of each pixel:
    
   // create a whitish spot in the left-top corner of the icon
   double id4 = ICON_DIMENSION / 4.0;
   double spotX = id4;
   double spotY = id4;
   for (int col = 0; col < ICON_DIMENSION; col++) {
      for (int row = 0; row < ICON_DIMENSION; row++) {
         // distance to spot
         double dx = col - spotX;
         double dy = row - spotY;
         double dist = Math.sqrt(dx * dx + dy * dy);

         // distance of 0.0 - comes 90% to Color.white
         // distance of ICON_DIMENSION - stays the same

         if (dist > ICON_DIMENSION) {
            dist = ICON_DIMENSION;
         }

         int currColor = image.getRGB(col, row);
         int transp = (currColor >>> 24) & 0xFF;
         int oldR = (currColor >>> 16) & 0xFF;
         int oldG = (currColor >>> 8) & 0xFF;
         int oldB = (currColor >>> 0) & 0xFF;

         double coef = 0.9 - 0.9 * dist / ICON_DIMENSION;
         int dr = 255 - oldR;
         int dg = 255 - oldG;
         int db = 255 - oldB;
   
         int newR = (int) (oldR + coef * dr);
         int newG = (int) (oldG + coef * dg);
         int newB = (int) (oldB + coef * db);

         int newColor = (transp << 24) | (newR << 16) | (newG << 8)
            | newB;
         image.setRGB(col, row, newColor);
      }
   }
Now, draw the outline of the icon in black. Here, based on the background color, we can choose the color of the border as lying somewhere 70-80% on the way from the background color to black. In this way, the icon will have matching border color.
   
   // draw outline of the icon
   graphics.setColor(Color.black);
   graphics.drawOval(0, 0, ICON_DIMENSION - 1, ICON_DIMENSION - 1);
Now, take the input letter and make it capital (this looks much better on icons). Then, set font that is a few pixels smaller than the icon dimension. Compute the bounds of this letter, and set the position for this letter so that it will be centered in the icon's center:
       
   letter = Character.toUpperCase(letter);
   graphics.setFont(new Font("Arial", Font.BOLD, ICON_DIMENSION-5));
   FontRenderContext frc = graphics.getFontRenderContext();
   TextLayout mLayout = new TextLayout("" + letter, graphics.getFont(),
      frc);

   float x = (float) (-.5 + (ICON_DIMENSION - mLayout.getBounds()
      .getWidth()) / 2);
   float y = ICON_DIMENSION
      - (float) ((ICON_DIMENSION - mLayout.getBounds().getHeight()) / 2);
Now we can draw the letter (in black color):
   // draw the letter
   graphics.drawString("" + letter, x, y);
Put optional plus sign in the top-right corner of the icon. First, create a semi-transparent white background for it, and then draw a red plus-sign two pixels thick:
   // if collection - draw '+' sign
   if (
   int height = 6;
   BufferedImage image = new BufferedImage(width, height,
      BufferedImage.TYPE_INT_ARGB);
   // set completely transparent
   for (int col = 0; col < width; col++) {
      for (int row = 0; row < height; row++) {
         image.setRGB(col, row, 0x0);
      }
   }
Get the graphics context, set antialiasing hint and draw an arrow of specified color:
   Graphics2D graphics = (Graphics2D) image.getGraphics();
   graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON);

   // draw arrow
   Polygon pol = new Polygon();
   int ya = 3;
   pol.addPoint(1, ya);
   pol.addPoint(width / 2 + 3, ya);
   pol.addPoint(width / 2 + 3, ya + 2);
   pol.addPoint(width - 1, ya);
   pol.addPoint(width / 2 + 3, ya - 2);
   pol.addPoint(width / 2 + 3, ya);
   graphics.setColor(color);
   graphics.drawPolygon(pol);
And now for the tricky part - we have to compute the halo. Here, if an arrow pixel was completely opaque, it should have less transparent halo than arrow pixel that was only partly opaque (as on arrow's head for example). Here, we create another image with the halo footprint, and then draw the original arrow on top of it. Each arrow pixel contributes to its 8 neighbouring pixels. The final opacity of the halo footprint is the maximal opacity of all neighbouring arrow pixels:
   // create semi-transparent halo around arrow (to make it stand
   // out)
   BufferedImage fimage = new BufferedImage(width, height,
      BufferedImage.TYPE_INT_ARGB);
   // set completely transparent
   for (int col = 0; col < width; col++) {
      for (int row = 0; row < height; row++) {
         fimage.setRGB(col, row, 0x0);
      }
   }
   Graphics2D fgraphics = (Graphics2D) fimage.getGraphics();
   for (int col = 0; col < width; col++) {
      int xs = Math.max(0, col - 1);
      int xe = Math.min(width - 1, col + 1);
      for (int row = 0; row < height; row++) {
         int ys = Math.max(0, row - 1);
         int ye = Math.min(height - 1, row + 1);
         int currColor = image.getRGB(col, row);
         int opacity = (currColor >>> 24) & 0xFF;
         if (opacity > 0) {
            // mark all pixels in 3*3 area
            for (int x = xs; x <= xe; x++) {
               for (int y = ys; y <= ye; y++) {
                  int oldOpacity = (fimage.getRGB(x, y) >>> 24) & 0xFF;
                  int newOpacity = Math.max(oldOpacity, opacity);
                  // set semi-transparent white
                  int newColor = (newOpacity << 24) | (255 << 16) |
                     (255 << 8) | 255;
                  fimage.setRGB(x, y, newColor);
               }
            }
         }
      }
   }
The final step - reduce the opacity of the halo by 30%. This is needed to reduce complete opacity around vertical and horizontal lines:
   // reduce opacity of all pixels by 30%
   for (int col = 0; col < width; col++) {
      for (int row = 0; row < height; row++) {
         int oldOpacity = (fimage.getRGB(col, row) >>> 24) & 0xFF;
         int newOpacity = (int)(0.7*oldOpacity);
         int newColor = (newOpacity << 24) | (255 << 16) |
            (255 << 8) | 255;
         fimage.setRGB(col, row, newColor);
      }
   }
Now, draw the original arrow on top of its halo
   // draw the original arrow image on top of the halo
   fgraphics.drawImage(image, 0, 0, null);
Going back to the original image (the letter and optional plus sign) - draw the arrow image on top of it:
   BufferedImage arrowImage = getArrowImage(arrowColor, ICON_DIMENSION);
   graphics.drawImage(arrowImage, 0, ICON_DIMENSION
     - arrowImage.getHeight(), null);
Arguably, this was a lot of code. On the other hand, you can now create icon with any letter, any background, optional plus sign and optional arrow of any color. See how nice your application can become:

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

  • This is a good idea. It is also nice that your "icons" can scale to different resolutions, unlike a static icon.

    Posted by: burke_e on February 25, 2005 at 06:57 AM


  • Hmmm... interesting... like in the Chinese curse that is :P


    Alternatively, you might want to check The Iconfactory:


    http://iconfactory.com/


    And their excellent, royalty-free, stock icons:


    http://www.stockicons.com/


    http://www.stockicons.com/browse.asp

    Posted by: zoe_info on February 25, 2005 at 08:06 AM

  • zoe_info,
    Missing the whole point here. You don't have to go searching for icons on the web and then make compromises on colors, sizes etc. You don't have to bundle gif images with your application and use URL, ClassLoader etc. You don't have to worry if they are royalty free or have been copied by that web site from another one, which is not. You can make them yourself, at run-time, with little extra effort.
    You can make them change color schemes when you change your application skin. You can change them when you change look & feel. You can scale them along with fonts. You can do anything, because you create them.

    Posted by: kirillcool on February 25, 2005 at 12:07 PM

  • Neat technique! Can you post the code that makes the "Image creator demo" screenshot?

    Posted by: mprudhom on February 25, 2005 at 04:14 PM

  • This same technique can be used to auto-generate cursors for a given icon. For example, suppose you click the Find button on the toolbar, which has a Binoculars icon. Well the cursor would then be the usual arrow cursor, but with a mini Binoculars icon beside it.

    We use this technique in JUMP, which is an open-source Java GIS.

    Posted by: jonathanaquino on February 25, 2005 at 10:07 PM

  • Hi Kirill,

    Those are some nice looking icons... However, I think you could greatly simpilify the code that generates them if you leverage Java 2D. Note that BufferedImage.setRGB() usually is not the most efficient approach, especially for the types of operations you are using.

    First, don't use setRGB() to make an image completely transparent. Instead, use:

    g.setComposite(AlphaComposite.Src);
    g.setColor(new Color(0));
    g.fillRect(0, 0, w, h);


    To achieve the whitish spot effect in the upper-left corner, set a GradientPaint that goes from whitish in the upper-left to your background color in the lower-right. Then call g.fillOval() so that you fill the background of the oval in one easy step. That will be way more efficient than your manual approach, and it should achieve the same effect.

    Finally, to create the semi-transparent halo around the arrow... First render the arrow with a wide stroke in your translucent white color, and then render it again with a thin stroke in the black color. For example:

    Stroke orig = getStroke();
    g.setColor(translucentWhite);
    g.setStroke(new BasicStroke(3.0f));
    g.draw(arrow);
    g.setColor(black);
    g.setStroke(orig);
    g.draw(arrow);


    Hope this helps...

    Chris
    (Java 2D Team)

    Posted by: campbell on February 25, 2005 at 11:23 PM

  • Chris, thanks for the tips. A couple of comments, though.
    I couldn't use GradientPaint - the spot itself is in the middle of the top-left quarter, and not in the corner. Also, having pixel-wise control can be easily extended into Phong shading for bigger icons. In addition, I wanted to demonstrate more technical approach to creating the halo, not using the bigger stroke. It's all a question of having pixel-wise control for more precise manipulations. In this case, the halo (after creation) is made 30% more transparent. This can easily be achieved using strokes. But if i wanted to create non-linear fall-off, strokes are no good.
    Thanks again for the suggestions.

    Posted by: kirillcool on February 26, 2005 at 12:45 AM

  • mprudhom,
    Nothing special in the demo. I have two functions in ImageCreator

    public static ImageIcon getSingleLetterIcon(char letter,
    boolean isCollection, Color backgroundColor);
    public static ImageIcon getLetterIconWithArrow(char letter,
    boolean isCollection, Color backgroundColor,
    boolean isArrowToRight, Color arrowColor);

    The demo is somewhat a hack:

    private static class ImageCreatorFrame extends JFrame {
    private ImageIcon[][] icons;

    private int columns;

    private int rows;

    public static final int ICON_DIM = 20;

    public ImageCreatorFrame() {
    super("Image creator demo");

    char[] letters = { 'a', 'b', 'c', 'e', 'f', 'g', 'm', 'w', 'q', 'z' };
    Color[] bgColors = { new Color(96, 224, 96),
    new Color(141, 112, 255), new Color(32, 128, 255) };
    Color[] arrColors = { new Color(128, 32, 0), new Color(0, 128, 32) };

    this.columns = (2 + 4 * arrColors.length) * bgColors.length;
    this.rows = letters.length;
    this.icons = new ImageIcon[columns][rows];

    for (int row = 0; row < letters.length; row++) {
    char letter = letters[row];
    int col = 0;
    for (int bgColorIndex = 0; bgColorIndex < bgColors.length; bgColorIndex++) {
    Color bgColor = bgColors[bgColorIndex];
    icons[col++][row] = ImageCreator.getSingleLetterIcon(
    letter, false, bgColor);
    icons[col++][row] = ImageCreator.getSingleLetterIcon(
    letter, true, bgColor);
    for (int arColorIndex = 0; arColorIndex < arrColors.length; arColorIndex++) {
    Color arColor = arrColors[arColorIndex];
    icons[col++][row] = ImageCreator
    .getLetterIconWithArrow(letter, false, bgColor,
    false, arColor);
    icons[col++][row] = ImageCreator
    .getLetterIconWithArrow(letter, false, bgColor,
    true, arColor);
    icons[col++][row] = ImageCreator
    .getLetterIconWithArrow(letter, true, bgColor,
    false, arColor);
    icons[col++][row] = ImageCreator
    .getLetterIconWithArrow(letter, true, bgColor,
    true, arColor);
    }
    }
    }

    int width = this.columns * ICON_DIM + 20;
    int height = (this.rows + 2) * ICON_DIM;
    Dimension dim = new Dimension(width, height);
    this.setPreferredSize(dim);
    this.setSize(dim);
    this.setResizable(false);
    }

    public void paint(Graphics g) {
    for (int col = 0; col < this.columns; col++) {
    for (int row = 0; row < this.rows; row++) {
    g.drawImage(this.icons[col][row].getImage(), 5 + col
    * ICON_DIM, 35 + row * ICON_DIM,
    (ImageObserver) null);
    }
    }
    }
    }

    Given that default icon dimension is 15, setting ICON_DIM to 20 gives 5 pixel gap between the icons.


    Posted by: kirillcool on February 26, 2005 at 12:49 AM

  • Good article, Kirill.
    One other source for royalty free icons is

    http://www.iconexperience.com

    There you can find 1500 high quality icons designed with a consistent style.

    Posted by: jansan on February 28, 2005 at 05:59 AM

  • Kiril & also Chris, nice posts. Thanks

    Posted by: ronaldyang on February 28, 2005 at 09:27 AM

  • Nice work. I tried to do a similar thing some time ago, but tried to use less than the pixel-level control you do here and the results were disappointing. Could you post the entire class? I'd love to actually try to run this.

    Posted by: scott_sauyet on February 28, 2005 at 01:38 PM

  • Scott,
    The complete code can be found in the new project under development on java.net. You can download source jar file from https://jaxb-workshop.dev.java.net/files/documents/2861/11407/jaxbw-src.jar. It contains the current version of ImageCreator in it. Note that it contains additional functions for run-time creation of icons and images. For example, it provides functions for creating error and success images, gradient lines and so on.

    Posted by: kirillcool on February 28, 2005 at 09:38 PM

  • Thanks, I'll take a look. Again, a very enjoyable article. -- Scott

    Posted by: scott_sauyet on March 01, 2005 at 06:24 AM

  • Nice blog, but I suggest having a look at the fantastic free LGPLed KDE icon theme "Nuvola" at Icon-King.com I

    Posted by: jroar on March 01, 2005 at 06:50 AM

  • Hello

    Nice Demo - But how can i store the icons to my harddisk. My problem is to store an Icon.

    Posted by: dude on April 23, 2005 at 08:02 AM

  • dude,
    Create BufferedImage, get its Graphics, ask your icon to draw itself (drawIcon) on that Graphics and then use JAI to save your BufferedImage to disk.

    Posted by: kirillcool on April 23, 2005 at 08:15 AM





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