The Java 2D API is an enhancement of original Java graphics and provides advanced two-dimensional graphics capabilities for programmers who require detail and complex graphical manipulation. Graphics2D is an extension of the legacy Graphics class of the AWT package and supports more drawing capabilities, such as providing sophisticated control over geometry, coordinates transformations, color management and ease of rendering shapes, texts, and images. The article shall explore its use in drawing some complex graphical elements.
Coordinate Spaces and Rendering Context
Graphics2D works in a device-independent coordinate system, called user space. Every Graphics2D object inherently contains an AffineTransformation object that is responsible for converting user space coordinates to the device-dependent coordinates. Device space coordinate refers to the individual device pixel, whereas user space coordinates refer to the reference point of virtual space coordinates. Mapping between two is crucial in graphics painting. A Graphics2D rendering context is set up to display text, shapes, or images and then we call the rendering methods such as draw or fill. The attributes of rendering context can be the following:
- Stroke:Stroke is defined by the pen style to outline a shape. The pen style can be lines with any point size, dashing pattern, and so forth.
- Fill: Fill style is used to fill shape interiors with solid colors, gradients, or patterns.
- Composite: This style is useful when rendering overlapping objects.
- Transform: Transformation refers to translation, rotation, scaling, or shearing and it is applied during the rendering process from the user space to device space coordinates.
- Clip: Clipping refers to restricted rendering, set only to the viewable area of the Shape.
- Font: Converting text to glyphs.
- Rendering hints: Specifies the preference such as whether antialiasing should be used.
An Example of Complex Graphic Design
Let us consider an example of creating an analog clock. In this example, we shall use JPanel, a Swing component, as our drawing canvas. To draw on the component here, we override the paintComponent() method, which is inherited by the JPanel from JComponent. Following are the crucial steps we follow for our program.
Step 1: Manipulating Coordinate Space
Coordinate origins are by default situated at the top left corner of the component. So, for the convenience of drawing, we’ll translate the coordinate system to the center.
g2d.translate(getWidth() / 2, getHeight() / 2);
Then, we set up the scaling factor for the drawing, so that the clock face is repainted according to the size of the component.
int side = getWidth() > getHeight()
? getHeight() : getWidth();
g2d.scale(side / 250, side / 250);
The scale function defines that the clock face is resized with the size of the component.
Step 2: Drawing the Clock Face
Draw a circle and fill it with white color with a grayed outline. Stroke defines thinness/thickness of the outline.
g2d.setPaint(Color.white); g2d.fill(new Arc2D.Double(-110, -110, 220, 220, 0, 360, Arc2D.CHORD)); g2d.setColor(Color.darkGray); g2d.setStroke(new BasicStroke(4.0f)); g2d.draw(new Arc2D.Double(-110, -110, 220, 220, 0, 360, Arc2D.CHORD));
Once the circles are drawn, we need to draw 60 second points on the circle, each at an angular distance of 6 degrees. Because the rotate function of Java Graphics takes a radian value as a parameter, we convert 6 degree to radians by the formula (π/180 x 6.0).
for (int i = 0; i < 60; i++) { if ((i % 5) != 0) { g2d.setStroke(new BasicStroke(1.0f)); g2d.setColor(Color.darkGray); g2d.drawLine(92, 0, 96, 0); } else { g2d.setColor(new Color(255, 22, 10)); g2d.setStroke(new BasicStroke(2.0f)); g2d.drawLine(88, 0, 96, 0); } g2d.rotate((Math.PI / 180.0) * 6.0); }
Step 3: Drawing Second, Minute, and Hour Hands
Before drawing the clock hands, we need to find out the exact position of second, minute, and hour hands according to the system clock of the computer. This can be found out with the help of the standard mathematical formula of a circle function. Each hand must be repainted with different coordinates calculated upon runtime rotation as follows.
Figure 1: The basic clock face and its mathematical components
In Java above formula can be written as the following. The constant values represent the length of the hands: the hour, second, and minute hands.
int s = Calendar.getInstance().get(Calendar.SECOND); xs = (int) (Math.cos(s * Math.PI / 30 - Math.PI / 2) * 80 + 0); ys = (int) (Math.sin(s * Math.PI / 30 - Math.PI / 2) * 80 + 0); int m = Calendar.getInstance().get(Calendar.MINUTE); xm = (int) (Math.cos(m * Math.PI / 30 - Math.PI / 2) * 75 + 0); ym = (int) (Math.sin(m * Math.PI / 30 - Math.PI / 2) * 75 + 0); int h = Calendar.getInstance().get(Calendar.HOUR_OF_DAY); xh = (int) (Math.cos((h * 30 + m / 2) * Math.PI / 180 - Math.PI/ 2) * 60 + 0); yh = (int) (Math.sin((h * 30 + m / 2) * Math.PI / 180 - Math.PI/ 2) * 60 + 0);
Step 4: Manipulating the Graphic with Rendering Hints
If we do not provide any rendering hints, a graphic in Java has a jagged appearance. There is a collection of keys and associated values provided by the RenderingHints class. The value provides a choice of a list of algorithms that performs rendering and image manipulation services implicitly. They are basically hints, as the name suggests, because implementation may completely ignore or provide an algorithm that closely resembles the request depending upon the system support. As a thumb rule, the following hints are most common in graphic design and there is no harm in including all of them. For more information on rendering hints, refer to javadoc.
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE); g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_TEXT_LCD_CONTRAST, 100); g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
Step 5: Animating the Clock
Animating the hour, minute, and second hands requires rotating respective lines according to the time function. In Java, we achieve this with the help of a thread that repaints the graphic design so far we have created it with the rotational values achieved from the preceding mathematical calculation. So, the skeleton of the run() function of Thread is as follows.
public void run() { while (true) { try { . . . repaint(); Thread.sleep(500); } catch (InterruptedException ie) { ie.printStackTrace(); } } }
Putting It All Together
Now, let’s put it all together into a working program. Type or copy-paste the code and see for yourself.
import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.geom.Arc2D; import java.util.Calendar; import javax.swing.JFrame; import javax.swing.JPanel; public class AnalogClock extends JPanel implements Runnable{ private static final long serialVersionUID = 1L; int xs, ys, xm, ym, xh, yh; Thread localThread=new Thread(this); public AnalogClock(){ this.setDoubleBuffered(true); localThread.start(); } public void paintComponent(Graphics g) { Graphics2D g2d = (Graphics2D) g; g2d.translate(getWidth() / 2, getHeight() / 2); int side = getWidth() > getHeight() ? getHeight() : getWidth(); g2d.scale(side / 250, side / 250); setAllRenderingHints(g2d); drawClockFace(g2d); drawClockHands(g2d); } @Override public void run() { while (true) { try { int s = Calendar.getInstance(). get(Calendar.SECOND); xs = (int) (Math.cos(s * Math.PI / 30 - Math.PI / 2) * 80 + 0); ys = (int) (Math.sin(s * Math.PI / 30 - Math.PI / 2) * 80 + 0); int m = Calendar.getInstance(). get(Calendar.MINUTE); xm = (int) (Math.cos(m * Math.PI / 30 - Math.PI / 2) * 75 + 0); ym = (int) (Math.sin(m * Math.PI / 30 - Math.PI / 2) * 75 + 0); int h = Calendar.getInstance(). get(Calendar.HOUR_OF_DAY); xh = (int) (Math.cos((h * 30 + m / 2) * Math.PI / 180 - Math.PI / 2) * 60 + 0); yh = (int) (Math.sin((h * 30 + m / 2) * Math.PI / 180 - Math.PI / 2) * 60 + 0); repaint(); Thread.sleep(500); } catch (InterruptedException ie) { ie.printStackTrace(); } } } private void setAllRenderingHints(Graphics2D g2d) { g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE); g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_TEXT_LCD_CONTRAST, 100); g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); } private void drawClockFace(Graphics2D g2d) { g2d.setPaint(Color.white); g2d.fill(new Arc2D.Double(-110, -110, 220, 220, 0, 360, Arc2D.CHORD)); g2d.setColor(Color.darkGray); g2d.setStroke(new BasicStroke(4.0f)); g2d.draw(new Arc2D.Double(-110, -110, 220, 220, 0, 360, Arc2D.CHORD)); for (int i = 0; i < 60; i++) { if ((i % 5) != 0) { g2d.setStroke(new BasicStroke(1.0f)); g2d.setColor(Color.darkGray); g2d.drawLine(92, 0, 96, 0); } else { g2d.setColor(new Color(255, 22, 10)); g2d.setStroke(new BasicStroke(2.0f)); g2d.drawLine(88, 0, 96, 0); } g2d.rotate((Math.PI / 180.0) * 6.0); } } private void drawClockHands(Graphics2D g2d) { g2d.setColor(new Color(220, 22, 10)); g2d.setStroke(new BasicStroke(5.0f)); g2d.drawLine(0, 0, xh, yh); g2d.setStroke(new BasicStroke(3.0f)); g2d.drawLine(0, 0, xm, ym); g2d.setColor(Color.black); g2d.setStroke(new BasicStroke(2.0f)); g2d.drawLine(0, 0, xs, ys); g2d.setColor(Color.black); g2d.fillOval(-5, -5, 10, 10); g2d.setColor(Color.white); g2d.fillOval(-2, -2, 4, 4); } public static void main(String[] args) { JFrame f = new JFrame(); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setSize(400, 400); AnalogClock a = new AnalogClock(); f.add(a, BorderLayout.CENTER); f.setVisible(true); } }
Output
Figure 2: Clock face created with Java code (output from previous code example)
Figure 3: Clock face loaded from an image file (not coded in preceding example. Hint: Use BufferedImage)
Figure 4: Another variation of the clock face created with Java code (not coded in previous example)
Conclusion
The preceding example provides a hint of manipulating complex design principles in Java 2D API. Another hint: Try to load a clock face image from a picture file (refer to Figure 2) with the help of BufferedImage rather than designing it from scratch in Java code or modify the hard-coded design according to your creative ideas. Use AffineTransformation to zoom in/out the drawing canvas. Have fun with Java 2D!