Skip to main content

Java 2D Trickery: Light and Shadow

Posted by campbell on July 27, 2006 at 4:20 PM PDT


Note: The code snippets in this blog entry are intended to be used with Scott Violet's nifty Interactive Graphics Editor. Just cut and paste the code into that application, et voila: instant gratification. This allows you to tinker with the code and immediately see how your changes affect the rendering.

In the first installment of my "Trickery" series, I demonstrated a technique for achieving a soft clipping effect. Now let's put it to good use. In this installment I'll show how to add a lighting effect to give your otherwise flat shapes a 3D-ish appearance.

If you like your literature illustrated, perhaps a picture will help. I'll show you how to go from the boring, flat shape on the left to the slightly less boring, glossy shape on the right:


Flat Shape
Glowing Shape

With the right colors, you can use this technique to simulate a colored light shining across your shape, producing a subtle glow. How do we achieve this effect? Check out the code below; the comments above the drawBorderGlow() method explain the core approach in a bit more detail:

[prettify]
import java.awt.geom.*;
import java.awt.image.*;

private static final Color clrHi = new Color(255, 229, 63);
private static final Color clrLo = new Color(255, 105, 0);

private static final Color clrGlowInnerHi = new Color(253, 239, 175, 148);
private static final Color clrGlowInnerLo = new Color(255, 209, 0);
private static final Color clrGlowOuterHi = new Color(253, 239, 175, 124);
private static final Color clrGlowOuterLo = new Color(255, 179, 0);

private Shape createClipShape() {
    float border = 20.0f;

    float x1 = border;
    float y1 = border;
    float x2 = width - border;
    float y2 = height - border;

    float adj = 3.0f; // helps round out the sharp corners
    float arc = 8.0f;
    float dcx = 0.18f * width;
    float cx1 = x1-dcx;
    float cy1 = 0.40f * height;
    float cx2 = x1+dcx;
    float cy2 = 0.50f * height;

    GeneralPath gp = new GeneralPath();
    gp.moveTo(x1-adj, y1+adj);
    gp.quadTo(x1, y1, x1+adj, y1);
    gp.lineTo(x2-arc, y1);
    gp.quadTo(x2, y1, x2, y1+arc);
    gp.lineTo(x2, y2-arc);
    gp.quadTo(x2, y2, x2-arc, y2);
    gp.lineTo(x1+adj, y2);
    gp.quadTo(x1, y2, x1, y2-adj);
    gp.curveTo(cx2, cy2, cx1, cy1, x1-adj, y1+adj);
    gp.closePath();
    return gp;
}

private BufferedImage createClipImage(Shape s) {
    // Create a translucent intermediate image in which we can perform
    // the soft clipping
    GraphicsConfiguration gc = g.getDeviceConfiguration();
    BufferedImage img = gc.createCompatibleImage(width, height, Transparency.TRANSLUCENT);
    Graphics2D g2 = img.createGraphics();

    // Clear the image so all pixels have zero alpha
    g2.setComposite(AlphaComposite.Clear);
    g2.fillRect(0, 0, width, height);

    // Render our clip shape into the image.  Note that we enable
    // antialiasing to achieve the soft clipping effect.  Try
    // commenting out the line that enables antialiasing, and
    // you will see that you end up with the usual hard clipping.
    g2.setComposite(AlphaComposite.Src);
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setColor(Color.WHITE);
    g2.fill(s);
    g2.dispose();

    return img;
}

private static Color getMixedColor(Color c1, float pct1, Color c2, float pct2) {
    float[] clr1 = c1.getComponents(null);
    float[] clr2 = c2.getComponents(null);
    for (int i = 0; i < clr1.length; i++) {
        clr1[i] = (clr1[i] * pct1) + (clr2[i] * pct2);
    }
    return new Color(clr1[0], clr1[1], clr1[2], clr1[3]);
}

// Here's the trick... To render the glow, we start with a thick pen
// of the "inner" color and stroke the desired shape.  Then we repeat
// with increasingly thinner pens, moving closer to the "outer" color
// and increasing the opacity of the color so that it appears to
// fade towards the interior of the shape.  We rely on the "clip shape"
// having been rendered into our destination image already so that
// the SRC_ATOP rule will take care of clipping out the part of the
// stroke that lies outside our shape.
private void paintBorderGlow(Graphics2D g2, int glowWidth) {
    int gw = glowWidth*2;
    for (int i=gw; i >= 2; i-=2) {
        float pct = (float)(gw - i) / (gw - 1);

        Color mixHi = getMixedColor(clrGlowInnerHi, pct,
                                    clrGlowOuterHi, 1.0f - pct);
        Color mixLo = getMixedColor(clrGlowInnerLo, pct,
                                    clrGlowOuterLo, 1.0f - pct);
        g2.setPaint(new GradientPaint(0.0f, height*0.25f,  mixHi,
                                      0.0f, height, mixLo));
        //g2.setColor(Color.WHITE);

        // See my "Java 2D Trickery: Soft Clipping" entry for more
        // on why we use SRC_ATOP here
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, pct));
        g2.setStroke(new BasicStroke(i));
        g2.draw(clipShape);
    }
}

Shape clipShape = createClipShape();
//Shape clipShape = new Ellipse2D.Float(width/4, height/4, width/2, height/2);

// Clear the background to white
g.setColor(Color.WHITE);
g.fillRect(0, 0, width, height);

// Set the clip shape
BufferedImage clipImage = createClipImage(clipShape);
Graphics2D g2 = clipImage.createGraphics();

// Fill the shape with a gradient
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setComposite(AlphaComposite.SrcAtop);
g2.setPaint(new GradientPaint(0, 0, clrHi, 0, height, clrLo));
g2.fill(clipShape);

// Apply the border glow effect
paintBorderGlow(g2, 8);

g2.dispose();

g.drawImage(clipImage, 0, 0, null);
[/prettify]

Note that I've left a few "alternate" lines of code commented out in the example above. Feel free to try uncommenting those lines in IGE and see how they affect the rendering.

Bonus: Astute readers may notice that the same technique used in the paintBorderGlow() method above could be used to add a drop shadow around the shape. Care to guess how this would work? Okay, time's up. Instead of rendering the border on top of our shape (remember that the clip ensures that the stroke only touches the inside of the shape), we can render a varying gray border around our shape beforehand. This means the shadow stroke will appear outside of our shape; the inner part of the shadow stroke will be effectively rendered over by our shape.

Here's some more code you can insert into the above example to add a shadow border to that same shape:

[prettify]
private void paintBorderShadow(Graphics2D g2, int shadowWidth) {
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                        RenderingHints.VALUE_ANTIALIAS_ON);
    int sw = shadowWidth*2;
    for (int i=sw; i >= 2; i-=2) {
        float pct = (float)(sw - i) / (sw - 1);
        g2.setColor(getMixedColor(Color.LIGHT_GRAY, pct,
                                  Color.WHITE, 1.0f-pct));
        g2.setStroke(new BasicStroke(i));
        g2.draw(clipShape);
    }
}

// Apply the border shadow before we paint the rest of the shape
paintBorderShadow(g, 6);
[/prettify]

And here's the resulting image:


Shadow

Note that this is just a quick attempt at adding a drop shadow for demonstration purposes. If I wasn't so lazy, I'd probably use a lighter gray color and a non-linear ramp to achieve a more realistic effect. Also note that this is just one of many ways to add a drop shadow using Java 2D. Romain has discussed different drop shadow implementations in his blog here and here. The SwingLabs folks have a DropShadowBorder in the SwingX project, and DropShadowPanel is currently in the Incubator.

In the next installment of my "Trickery" series, I'll move away from low-level Java 2D effects and show you how to add animations to your applications using the SwingX Painters API and Chet's Timing Framework [tm]. In the meantime, let's see if Josh and Richard pick up the slack and turn some of the above code into stock painters (nudge nudge).




In my ears: Wire, "Chairs Missing" [for about the 3 billionth time in my life]


In my eyes: "Idiot's Guide To Buying A Home" [I really hate growing up...]

Related Topics >>