Sun Java Solaris Communities My SDN Account Join SDN
 
Core Java Technologies Tech Tips

Java 2D Trickery

By Chris Campbell, September 23, 2006  


In This Issue

Welcome to the Core Java Technologies Tech Tips for September 2006. Core Java Technology Tech Tips provide hints and tips to help you get the most out of core Java technologies and APIs, such as those in the Java 2 Platform, Standard Edition (J2SE).

This issue covers:

» Java 2D Soft Clipping
» Java 2D Light and Shadows

These tips were developed using Java 2 Platform, Standard Edition Development Kit 5.0 (JDK 5.0). You can download JDK 5.0 at http://java.sun.com/j2se/1.5.0/download.jsp.

Chris Campbell, a Java 2D software engineer at Sun Microsystems, provided this issue's tech tips.

See the Subscribe/Unsubscribe note at the end of this newsletter to subscribe to Tech Tips that focus on technologies and products in other Java platforms.
 

Note: You can use Scott Violet's Interactive Graphics Editor (IGE) to run the code samples in this month's tech tips. Cut and paste the code into the Interactive Graphics Editor application to see how your changes affect rendering.

Java 2D Soft Clipping

If you're familiar with Java 2D, you probably already know that you can use any arbitrary shape to clip your rendering. For example, you can give the illusion of someone shining a flashlight on your application by setting a circular clip and then rendering your scene normally. You can read the Java Tutorial for refresher material about Java 2D clipping.

When you clip using a complex shape, you will typically see "jaggies" around the edges of the clipped region. Jaggies are ugly, ragged edges on your rendered images. To illustrate an effect I call "hard clipping," try the following example:

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

// Set the clip shape
g.clip(new java.awt.geom.Ellipse2D.Float(width/4, height/4, width/2, height/2));

// Fill the area with a gradient; this will be clipped by our ellipse, but
// note the ugly jaggies
g.setPaint(new GradientPaint(0, 0, Color.RED, 0, height, Color.YELLOW));
g.fillRect(0, 0, width, height);
    

Here's the resulting image:

Frame1

Figure 1: Clipping can create hard, ragged edges.

Wouldn't it be nice if you could antialias those hard edges to remove the jaggies caused by clipping? Unfortunately Sun's current implementation of Java 2D does not support "soft clipping." I was surprised to find that when I tried the above code on my Mac, there were no jaggies! What's going on here? It turns out that Apple's Java 2D implementation uses Quartz under the hood, which appears to do soft clipping by default. Apple is planning to use Sun's software renderer instead of their Quartz renderer by default in Java SE 6, so these tips should be relevant for Macs as well.

You might expect a RenderingHint object to control this behavior, but sorry, no such luck. A few developers have asked for soft clipping in the past, but it doesn't seem to be common enough to warrant adding support for it in our implementation.

Fortunately, we've found a fairly simple way to achieve a soft clipping effect using an intermediate image (see Chet Haases' article on that subject) and a little known AlphaComposite rule known as SrcAtop. Note that SrcIn would work equally well in this example, but SrcAtop has the added benefit that it blends the source with the destination, as opposed to simply replacing it. Try out the following code snippet:

import java.awt.image.*;

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

// 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.fillOval(width/4, height/4, width/2, height/2);

// Here's the trick... We use SrcAtop, which effectively uses the
// alpha value as a coverage value for each pixel stored in the
// destination.  For the areas outside our clip shape, the destination
// alpha will be zero, so nothing is rendered in those areas.  For
// the areas inside our clip shape, the destination alpha will be fully
// opaque, so the full color is rendered.  At the edges, the original
// antialiasing is carried over to give us the desired soft clipping
// effect.
g2.setComposite(AlphaComposite.SrcAtop);
g2.setPaint(new GradientPaint(0, 0, Color.RED, 0, height, Color.YELLOW));
g2.fillRect(0, 0, width, height);
g2.dispose();

// Copy our intermediate image to the screen
g.drawImage(img, 0, 0, null);
    

Compare the resulting image in Figure 2 to the jaggy one above:

Antialiased soft clip
Figure 2: Antialiasing creates a soft clipping effect.

Looks better, right? I'll admit that this example is a bit contrived, and it might be hard to see the real world applicability. However, my next next tip will show you how to apply this technique when creating a lighting effect for arbitrary shapes.

Java 2D Light and Shadows

In the first tip I demonstrated a technique for achieving a soft clipping effect. Now let's put it to good use. In this tip I'll show how to add a lighting effect to give your otherwise flat shapes a 3D 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 imageGlowing image
Figure 3: Use colors to simulate a glowing effect.

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:

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);

Note that I've left a few "alternate" lines of code commented out in the example above. Feel free to try uncommenting those lines to 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:

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);
    

And here's the resulting image:

Shadow
Figure 4: Add a drop shadow using Java 2D.

Note that this is just a quick attempt at adding a drop shadow for demonstration purposes. If I weren'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 Guy has discussed different drop shadow implementations these two blog entries: Non Rectangular Shadows and Fast or Good Drop Shadows. The SwingLabs folks have a DropShadowBorder in the SwingX project, and DropShadowPanel is currently in the Incubator.

For More Information

You can get more information about Java 2D and the Java Platform from the following sources:

Rate and Review
Tell us what you think of the content of this page.
Excellent   Good   Fair   Poor  
Comments:
Your email address (no reply is possible without an address):
Sun Privacy Policy

Note: We are not able to respond to all submitted comments.