Beginning graphics: an introduction to Java2D
|
import javax.swing.*;
import java.awt.*;
public class DrawRectangle extends JPanel {
public void paintComponent(Graphics g) {
super.paintComponent(g); // always include this
g.drawLine(100, 100, 100, 200);
g.drawLine(100, 200, 300, 200);
g.drawLine(300, 200, 300, 100);
g.drawLine(300, 100, 100, 100);
}
public static void main(String[] args) {
DrawRectangle drawRectangle = new DrawRectangle();
JFrame frame = new JFrame("Draw Rectangle");
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.getContentPane().add(drawRectangle);
frame.setSize(400, 300);
frame.setVisible(true);
}
}
| |
|
- Two objects can be seen: a JFrame that
holds the picture and a JPanel on which the picture is drawn.
-
When the JFrame is made visible, the paintComponent
of any component contained within it is invoked.
DrawRectangle's method paintComponent overrides
that of its superclass JPanel to give the drawing
instructions.
- The Graphics object passed to the paintComponent
method is a rendering engine, an object that, in this case,
translates drawing instructions into the actual picture we see.
- Always include super.paintComponent(Graphics g) as the
first line when overriding the paintComponent method of a
component. Without that instruction, some very weird behavior can
result.
- This example uses Java's Swing components.
| |
| |
The default coordinate system |
|
|
The units are given in pixels.
This is a left-handed coordinate system.
| |
|
- Java2D provides a more powerful rendering engine,
Graphics2D, a subclass of Graphics.
-
Several new classes are provided in java.awt.geom: points,
lines, ellipses and others.
-
Coordinates may be specified as either floats or
doubles.
-
Line2D and Ellipse2D are subclasses (actually,
implementations) of Shape. A Shape can be either
filled or drawn.
-
A Point2D is not a Shape but instead gives
a convenient way to pass coordinates around to various methods.
-
Colors can be chosen from among the class fields of Color or
by specifying the red, green and blue values as ints between
0 and 255 (or as floats between 0 and 1).
-
Generally speaking, Java paint is opaque so the order in which
elements are drawn is important when they overlap.
-
Only include drawing commands in the paintComponent method.
| |
| |
Some more graphics primitives |
|
|
Quadratic curves
A quadratic curve has the form
where
| |
|
Quadratic curves may be drawn elegantly by a subdivision process
The curve is broken into two halves, each of which is itself a
quadratic curve whose control points are obtained by averaging the
original control points.
| |
|
Quadratic curves may be instantiated using either
QuadCurve2D.Double quad = new QuadCurve2D.Double(double x0, double y0,
double x1, double y1, double x2, double y2);
or
QuadCurve2D.Double quad = new QuadCurve2D.Double();
quad.setCurve(Point2D p0, Point2D p1, Point2D p2);
| |
|
A cubic curve has the form
where
| |
|
Cubic curves may be instantiated using either
CubicCurve2D.Double quad = new CubicCurve2D.Double(double x0, double y0,
double x1, double y1, double x2, double y2, double x3, double y3);
or
CubicCurve2D.Double quad = new CubicCurve2D.Double();
quad.setCurve(Point2D p0, Point2D p1, Point2D p2, Point2D p3);
| |
|
Rectangular shapes, including rectangles and ellipses, are
generally defined by stating the upper left corner of the smallest
rectangle containing the shape along with its width and height.
Rectangle2D.Double rectangle = new Rectangle2D.Double(double ulx, double uly,
double width, double height);
Ellipse2D.Double ellipse = new Ellipse2D.Double(double ulx, double uly,
double width, double height);
| |
|
Arc2D.Double arc = new Arc2D.Double(double ulx, double uly, double width,
double height, double angleStart, double angleEnd, int closure);
-
The angles are measured in degrees. There are methods
Math.toRadians and Math.toDegrees for conversions.
-
The angles are given with respect to a parametrization of the ellipse:
-
Remember that Java gives us a left-handed coordinate system.
-
The constant closure has one of the values
Arc2D.OPEN, Arc2D.PIE or Arc2D.CHORD
| |
|
The class GeneralPath lets us build paths like we might in
PostScript.
GeneralPath path = new GeneralPath();
path.moveTo(50, 50);
path.lineTo(150, 150);
path.quadTo(200, 200, 250, 150);
path.curveTo(250, 250, 150, 250, 150, 200);
path.closePath();
g.draw(path);
|
|
| |
|
Constructive area geometry operations area available through the
Area class.
Area area = new Area(new Rectangle(50, 25, 100, 125));
Area addArea = new Area(new Ellipse2D.Float(100, 100, 75, 75));
Area intersectArea = new Area(new Ellipse2D.Float(50, 50, 125, 125));
area.add(addArea);
area.intersect(intersectArea);
g.setPaint(Color.blue);
g.fill(area);
g.setPaint(Color.black);
g.setStroke(new BasicStroke(4f, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER, 3f));
g.draw(area);
| |
|
Any Shape object can produce a PathIterator, a
description of the outline of the Shape in terms of line
segments and quadratic and cubic curves.
Here is how Java2D draws a circle:
PathIteratedCircle.java
| |
| |
Graphic2D's internal state |
|
|
A Graphics2D object uses some adjustable parameters when
rendering a Shape. These include
- Paint: at this stage, think of this as the current
color.
- Font
- Transform: converts the coordinates used in defining
Shapes into pixels
- Stroke: controls the thickness and other attributes of
curves drawn
- Clipping shape: restricts the visible region in which
Shape's are rendered
- Compositing rule: controls how colors are blended when
placed on top of one another
- Rendering hints: allows us to fine tune some parameters
to favor either performance or quality
|
|
|
Font font = new Font("sanserif", Font.BOLD, 80);
g.setFont(font);
g.drawString("graphics", 50, 100);
| |
|
A FontMetrics object allows us to measure the size of a
String.
FontMetrics fm = g.getFontMetrics();
Rectangle2D rect = fm.getStringBounds("graphics", g);
This produces the smallest rectangle containing the String
were it to be drawn at the point (0, 0).
| |
|
Affine transforms of the plane have the form
A Graphics2D maintains an internal affine transform to
convert the coordinates of a Shape into pixels on the
computer screen using an AffineTransform object.
A
Shape is defined in a coordinate system called user
space. Pixels live in a coordinate system called device
space.
The AffineTransform is a map from user space to
device space.
| |
| |
Creating AffineTransforms |
|
|
AffineTransforms are ubiquitous in Java2D and there are
consequently many ways to instantiate them.
AffineTransform transform = new AffineTransform();
gives an object representing the identity transform. Instructions
such as
transform.translate(100, 200);
transform.scale(2, -1);
transform.shear(1, 0);
modify transform by composing it, on the right, with the
indicated transform.
| |
|
There are two ways to use AffineTransforms. First, one
may modify a Graphics2D's internal transform. Here is an
example along with the result:
AffineTransform at = new AffineTransform();
at.translate(150, 200);
at.rotate(-Math.PI/3);
at.scale(2, 0.5);
g.transform(at);
Ellipse2D.Float circle =
new Ellipse2D.Float(-50, -50, 100, 100);
g.draw(circle);
|
 |
| |
|
Alternatively, we may transform shapes.
AffineTransform at = new AffineTransform();
at.translate(150, 200);
at.rotate(-Math.PI/3);
at.scale(2, 0.5);
Ellipse2D.Float circle =
new Ellipse2D.Float(-50, -50, 100, 100);
Shape shape = at.createTransformedShape(circle);
g.draw(shape);
|
 |
| |
| |
Reasons to transform Shapes |
|
|
I generally transform Shapes rather than alter the
Graphics2D's underlying AffineTransform.
This is to avoid the problem we've just seen with non-uniform
scaling.
Also, the thickness of lines and the size of points drawn are most
conveniently expressed in terms of pixels.
Finally, when we work with events, some important information will
be given to us in pixels.
| |
|
The inverse of an AffineTransform may be found like this:
AffineTransform inverse;
try {
inverse = transform.createInverse();
} catch(NoninvertibleTransformException ex) {
do something here if transform is not invertible
}
| |
|
Let's look at an example. In a 301 by 301 pixel frame, we will
draw the graph of the function
y = x3 - x
in the viewing rectangle
along with a 1 by 1 grid.
CubicGraph.java
| |
|
It would be most convenient if user space agreed with our
mathematical coordinate system. We will therefore set up an
AffineTransform that converts mathematical coordinates into
the coordinates of the JPanel.
AffineTransform transform = new AffineTransform();
transform.translate(150, 150);
transform.scale(1, -1);
transform.scale(75, 50);
The origin of the coordinate system is moved to the center of the
JPanel, the vertical coordinate is scaled so that it
increases as we move upward and finally, both coordinates are scaled
so that the viewing rectangle will fill up the JPanel.
| |
|
g.setPaint(Color.lightGray);
GeneralPath path = new GeneralPath();
for (int i = -2; i <= 2; i++) {
path.moveTo(i, -3);
path.lineTo(i, 3);
}
for (int i = -3; i <= 3; i++) {
path.moveTo(-2, i);
path.lineTo(2, i);
}
g.draw(transform.createTransformedShape(path));
| |
|
int steps = 50;
float dx = 4.0f/steps;
g.setPaint(Color.black);
path = new GeneralPath();
path.moveTo(-2, valueAt(-2));
for (float x = -2+dx; x <= 2; x += dx)
path.lineTo(x, valueAt(x));
g.draw(transform.createTransformedShape(path));
Here the method valueAt(double x) returns the value of
the function evaluated at x.
| |
|
Here are some examples illustrating how a Graphics2D's
stroke attribute may be modified.
BasicStroke stroke = new BasicStroke(50);
g.setStroke(stroke);
g.draw(new Line2D.Float(50, 50, 250, 150));
|
|
This example explains why a non-uniform scaling
of a Graphics2D's AffineTransform produces a curve
of varying thickness: the outline of the stroked curve is
transformed.
| |
|
BasicStroke stroke = new BasicStroke(25f, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER);
|
|
| |
|
BasicStroke stroke = new BasicStroke(25f, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_MITER);
|
|
| |
|
BasicStroke stroke = new BasicStroke(25f, BasicStroke.CAP_SQUARE,
BasicStroke.JOIN_MITER);
|
|
| |
|
BasicStroke stroke = new BasicStroke(5f, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER, 1f,
new float[] {10, 5, 5, 5}, 2);
|
|
| |
|
A clipping Shape may be given so that anything outside
the Shape is not visible.
GeneralPath path = new GeneralPath();
for (int i = 0; i <= 300; i+=20) {
path.moveTo(i, 0);
path.lineTo(i, 300);
path.moveTo(0, i);
path.lineTo(300, i);
}
Ellipse2D.Double circle = new Ellipse2D.Double(-1, -1, 2, 2);
AffineTransform transform = new AffineTransform();
transform.translate(150, 150);
transform.rotate(Math.PI/6);
transform.scale(75, -100);
Shape clippingShape = transform.createTransformedShape(circle);
Shape originalClippingShape = g.getClip();
g.setClip(clippingShape);
g.setPaint(Color.lightGray);
g.draw(path);
g.setClip(originalClippingShape);
g.setPaint(Color.black);
g.draw(clippingShape);
| |
|
Earlier, we said that Java paint is opaque. However, this
behavior can be modified through compositing rules, a sample
of which is demonstrated here. The set of Porter-Duff compositing
rules is implemented by java.awt.AlphaComposite.
Rectangle2D.Double rectangle = new Rectangle2D.Double(50, 50, 150, 100);
g.setPaint(Color.blue); g.fill(rectangle);
g.setPaint(Color.black); g.draw(rectangle);
Composite composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.8f);
g.setComposite(composite);
rectangle = new Rectangle2D.Double(90, 75, 125, 150);
g.setPaint(Color.red); g.fill(rectangle);
g.setPaint(Color.black); g.draw(rectangle);
| |
|
Alternatively, we can define transparent colors by specifying an
alpha value.
Rectangle2D.Double rectangle = new Rectangle2D.Double(50, 50, 150, 100);
g.setPaint(new Color(0f, 0f, 1f)); g.fill(rectangle);
g.setPaint(Color.black); g.draw(rectangle);
rectangle = new Rectangle2D.Double(90, 75, 125, 150);
g.setPaint(new Color(1f, 0f, 0f, 0.8f)); g.fill(rectangle);
g.setPaint(Color.black); g.draw(rectangle);
| |
|
Rendering hints help us control some of the details in the
rendering process. Generally speaking, these involve a compromise
between quality and speed.
For us, one of the most useful is anti-aliasing. In a
black and white image, anti-aliasing allows some pixels to be colored
in shades of gray to capture the fact that, say, a line passes
nearby. This produces images that appear smoother.
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
Compare the following images: on the left, anti-aliasing is turned
off while on the right, it is turned on.
| |
|
By default, the endpoints of line segments are adjusted to the
center of the nearest pixel. This can produce some undesirable effects
in mathematical illustrations.
To defeat this behavior, use
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_PURE);
as shown on the right below.
| |