About Me

My photo
I'm a colonist who has declared war on machines and intend to conquer them some day. You'll often find me deep in the trenches fighting off bugs and ugly defects in code. When I'm not tappity-tapping at my WMD (also, known as keyboard), you'll find me chatting with friends, reading comics or playing a PC game.

Tuesday, November 24, 2009

LWUIT and WPF Gradients Mash-up #2

"On the last episode of Angelo's Stuff..."
We saw how to create a multi-coloured linear gradient. We also learnt the concept and purpose of gradient stops. This time around, we're going to dive into radial gradients.

What's so hard about radial gradients?
First off, there are a couple of things that radically differentiate linear and radial gradients.
  1. Linear radients are memory efficient. Only a small strip of an image needs to be made and that same image is tiled either horizontally or vertically across the area to be painted. Radial gradients are not tegular. We need an image that is as large as the area to be painted. A lot of memory is going to be used for a fancy effect. The good news is that the memory will be used up at run-time. So, the downloadable application isn't bloated.
  2. Radial gradients have a lot of customization options. Since we're using up a large image, we might as well get our memory's worth.
Let's paint the town red!

The GradientStop class.
See previous post.

The RadialGradient class.
public class RadialGradient
{
private Vector vGradientStops;
private int radiusX, radiusY;
private int marginTop, marginLeft;

public final static int FILL = 0xffff;
public final static int CENTER = 0xffff;

public RadialGradient( Vector gradientStops,
int radiusX, int radiusY,
int marginLeft, int marginTop )
{
vGradientStops = new Vector();
if(gradientStops != null)
{
//Ensure the gradient stops are ordered by offset in the member Vector.
for(int i = 0; i < gradientStops.size(); ++i)
{
GradientStop newGradientStop =
(GradientStop) gradientStops.elementAt(i);
int insertPos = 0;
for(insertPos = 0; insertPos < vGradientStops.size(); ++insertPos)
{
GradientStop existingGradientStop =
(GradientStop) vGradientStops.elementAt(insertPos);
if(newGradientStop.getOffset() < existingGradientStop.getOffset())
{
break;
}
}

vGradientStops.insertElementAt(newGradientStop, insertPos);
}
}

this.radiusX = radiusX;
this.radiusY = radiusY;
this.marginTop = marginTop;
this.marginLeft = marginLeft;
}

//Getters.
public Vector getGradientStops()
{ return vGradientStops; }
public int getRadiusX()
{ return radiusX; }
public int getRadiusY()
{ return radiusY; }
public int getMarginLeft()
{ return marginLeft; }
public int getMarginTop()
{ return marginTop; }
}

The constructor of this class takes the following parameters:
  1. A Vector of GradientStop. This allows us to use multiple colours within a single gradient.
  2. An X radius. This is the width of the radial gradient. Specify RadialGradient.FILL in order to have the radial gradient fill the entire width of the painted area.
  3. A Y radius. This is the height of the radial gradient. Specify RadialGradient.FILL in order to have the radial gradient fill the entire height of the painted area.
  4. A left margin. This is the distance of the center of the gradient from the left-most point of the area to be painted. Specify RadialGradient.CENTER to have the radial gradient show up in the horizontal center of the painted area.
  5. A top margin. This is the distance of the center of the gradient from the top-most point of the area to be painted. Specify RadialGradient.CENTER to have the radial gradient show up in the vertical center of the painted area.
What are these margins for?
The margins are basically used to space the radial gradient in the area to be painted. Assume that we have a radial gradient with an X radius and Y radius of 20 each. Now let's say that we want to paint this radial gradient at the top left of the painted area. Then all we need to do is specify left and top margins of 10 (half the respective radii). The illustration below should clarify things.

The Painter.
We're going to cache an image that is as large as the area to be painted. So, why restrict ourselves to a single radial gradient? Let's call our custom Painter - MultiRadialGradientPainter. The user of our Painter should be able to overlay multiple radial gradients over each other.
public class MultiRadialGradientPainter implements Painter
{
private byte opacity;
private Image cache;
private Vector vRadialGradients;
private int backFillColor;

public MultiRadialGradientPainter( Vector radialGradients,
byte opacity, int backFillColor )
{
vRadialGradients = radialGradients;
this.opacity = opacity;
this.backFillColor = backFillColor;
}

//Special case overload for single Radial Gradients only.
public MultiRadialGradientPainter( RadialGradient radialGradient,
byte opacity, int backFillColor )
{
vRadialGradients = new Vector();
vRadialGradients.addElement(radialGradient);

this.opacity = opacity;
this.backFillColor = backFillColor;
}

public void paint(Graphics g, Rectangle rect)
{
final Dimension d = rect.getSize();
final int x = rect.getX();
final int y = rect.getY();
final int width = d.getWidth();
final int height = d.getHeight();

final int size = Math.max(width, height);
if(cache == null || size != cache.getWidth())
{
cache = Image.createImage(size, size);
Graphics dc = cache.getGraphics();

//Set the backFillColor before drawing the gradients.
if(backFillColor >= 0)
{
dc.setColor(backFillColor);
dc.fillRect(0, 0, width, height);
}

for(int i = 0; i < vRadialGradients.size(); ++i)
{
final RadialGradient radialGradient =
(RadialGradient)vRadialGradients.elementAt(i);
int radiusX = radialGradient.getRadiusX();
int radiusY = radialGradient.getRadiusY();
int marginLeft = radialGradient.getMarginLeft();
int marginTop = radialGradient.getMarginTop();

if(radiusX >= RadialGradient.FILL)
radiusX = size;
if(radiusY >= RadialGradient.FILL)
radiusY = size;

if(marginTop >= RadialGradient.CENTER)
marginTop = height / 2;
if(marginLeft >= RadialGradient.CENTER)
marginLeft = width / 2;

final Vector vGradientStops = radialGradient.getGradientStops();
for(int j = vGradientStops.size() - 2; j > -1; --j)
{
GradientStop thisGradientStop =
((GradientStop)vGradientStops.elementAt(j));
GradientStop nextGradientStop =
((GradientStop)vGradientStops.elementAt(j + 1));

drawRadialGradient(dc,
thisGradientStop.getColor(),
nextGradientStop.getColor(),
(int)(radiusX * (thisGradientStop.getOffset() / 100.0)),
(int)(radiusX * (nextGradientStop.getOffset() / 100.0)),
(int)(radiusY * (thisGradientStop.getOffset() / 100.0)),
(int)(radiusY * (nextGradientStop.getOffset() / 100.0)),
marginLeft, marginTop
);
}
}

if(opacity < 255)
{
cache = cache.modifyAlpha(opacity);
}
}

g.drawImage(cache, x, y);
}

//This routine does the job of drawing a radial gradient.
private void drawRadialGradient(Graphics dc, int srcColor, int destColor,
int startRadiusX, int endRadiusX,
int startRadiusY, int endRadiusY,
int marginLeft, int marginTop)
{
final int srcR = srcColor >> 16;
final int srcG = srcColor >> 8 & 0xff;
final int srcB = srcColor & 0xff;

final int destR = destColor >> 16;
final int destG = destColor >> 8 & 0xff;
final int destB = destColor & 0xff;

final int dx = (endRadiusX - startRadiusX);
final int dy = (endRadiusY - startRadiusY);
final int biggerDiff = (dx > dy) ? dx : dy;

for(int i = biggerDiff; i > 0; --i)
{
final int interpolatedR =
(int)(srcR + ((destR - srcR) * ((float)i / biggerDiff)));
final int interpolatedG =
(int)(srcG + ((destG - srcG) * ((float)i / biggerDiff)));
final int interpolatedB =
(int)(srcB + ((destB - srcB) * ((float)i / biggerDiff)));

final int interpolatedColor =
interpolatedB | (interpolatedG << 8) | (interpolatedR << 16);

dc.setColor(interpolatedColor);

final int currentRadiusX = startRadiusX + (i * dx / biggerDiff);
final int currentRadiusY = startRadiusY + (i * dy / biggerDiff);

dc.fillArc(
marginLeft - (currentRadiusX / 2),
marginTop - (currentRadiusY / 2),
currentRadiusX, currentRadiusY, 0, 360);
}
}
}
The constructor of MultiRadialGradientPainter takes the following arguments:
  1. A Vector of RadialGradient. This allows us to have multiple radial gradients drawn by the painter.
  2. Opacity. A byte value that controls the transparency of the entire painted image.
  3. A back fill color. This colour will be used to fill the remainder of the space in the image where there is no gradient.
How about an example?
We have a simple LWUIT Form with a Label at the top (BorderLayout.NORTH).






















The screenshot on the left shows the Form without the Label. The Form has two radial gradients - one on the top and the other in the center. The screenshot on the right shows the barebones Form with the Label. The Label has a linear gradient.

This is what the code for setting these gradients looks like:
{
Vector vGradientStops = new Vector();
vGradientStops.addElement(new GradientStop(0xffffff, (byte)0));
vGradientStops.addElement(new GradientStop(0x434343, (byte)50));
vGradientStops.addElement(new GradientStop(0xffffff, (byte)60));

theLabel.getStyle().setBgPainter(
new LinearGradientPainter(vGradientStops, (byte)127, false));
}
{
Vector vGradientStops = new Vector();
vGradientStops.addElement(new GradientStop(0x704700, (byte)0));
vGradientStops.addElement(new GradientStop(0x251801, (byte)100));

Vector vRadialGradients = new Vector();
vRadialGradients.addElement(
new RadialGradient(vGradientStops,
RadialGradient.FILL, RadialGradient.FILL,
RadialGradient.CENTER, RadialGradient.CENTER));

Vector vSmallGradientStops = new Vector();
vSmallGradientStops.addElement(new GradientStop(0xffa300, (byte)0));
vSmallGradientStops.addElement(new GradientStop(0xffa300, (byte)50));
vSmallGradientStops.addElement(new GradientStop(0x251801, (byte)100));

vRadialGradients.addElement(
new RadialGradient(vSmallGradientStops,
RadialGradient.FILL, 50,
RadialGradient.CENTER, 0));

theForm.getStyle().setBgPainter(
new MultiRadialGradientPainter(vRadialGradients, (byte)255, 0x251801));

}
Note that the linear gradient is at 50% opacity. When combining the two together, we get the effect as seen below.
Pretty neat, huh?

Is that all?
Well, yes. That's it. A word of caution: Use radial gradients sparingly. Remember that they require memory allocated for an image that is as large as the area to be painted. If we were to start using radial gradients for all our components then we would run into Out Of Memory exceptions. Also, expect the unexpected. If we do run into an OOM exception, we need to gracefully handle it and fall back on a less fancy UI.

Whew! That was a pretty long-winded post but I hope the MultiRadialGradientPainter proves useful to LWUIT designer/developers out there. If you find an issue with the code feel free to inform me and I'll be happy to look into it. Happy coding!

No comments: