Punched out buttons with inner shadows

April 16, 2009

Mac OS X adds a nice visual accent to icons that are added to textured buttons. When you add a black icon to a textured button in Interface Builder, the black image acts like a mask and punches out part of the button. Here’s a close up look at the punched out effect:

unified_toolbar

You can see that the punched out area is filled with a gradient paint and a slight drop shadow. I’ve replicated the effect in Java, which you can see below. I’ve included the original image, along with the punched out product.

genius einstein1
plus plus_big

Here’s an icon with this effect added to a Java Mac OS X textured button — it’s subtle, but these are the details that give your UI a really polished look.
gear_button
And of course, here’s the code to produce these icons:

public class PunchIconFactory {
    public static Icon createPunchedIcon(Image image, 
                                         int unblurredShadowSize_pixels) {
        // create an image in which to draw the given image with the inner
        // shadow.
        BufferedImage newImage = new BufferedImage(image.getWidth(null),
                image.getHeight(null), BufferedImage.TYPE_4BYTE_ABGR);

        // 1) paint a gradient background in the resultant image.
        Graphics2D graphics = newImage.createGraphics();
        GradientPaint paint = new GradientPaint(0,0,Color.BLACK,0,
                image.getHeight(null),new Color(0x3e3e3e));
        graphics.setPaint(paint);
        graphics.fillRect(0, 0, newImage.getWidth(), newImage.getHeight());

        // 2) paint the given image into resultant image, only keeping pixels that
        //    existed in the given image.
        graphics.setComposite(AlphaComposite.DstIn);
        graphics.drawImage(image, 0, 0, null);

        // 3) create an inner shadow for the given image.
        BufferedImage shadowImage = 
                createInnerShadow(image, unblurredShadowSize_pixels);
        graphics.setComposite(AlphaComposite.SrcAtop);
        graphics.drawImage(shadowImage,0,0,null);

        graphics.dispose();

        return new ImageIcon(newImage);
    }

    private static BufferedImage createInnerShadow(
            Image image, int unblurredShadowSize_pixels) {
        // create an image padded by the shadow size. this allows the given
        // image to abut the edge and still receive an inner shadow. if we don't
        // do this, an image that abuts the edge will have a shadow that is
        // blurred into full alpha transparency, when it should in fact be opaque.
        int twiceShadowSize = unblurredShadowSize_pixels * 2;
        BufferedImage punchedImage = new BufferedImage(
                image.getWidth(null) + twiceShadowSize,
                image.getHeight(null) + twiceShadowSize,
                BufferedImage.TYPE_INT_ARGB);

        // 1) start by filling the entire rectangle with black.
        Graphics2D graphics = punchedImage.createGraphics();
        graphics.setColor(new Color(0,0,0,140));
        graphics.fillRect(0, 0, punchedImage.getWidth(), punchedImage.getHeight());
        // 2) next erase the given image from the previously drawn rectangle. this
        //    punches out the image from the rectangle, which will let the "light"
        //    flow through when we create the drop shadow. note that we're moving
        //    down and to left shadowSize pixels to compensate for the pad, then
        //    another shadowSize pixels to offset the image.
        graphics.setComposite(AlphaComposite.DstOut);
        graphics.drawImage(image, twiceShadowSize, twiceShadowSize, null);
        graphics.dispose();

        // create a drop shadow for the punched out image.
        BufferedImage innerShaodowImage = createLinearBlurOp(
                unblurredShadowSize_pixels).filter(punchedImage, null);

        // return an image of the original size. we're subtracting off the pad
        // that we added in the beginning which was only used to allow images
        // that abut the edge.
        return innerShaodowImage.getSubimage(unblurredShadowSize_pixels,
                unblurredShadowSize_pixels, punchedImage.getWidth()-twiceShadowSize,
                punchedImage.getHeight()-twiceShadowSize);
    }

    private static ConvolveOp createLinearBlurOp(int size) {
        float[] data = new float[size * size];
        float value = 1.0f / (float) (size * size);
        for (int i = 0; i < data.length; i++) {
            data[i] = value;
        }
        return new ConvolveOp(new Kernel(size, size, data));
    }

7 Responses to “Punched out buttons with inner shadows”

  1. Jambo Says:

    Nice article. Very useful.

  2. Jambo Says:

    I studied this topic some time ago. Your method is really nice. The only thing that is missing is the highlights on the borders of the punched out icon of Apple. As light comes from top left, if look carefuly, you will notice that the hightlights appear on the borders that sould be exposed to the light. One simple method is to apply a very small drop shadow with a light color (white with transparency).

  3. Ken Says:

    Good eye Jambo. I also realized that I was missing the little white highlight on the bottom side of the punched out image, but decided not to tackle that detail right now.

    Great feedback.
    -Ken

  4. Harald K. Says:

    Hi Ken! Nice effect!

    One could argue that your effect looks better, especially in larger sizes. But to me it looks like the OS X icons has the light straight down (not down/right), so the shadow and highlight should only be painted below the edges.

    Anyway, it looks great as always. :-)

    .k

  5. Ken Says:

    Hi Harald,

    Good eye — your right that the light in Apple’s version is coming from directly above. Either way, I think it adds the right amount of visual interest.

    Thanks for the feedback!
    -Ken


  6. […] Orr has a very interesting post on creating punched out buttons with inner shadows , a.k.a the Mac OS […]

  7. Lars Says:

    Truly impressive.


Leave a comment