Subpages:
1. Java2D API overview
2. Rendering charts
3. Rendering text strings
4. Rendering images
23.3 Rendering strings
In this section we'll demonstrate advantages of using Java2D for rendering strings. This is especially useful for relatively big fonts used to display titles. The following example introduces a custom label component which is capable of rendering strings with various visual effects, including such things as animation using an image, continuously changing foreground color, and outlining.

Figure 23.3 JLabel2Ds with various visual effects, and a plain JLabel for comparison.
<<figure23-3.gif>>
The Code: Labels2D.java
see \Chapter23\2
import java.awt.*;
import java.awt.event.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.*;
import javax.swing.border.*;
public class Labels2D extends JFrame
{
public Labels2D() {
super("2D Labels");
setSize(600, 250);
getContentPane().setLayout(new GridLayout(6, 1, 5, 5));
getContentPane().setBackground(Color.white);
Font bigFont = new Font("Helvetica",Font.BOLD, 24);
JLabel2D lbl = new JLabel2D("Simple JLabel2D With Outline",
JLabel.CENTER);
lbl.setFont(bigFont);
lbl.setForeground(Color.blue);
lbl.setBorder(new LineBorder(Color.black));
lbl.setBackground(Color.cyan);
lbl.setOutlineColor(Color.yellow);
lbl.setStroke(new BasicStroke(5f));
lbl.setOpaque(true);
lbl.setShearFactor(0.3);
getContentPane().add(lbl);
lbl = new JLabel2D("JLabel2D With Color Gradient",
JLabel.CENTER);
lbl.setFont(bigFont);
lbl.setOutlineColor(Color.black);
lbl.setEffectIndex(JLabel2D.EFFECT_GRADIENT);
GradientPaint gp = new GradientPaint(0, 0,
Color.red, 100, 50, Color.blue, true);
lbl.setGradient(gp);
getContentPane().add(lbl);
lbl = new JLabel2D(
"JLabel2D Filled With Image", JLabel.CENTER);
lbl.setFont(bigFont);
lbl.setEffectIndex(JLabel2D.EFFECT_IMAGE);
ImageIcon icon = new ImageIcon("mars.gif");
lbl.setForegroundImage(icon.getImage());
lbl.setOutlineColor(Color.red);
getContentPane().add(lbl);
lbl = new JLabel2D("JLabel2D With Image Animation",
JLabel.CENTER);
lbl.setFont(bigFont);
lbl.setEffectIndex(JLabel2D.EFFECT_IMAGE_ANIMATION);
icon = new ImageIcon("ocean.gif");
lbl.setForegroundImage(icon.getImage());
lbl.setOutlineColor(Color.black);
lbl.startAnimation(300);
getContentPane().add(lbl);
lbl = new JLabel2D("JLabel2D With Color Animation",
JLabel.CENTER);
lbl.setFont(bigFont);
lbl.setEffectIndex(JLabel2D.EFFECT_COLOR_ANIMATION);
lbl.setGradient(gp);
lbl.setOutlineColor(Color.black);
lbl.startAnimation(300);
getContentPane().add(lbl);
JLabel lbl1 = new JLabel("Plain JLabel For Comparison",
JLabel.CENTER);
lbl1.setFont(bigFont);
lbl1.setForeground(Color.black);
getContentPane().add(lbl1);
WindowListener wndCloser = new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
};
addWindowListener(wndCloser);
setVisible(true);
}
public static void main(String argv[]) { new Labels2D(); }
}
class JLabel2D extends JLabel
{
public static final int EFFECT_PLAIN = 0;
public static final int EFFECT_GRADIENT = 1;
public static final int EFFECT_IMAGE = 2;
public static final int EFFECT_IMAGE_ANIMATION = 3;
public static final int EFFECT_COLOR_ANIMATION = 4;
protected int m_effectIndex = EFFECT_PLAIN;
protected double m_shearFactor = 0.0;
protected Color m_outlineColor;
protected Stroke m_stroke;
protected GradientPaint m_gradient;
protected Image m_foregroundImage;
protected Thread m_animator;
protected boolean m_isRunning = false;
protected int m_delay;
protected int m_xShift;
public JLabel2D() { super(); }
public JLabel2D(String text) { super(text); }
public JLabel2D(String text, int alignment) {
super(text, alignment);
}
public void setEffectIndex(int effectIndex) {
m_effectIndex = effectIndex;
repaint();
}
public int getEffectIndex() { return m_effectIndex; }
public void setShearFactor(double shearFactor) {
m_shearFactor = shearFactor;
repaint();
}
public double getShearFactor() { return m_shearFactor; }
public void setOutlineColor(Color outline) {
m_outlineColor = outline;
repaint();
}
public Color getOutlineColor() { return m_outlineColor; }
public void setStroke(Stroke stroke) {
m_stroke = stroke;
repaint();
}
public Stroke getStroke() { return m_stroke; }
public void setGradient(GradientPaint gradient) {
m_gradient = gradient;
repaint();
}
public GradientPaint getGradient() { return m_gradient; }
public void setForegroundImage(Image img) {
m_foregroundImage = img;
repaint();
}
public Image getForegroundImage() { return m_foregroundImage; }
public void startAnimation(int delay) {
if (m_animator != null)
return;
m_delay = delay;
m_xShift = 0;
m_isRunning = true;
m_animator = new Thread() {
double arg = 0;
public void run() {
while(m_isRunning) {
if (m_effectIndex==EFFECT_IMAGE_ANIMATION)
m_xShift += 10;
else if (
m_effectIndex==EFFECT_COLOR_ANIMATION &&
m_gradient != null) {
arg += Math.PI/10;
double cos = Math.cos(arg);
double f1 = (1+cos)/2;
double f2 = (1-cos)/2;
arg = arg % (Math.PI*2);
Color c1 = m_gradient.getColor1();
Color c2 = m_gradient.getColor2();
int r = (int)(c1.getRed()*f1+c2.getRed()*f2);
r = Math.min(Math.max(r, 0), 255);
int g = (int)(c1.getGreen()*f1+c2.getGreen()*f2);
g = Math.min(Math.max(g, 0), 255);
int b = (int)(c1.getBlue()*f1+c2.getBlue()*f2);
b = Math.min(Math.max(b, 0), 255);
setForeground(new Color(r, g, b));
}
repaint();
try { sleep(m_delay); }
catch (InterruptedException ex) { break; }
}
}
};
m_animator.start();
}
public void stopAnimation() {
m_isRunning = false;
m_animator = null;
}
public void paintComponent(Graphics g) {
Dimension d= getSize();
Insets ins = getInsets();
int x = ins.left;
int y = ins.top;
int w = d.width-ins.left-ins.right;
int h = d.height-ins.top-ins.bottom;
if (isOpaque()) {
g.setColor(getBackground());
g.fillRect(0, 0, d.width, d.height);
}
paintBorder(g);
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
FontRenderContext frc = g2.getFontRenderContext();
TextLayout tl = new TextLayout(getText(), getFont(), frc);
AffineTransform shear = AffineTransform.
getShearInstance(m_shearFactor, 0.0);
Shape src = tl.getOutline(shear);
Rectangle rText = src.getBounds();
float xText = x - rText.x;
switch (getHorizontalAlignment()) {
case CENTER:
xText = x + (w-rText.width)/2;
break;
case RIGHT:
xText = x + (w-rText.width);
break;
}
float yText = y + h/2 + tl.getAscent()/4;
AffineTransform shift = AffineTransform.
getTranslateInstance(xText, yText);
Shape shp = shift.createTransformedShape(src);
if (m_outlineColor != null) {
g2.setColor(m_outlineColor);
if (m_stroke != null)
g2.setStroke(m_stroke);
g2.draw(shp);
}
switch (m_effectIndex) {
case EFFECT_GRADIENT:
if (m_gradient == null)
break;
g2.setPaint(m_gradient);
g2.fill(shp);
break;
case EFFECT_IMAGE:
fillByImage(g2, shp, 0);
break;
case EFFECT_COLOR_ANIMATION:
g2.setColor(getForeground());
g2.fill(shp);
break;
case EFFECT_IMAGE_ANIMATION:
if (m_foregroundImage == null)
break;
int wImg = m_foregroundImage.getWidth(this);
if (m_xShift > wImg)
m_xShift = 0;
fillByImage(g2, shp, m_xShift-wImg);
break;
default:
g2.setColor(getForeground());
g2.fill(shp);
break;
}
}
// Method fillByImage taken from JChart2D (section 23.2)
}
Understanding the Code
Class Labels2D
This class extends JFrame and provides the main container for our example. Its constructor creates five instances of our JLabel2D custom component, and one JLabel used for comparison. All labels are placed in a GridLayout containing one column, and each uses the same font. Various settings are used to render these custom labels, as defined in our JLabel2D class:
The first label uses border and background settings to demonstrate that they can be used the same way as with any other Swing components. (Note that we still have to set the opaque property to true for the background to be filled.) The outlineColor and stroke properties are set to outline the label's text. Finally the shearFactor property is set to make the text lean to the left.
The second label uses a GradientPaint instance (using red and blue colors) to fill the text's interior.
The third label uses an image to fill the text's interior.
The fourth label uses an image to fill the text's interior with animation of that image (cyclical shifting of the image in the horizontal direction).
The fifth label uses the same GradientPaint as the second one, but to produce an effect of color animation (a solid foreground color which is changed cyclically).
The sixth label is a plain JLabel component without any special effects.
Class JLabel2D
This class extends JLabel and provides various visual effects in text rendering. Constants are defined representing each available type of visual effect:
int EFFECT_PLAIN: no special effects used.
int EFFECT_GRADIENT: use gradient painting of the text's foreground.
int EFFECT_IMAGE: use an image to fill the text's foreground.
int EFFECT_IMAGE_ANIMATION: use a moving image to fill the text's foreground.
int EFFECT_COLOR_ANIMATION: use a cyclically changing color to fill the text's foreground.
Several instance variables are needed to hold data used by this class:
int m_effectIndex: type of the current effect used to render the text (defaults to EFFECT_PLAIN).
double m_shearFactor: shearing factor determining how the text will lean (positive: lean to the left; negative: lean to the right).
Color m_outlineColor: color used to outline the text.
Stroke m_stroke: stroke instance used to outline the text.
GradientPaint m_gradient: color gradient used to fill the text (this only takes effect when m_effectIndex is set to EFFECT_GRADIENT).
Image m_foregroundImage: image used to fill the text (this only takes effect when m_effectIndex is set to EFFECT_IMAGE).
Thread m_animator: thread which produces animation and color cycling.
boolean m_isRunning: true if the m_animator thread is running, false otherwise.
int m_delay: delay time in ms which determines the speed of animation.
int m_xShift: the current image offset in the horizontal direction (this only takes effect when m_effectIndex is set to EFFECT_IMAGE_ANIMATION).
There are three JLabel2D constructors, and each calls the corresponding superclass (JLabel) constructor. This class also defines several self-explanatory set/get accessors for our instance variables listed above.
The startAnimation() method starts our m_animator thread which executes every m_delay ms. In the case of image animation, this thread periodically increases our m_xShift variable which is used in the rendering routine below as an offset. Depending on how smooth we want our animation we can increase or decrease this shift value. Increasing it would give the appearance of speeding up the animation but would make it jumpier. Decreasing it would slow it down but it would appear much smoother.
In the case of color animation, the startAnimation() method determines the two colors of the current m_gradient, and calculates an intermediate color by 'drawing' a cosine curve between the red, green, and blue components of these colors. Local variable arg is incremented by Math.PI/10 each iteration. We take the cosine of this value and calculate two new values, f1 and f2, based on this result:
arg += Math.PI/10;
double cos = Math.cos(arg);
double f1 = (1+cos)/2;
double f2 = (1-cos)/2;
arg = arg % (Math.PI*2);
f1 and f2 will always sum to 1, and because arg is incremented by Math.PI/10, we will obtain a consistent cycle of 20 different combinations of f1 and f2 as shown below.
arg |
f1 |
f2 |
0.3141592653589793 |
0.9755282581475768 |
0.024471741852423234 |
0.6283185307179586 |
0.9045084971874737 |
0.09549150281252627 |
0.9424777960769379 |
0.7938926261462366 |
0.20610737385376343 |
1.2566370614359172 |
0.6545084971874737 |
0.3454915028125263 |
1.5707963267948966 |
0.5 |
0.49999999999999994 |
1.8849555921538759 |
0.34549150281252633 |
0.6545084971874737 |
2.199114857512855 |
0.20610737385376346 |
0.7938926261462365 |
2.5132741228718345 |
0.09549150281252632 |
0.9045084971874737 |
2.827433388230814 |
0.02447174185242323 |
0.9755282581475768 |
3.141592653589793 |
0.0 |
1.0 |
3.4557519189487724 |
0.024471741852423193 |
0.9755282581475768 |
3.7699111843077517 |
0.09549150281252625 |
0.9045084971874737 |
4.084070449666731 |
0.20610737385376338 |
0.7938926261462367 |
4.39822971502571 |
0.3454915028125262 |
0.6545084971874737 |
4.71238898038469 |
0.4999999999999999 |
0.5000000000000001 |
5.026548245743669 |
0.6545084971874736 |
0.3454915028125264 |
5.340707511102648 |
0.7938926261462365 |
0.20610737385376354 |
5.654866776461628 |
0.9045084971874736 |
0.09549150281252633 |
5.969026041820607 |
0.9755282581475767 |
0.024471741852423234 |
0.0 |
1.0 |
0.0 |
We then use f1 and f2 as factors for determining how much of the red, green, and blue component of each of the gradient's colors to use for the foreground image:
Color c1 = m_gradient.getColor1();
Color c2 = m_gradient.getColor2();
int r = (int)(c1.getRed()*f1+c2.getRed()*f2);
r = Math.min(Math.max(r, 0), 255);
int g = (int)(c1.getGreen()*f1+c2.getGreen()*f2);
g = Math.min(Math.max(g, 0), 255);
int b = (int)(c1.getBlue()*f1+c2.getBlue()*f2);
b = Math.min(Math.max(b, 0), 255);
setForeground(new Color(r, g, b));
This gives us 20 distinct colors. We can always increase or decrease this count by increasing or decreasing the denominator of arg respectively (e.g. arg = Math.PI/20 will give 40 distinct colors, and arg = Math.PI/5 will give 10 distinct colors).
The paintComponent() method first calculates the coordinates of the area available for rendering. Since we avoid the call to super.paintComponent() we are responsible for filling the background and border ourselves. So we first check the opaque property (inherited from JLabel) and, if it is set to true, the background is painted manually by filling the component's available region with its background color. We then call the paintBorder() method to render the border (if any) around the component.
Then the Graphics parameter is cast to a Graphics2D instance to obtain access to Java2D features. As we discussed above, two rendering hints are specified using the setRenderingHint() method: anti-aliasing and the priority of quality over speed (see 23.1.10).
A FontRenderContext instance is retrieved from the Graphics2D context and used to create a TextLayout instance using our label's text (refer back to 23.1.13 and 23.1.14). TextLayout's getOutline() method retrieves a Shape which outlines the given text. This method takes an AffineTransform instance as an optional parameter. We use this parameter to shear the text (if our m_shearFactor property is set to a non-zero value).
We then calculate the location of the text depending on the horizontal alignment of our label (center, or right; left needs no special handling). The y-coordinate of the text's baseline is calculated using the top margin, y, the height over 2, h/2, and the ascent over 4, tl.getAscent()/4 (see Figure 23.4). Why ascent/4? See chapter 2, section 2.8.3 for an explanation.
float yText = y + h/2 + tl.getAscent()/4;

Figure 23.4 Vertical positioning of the label text.
<<figure23-4.gif>>
We then create an AffineTransform for translation to the calculated position. The createTransformedShape() method is then used to get a Shape representing the outline of the text at that position.
If the m_outlineColor property is set, we draw the text's outline using that color and the assigned stroke instance, m_stroke (if set). The rest of the code fills the interior of the text's Shape we retrieved above using the specified visual effect:
For EFFECT_GRADIENT we use a GradientPaint instance to fill the interior.
For EFFECT_IMAGE we use our fillByImage() method to fill the interior with the specified image.
For EFFECT_COLOR_ANIMATION we simply fill the interior with the foreground color (which changes cyclically in the animation thread).
For EFFECT_IMAGE_ANIMATION we use our fillByImage() method with the shift parameter (which is periodically incremented by the animation thread).
For EFFECT_PLAIN we simply fill the interior with the foreground color.
Running the Code
Figure 23.3 shows our JLabel2D demo application in action. You can modify the settings of each label specified in the Labels2D class to try out different combinations of available visual effects. Try out different animation speeds and note how it affects the performance of the application. Choosing too small of a delay may virtually freeze the application. We might consider enforcing a lower bound to ensure that this does not happen.
Note that our JLabel2D component can be easily plugged into any Swing application and used by developers without specific knowledge of Java2D.



RSS feed Java FAQ News