Welcome
» 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.
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.
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:
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.
RenderingHint
Fortunately, we've found a fairly simple way to achieve a soft
clipping effect using an intermediate image (see Chet Haases' in Figure 2 to the jaggy one above:
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.
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:
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:
drawBorderGlow()
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) {
//(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.
paintBorderGlow()
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:
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.
DropShadowBorder
DropShadowPanel
For More Information
You can get more information about Java 2D and the Java Platform
from the following sources: