[ Team LiB ] Previous Section Next Section

12.12 Custom Shapes

Figure 12-4 showed how Java 2D can be used to draw and fill various types of shapes. One of the shapes shown in that figure is a spiral, which was drawn using the Spiral class, a custom Shape implementation shown in Example 12-15. That example is followed by another custom shape, PolyLine, in Example 12-16. PolyLine represents a series of connect line segments, and is the basis of the ScribblePane class of Example 11-13. PolyLine also features prominently in Chapter 14, where it is used for dragging-and-dropping scribbles.

The Shape interface defines three important methods (some of which have multiple overloaded versions) that all shapes must implement. The contains( ) methods determine whether a shape contains a point or a rectangle; a Shape has to be able to tell its inside from its outside. The intersects( ) methods determine whether any part of the shape intersects a specified rectangle. Since both contains( ) and intersects( ) are difficult to compute exactly for a spiral, the Spiral class approximates the spiral with a circle for the purposes of these methods.

The getPathIterator( ) methods are the heart of any Shape implementation. Each method returns a PathIterator object that describes the outline of the shape in terms of line and curve segments. Java 2D relies on PathIterator objects to draw and fill shapes. The key methods of the SpiralIterator implementation are currentSegment( ), which returns one line segment of the spiral, and next( ), which moves the iterator to the next segment. next( ) uses some hairy mathematics to make sure that the line segment approximation of the spiral is good enough.

Example 12-15. Spiral.java
package je3.graphics;
import java.awt.*;
import java.awt.geom.*;

/** This Shape implementation represents a spiral curve */
public class Spiral implements Shape {
    double centerX, centerY;            // The center of the spiral
    double startRadius, startAngle;     // The spiral starting point
    double endRadius, endAngle;         // The spiral ending point
    double outerRadius;                 // the bigger of the two radii
    int angleDirection;                 // 1 if angle increases, -1 otherwise

    // It's hard to do contains( ) and intersects( ) tests on a spiral, so we
    // do them on this circular approximation of the spiral.  This is not an
    // ideal solution, and is only a good approximation for "tight" spirals.
    Shape approximation;

     * The constructor.  It takes arguments for the center of the shape, the
     * start point, and the end point.  The start and end points are specified
     * in terms of angle and radius.  The spiral curve is formed by varying
     * the angle and radius smoothly between the two end points.
    public Spiral(double centerX, double centerY,
                  double startRadius, double startAngle,
                  double endRadius, double endAngle)
        // Save the parameters that describe the spiral
        this.centerX = centerX;         this.centerY = centerY;
        this.startRadius = startRadius; this.startAngle = startAngle;
        this.endRadius = endRadius;     this.endAngle = endAngle;

        // figure out the maximum radius, and the spiral direction
        this.outerRadius = Math.max(startRadius, endRadius);
        if (startAngle < endAngle) angleDirection = 1;
        else angleDirection = -1;
        if ((startRadius < 0) || (endRadius < 0))
            throw new IllegalArgumentException("Spiral radii must be >= 0");

        // Here's how we approximate the inside of the spiral
        approximation = new Ellipse2D.Double(centerX-outerRadius,
                                             outerRadius*2, outerRadius*2);

     * The bounding box of a Spiral is the same as the bounding box of a
     * circle with the same center and the maximum radius
    public Rectangle getBounds( ) {
        return new Rectangle((int)(centerX-outerRadius),
                             (int)(outerRadius*2), (int)(outerRadius*2));

    /** Same as getBounds( ), but with floating-point coordinates */
    public Rectangle2D getBounds2D( ) {
        return new Rectangle2D.Double(centerX-outerRadius, centerY-outerRadius,
                                      outerRadius*2, outerRadius*2);

     * These methods use a circle approximation to determine whether a point
     * or rectangle is inside the spiral.  We could be more clever than this.
    public boolean contains(Point2D p) { return approximation.contains(p); }
    public boolean contains(Rectangle2D r) { return approximation.contains(r);}
    public boolean contains(double x, double y) {
        return approximation.contains(x,y);
    public boolean contains(double x, double y, double w, double h) {
        return approximation.contains(x, y, w, h);

     * These methods determine whether the specified rectangle intersects the
     * spiral. We use our circle approximation.  The Shape interface explicitly
     * allows approximations to be used for these methods.
    public boolean intersects(double x, double y, double w, double h) {
        return approximation.intersects(x, y, w, h);
    public boolean intersects(Rectangle2D r) {
        return approximation.intersects(r);

     * This method is the heart of all Shape implementations.  It returns a
     * PathIterator that describes the shape in terms of the line and curve
     * segments that comprise it.  Our iterator implementation approximates
     * the shape of the spiral using line segments only.  We pass in a
     * "flatness" argument that tells it how good the approximation must be
     * (smaller numbers mean a better approximation).
    public PathIterator getPathIterator(AffineTransform at) {
        return new SpiralIterator(at, outerRadius/500.0);

     * Return a PathIterator that describes the shape in terms of line
     * segments only, with an approximation quality specified by flatness.
    public PathIterator getPathIterator(AffineTransform at, double flatness) {
        return new SpiralIterator(at, flatness);

     * This inner class is the PathIterator for our Spiral shape.  For
     * simplicity, it does not describe the spiral path in terms of Bezier
     * curve segments, but simply approximates it with line segments.  The
     * flatness property specifies how far the approximation is allowed to
     * deviate from the true curve.
    class SpiralIterator implements PathIterator {
        AffineTransform transform;    // How to transform generated coordinates
        double flatness;              // How close an approximation
        double angle = startAngle;    // Current angle
        double radius = startRadius;  // Current radius
        boolean done = false;         // Are we done yet?

        /** A simple constructor.  Just store the parameters into fields */
        public SpiralIterator(AffineTransform transform, double flatness) {
            this.transform = transform;
            this.flatness = flatness;

         * All PathIterators have a "winding rule" that helps to specify what
         * is the inside of an area and what is the outside.  If you fill a
         * spiral (which you're not supposed to do) the winding rule returned
         * here yields better results than the alternative, WIND_EVEN_ODD
        public int getWindingRule( ) { return WIND_NON_ZERO; }

        /** Returns true if the entire path has been iterated */
        public boolean isDone( ) { return done; }

         * Store the coordinates of the current segment of the path into the
         * specified array, and return the type of the segment.  Use
         * trigonometry to compute the coordinates based on the current angle
         * and radius.  If this was the first point, return a MOVETO segment,
         * otherwise return a LINETO segment. Also, check to see if we're done.
        public int currentSegment(float[  ] coords) {
            // given the radius and the angle, compute the point coords
            coords[0] = (float)(centerX + radius*Math.cos(angle));
            coords[1] = (float)(centerY - radius*Math.sin(angle));

            // If a transform was specified, use it on the coordinates
            if (transform != null) transform.transform(coords, 0, coords, 0,1);

            // If we've reached the end of the spiral, remember that fact
            if (angle == endAngle) done = true;

            // If this is the first point in the spiral, then move to it
            if (angle == startAngle) return SEG_MOVETO;

            // Otherwise draw a line from the previous point to this one
            return SEG_LINETO;

        /** This method is the same as above, except using double values */
        public int currentSegment(double[  ] coords) {
            coords[0] = centerX + radius*Math.cos(angle);
            coords[1] = centerY - radius*Math.sin(angle);
            if (transform != null) transform.transform(coords, 0, coords, 0,1);
            if (angle == endAngle) done = true;
            if (angle == startAngle) return SEG_MOVETO;
            else return SEG_LINETO;

         * Move on to the next segment of the path.  Compute the angle and
         * radius values for the next point in the spiral.
        public void next( ) {
            if (done) return;

            // First, figure out how much to increment the angle.  This
            // depends on the required flatness, and also upon the current
            // radius.  When drawing a circle (which we'll use as our
            // approximation) of radius r, we can maintain a flatness f by
            // using angular increments given by this formula:
            //      a = acos(2*(f/r)*(f/r) - 4*(f/r) + 1)
            // Use this formula to figure out how much we can increment the
            // angle for the next segment.  Note that the formula does not
            // work well for very small radii, so we special case those.
            double x = flatness/radius;
            if (Double.isNaN(x) || (x > .1))
                angle += Math.PI/4*angleDirection; 
            else {
                double y = 2*x*x - 4*x + 1;
                angle += Math.acos(y)*angleDirection;
            // Check whether we've gone past the end of the spiral
            if ((angle-endAngle)*angleDirection > 0) angle = endAngle;

            // Now that we know the new angle, we can use interpolation to
            // figure out what the corresponding radius is.
            double fractionComplete = (angle-startAngle)/(endAngle-startAngle);
            radius = startRadius + (endRadius-startRadius)*fractionComplete;
Example 12-16. PolyLine.java
package je3.graphics;
import java.awt.geom.*;  // PathIterator, AffineTransform, and related
import java.awt.Shape;
import java.awt.Rectangle;
import java.io.Externalizable;

 * This Shape implementation represents a series of connected line segments.
 * It is like a Polygon, but is not closed.  This class is used by the 
 * ScribblePane class of the GUI chapter.  It implements the Cloneable and
 * Externalizable interfaces so it can be used in the Drag-and-Drop examples
 * in the Data Transfer chapter.
public class PolyLine implements Shape, Cloneable, Externalizable {
    float x0, y0;    // The starting point of the polyline.
    float[  ] coords;  // The x and y coordinates of the end point of each line
                     // segment packed into a single array for simplicity:
                     // [x1,y1,x2,y2,...] Note that these are relative to x0,y0
    int numsegs;     // How many line segments in this PolyLine

    // Coordinates of our bounding box, relative to (x0, y0);
    float xmin=0f, xmax=0f, ymin=0f, ymax=0f;  
    // No-arg constructor assumes an origin of (0,0)
    // A no-arg constructor is required for the Externalizable interface
    public PolyLine( ) { this(0f, 0f); }

    // The constructor.
    public PolyLine(float x0, float y0) {
        setOrigin(x0,y0);     // Record the starting point.
        numsegs = 0;          // Note that we have no line segments, so far

    /** Set the origin of the PolyLine.  Useful when moving it */
    public void setOrigin(float x0, float y0) {
        this.x0 = x0; 
        this.y0 = y0;
    /** Add dx and dy to the origin */
    public void translate(float dx, float dy) {
        this.x0 += dx;
        this.y0 += dy;

     * Add a line segment to the PolyLine.  Note that x and y are absolute
     * coordinates, even though the implementation stores them relative to 
     * x0, y0;
    public void addSegment(float x, float y) {
        // Allocate or reallocate the coords[  ] array when necessary
        if (coords == null) coords = new float[32];
        if (numsegs*2 >= coords.length) {
            float[  ] newcoords = new float[coords.length * 2];
            System.arraycopy(coords, 0, newcoords, 0, coords.length);
            coords = newcoords;

        // Convert from absolute to relative coordinates
        x = x - x0;
        y = y - y0;

        // Store the data
        coords[numsegs*2] = x;
        coords[numsegs*2+1] = y;

        // Enlarge the bounding box, if necessary
        if (x > xmax) xmax = x;
        else if (x < xmin) xmin = x;
        if (y > ymax) ymax = y;
        else if (y < ymin) ymin = y;

    /*------------------ The Shape Interface --------------------- */

    // Return floating-point bounding box
    public Rectangle2D getBounds2D( ) {
        return new Rectangle2D.Float(x0 + xmin, y0 + ymin, 
                                     xmax-xmin, ymax-ymin);

    // Return integer bounding box, rounded to outermost pixels.
    public Rectangle getBounds( ) {
        return new Rectangle((int)(x0 + xmin - 0.5f),      // x0
                             (int)(y0 + ymin - 0.5f),      // y0
                             (int)(xmax - xmin + 0.5f),    // width
                             (int)(ymax - ymin + 0.5f));   // height

    // PolyLine shapes are open curves, with no interior.
    // The Shape interface says that open curves should be implicitly closed
    // for the purposes of insideness testing.  For our purposes, however,
    // we define PolyLine shapes to have no interior, and the contains( ) 
    // methods always return false.
    public boolean contains(Point2D p) { return false; }
    public boolean contains(Rectangle2D r) { return false; }
    public boolean contains(double x, double y) { return false; }
    public boolean contains(double x, double y, double w, double h) {
        return false;

    // The intersects methods simply test whether any of the line segments
    // within a polyline intersects the given rectangle.  Strictly speaking,
    // the Shape interface requires us to also check whether the rectangle
    // is entirely contained within the shape as well.  But the contains( )
    // methods for this class always return false.
    // We might improve the efficiency of this method by first checking for
    // intersection with the overall bounding box to rule out cases that
    // aren't even close.
    public boolean intersects(Rectangle2D r) {
        if (numsegs < 1) return false;
        float lastx = x0, lasty = y0;
        for(int i = 0; i < numsegs; i++) {  // loop through the segments
            float x = coords[i*2] + x0;
            float y = coords[i*2+1] + y0;
            // See if this line segment intersects the rectangle
            if (r.intersectsLine(x, y, lastx, lasty)) return true;
            // Otherwise move on to the next segment
            lastx = x;
            lasty = y;
        return false;  // No line segment intersected the rectangle

    // This variant method is just defined in terms of the last.
    public boolean intersects(double x, double y, double w, double h) {
        return intersects(new Rectangle2D.Double(x,y,w,h));

    // This is the key to the Shape interface; it tells Java2D how to draw
    // the shape as a series of lines and curves.  We use only lines
    public PathIterator getPathIterator(final AffineTransform transform) {
        return new PathIterator( ) {
                int curseg = -1; // current segment
                // Copy the current segment for thread-safety, so we don't
                // mess up if a segment is added while we're iterating
                int numsegs = PolyLine.this.numsegs;

                public boolean isDone( ) { return curseg >= numsegs; }

                public void next( ) { curseg++; }

                // Get coordinates and type of current segment as floats
                public int currentSegment(float[  ] data) {
                    int segtype;
                    if (curseg == -1) {       // First time we're called
                        data[0] = x0;         // Data is the origin point
                        data[1] = y0;
                        segtype = SEG_MOVETO; // Returned as a moveto segment
                    else { // Otherwise, the data is a segment endpoint
                        data[0] = x0 + coords[curseg*2];
                        data[1] = y0 + coords[curseg*2 + 1];
                        segtype = SEG_LINETO; // Returned as a lineto segment
                    // If a tranform was specified, transform point in place
                    if (transform != null)
                        transform.transform(data, 0, data, 0, 1);
                    return segtype;

                // Same as last method, but use doubles
                public int currentSegment(double[  ] data) {
                    int segtype;
                    if (curseg == -1) {
                        data[0] = x0;
                        data[1] = y0;
                        segtype = SEG_MOVETO;
                    else {
                        data[0] = x0 + coords[curseg*2];
                        data[1] = y0 + coords[curseg*2 + 1];
                        segtype = SEG_LINETO;
                    if (transform != null)
                        transform.transform(data, 0, data, 0, 1);
                    return segtype;

                // This only matters for closed shapes
                public int getWindingRule( ) { return WIND_NON_ZERO; }

    // PolyLines never contain curves, so we can ignore the flatness limit
    // and implement this method in terms of the one above.
    public PathIterator getPathIterator(AffineTransform at, double flatness) {
        return getPathIterator(at);

    /*------------------ Externalizable --------------------- */

     * The following two methods implement the Externalizable interface.
     * We use Externalizable instead of Seralizable so we have full control
     * over the data format, and only write out the defined coordinates
    public void writeExternal(java.io.ObjectOutput out)
        throws java.io.IOException
        for(int i=0; i < numsegs*2; i++) out.writeFloat(coords[i]);

    public void readExternal(java.io.ObjectInput in)
        throws java.io.IOException, ClassNotFoundException
        this.x0 = in.readFloat( );
        this.y0 = in.readFloat( );
        this.numsegs = in.readInt( );
        this.coords = new float[numsegs*2];
        for(int i=0; i < numsegs*2; i++) coords[i] = in.readFloat( );

    /*------------------ Cloneable --------------------- */

     * Override the Object.clone( ) method so that the array gets cloned, too.
    public Object clone( ) {
        try {
            PolyLine copy = (PolyLine) super.clone( );
            if (coords != null) copy.coords = (float[  ]) this.coords.clone( );
            return copy;
        catch(CloneNotSupportedException e) {
            throw new AssertionError( ); // This should never happen
    [ Team LiB ] Previous Section Next Section