Fun with Java2D - Strokes

Introduction

When you're drawing shapes with Java2D, wouldn't it be nice to spice them up a bit? Java2D gives you dashed lines, different line joins and caps and line widths, but what if you want to draw a railway line or a county boundary on a map? How about double lines? How about drawing text along a path? This article explains how to use the Java2D Stroke interface to do all this and more.

License

The downloadable source code on this page is released under the Apache License. Basically, this means that you are free to do whatever you like with this code, but it's not my fault if your satellite/nuclear power station/missile system fails as a result. Have fun!

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this code except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Strokes

Java comes with just one sort of line shape - the BasicStroke class. BasicStroke draws wide lines with round or square ends and round, bevelled or mitered joins. However, Java allows you to write your own stroke implementations which lets you have any sort of line you can think of.


A 10-point wide BasicStroke

The thing which does the work when drawing lines is the Stroke interface. This has exactly one interesting method:

	public Shape createStrokedShape( Shape shape );

All the method does is return a Shape which describes the outline of the stroke. When you call Graphics2D.draw(), all it does is call the current Stroke's getStrokedShape() method and then fills the result. Thus the code:

	g.draw( shape );

is more-or-less equivalent to:

	g.fill( g.getStroke().getStrokedShape( shape ) );

Note: At the time of writing, Mac OS X Java produces an error message if you try to use a custom stroke, even though it actually works correctly. The workround is to use the code above instead of calling the draw() method.

A Simple Stroke: CompositeStroke

Enough of the theory: let's get down to writing a Stroke of our own. You might think that we'll need lots of geometry for this - and mostly you'd be right - but we can do some simpler things. One interesting thing we can do is take an existing stroke and draw the outline of the stroke using another stroke. A picture is worth a thousand words here:


A 10-point BasicStroke stroked with a .5-point BasicStroke

To use this, we just call:

	g.setStroke( new CompositeStroke( new BasicStroke( 10f ), new BasicStroke( 0.5f ) ) );

Here's the source. As you can see it's simplicity itself, but it's also very powerful.

	public class CompositeStroke implements Stroke {
		private Stroke stroke1, stroke2;

		public CompositeStroke( Stroke stroke1, Stroke stroke2 ) {
			this.stroke1 = stroke1;
			this.stroke2 = stroke2;
		}

		public Shape createStrokedShape( Shape shape ) {
			return stroke2.createStrokedShape( stroke1.createStrokedShape( shape ) );
		}
	}

Drawing Shapes: ShapeStroke

Dashed lines are good, but sometime you need something more. Try drawing one of those lines on a map which consist of lots of crosses or railway lines and you'll see what I mean. Here I'll show a Stroke which will take an array of existing Shapes, which you can think of as the dash shapes, and will draw them along the path to be drawn, like this (note how the points of the stars follow the path):


A ShapeStroke using a star and a circle.

Here's the code to produce the figure. Star is a Shape class in the shape of, er, a star.

	g.setStroke(
		new ShapeStroke(
			new Shape[] {
				new Star( 5, 0, 0, 0, 0.5f, 6.0f),
				new Ellipse2D.Float(0, 0, 4, 4)
			},
			15.0f
		)
	);

Now we're going to have to get into geometry, but it won't be too hard. What we need to do is to walk the path, drawing a shape every so often rotated so that it follows the path. We'll flatten the path first so that we don't have to worry about curves and only have straight line segments to deal with. We'll walk along the path, segment by segment, keeping track of how far we've moved. If it's time to place a shape, we'll do so. The only tricky part is keeping track of segments which are too short to have a shape in, but still contribute their length, and segments which are long enough to require several shapes along their length. Note that when we reach the end of the path, it may not be time to draw a shape which means that the stroke will appear to not reach the end of the path. We can work round this, if desired, by measuring the path before we start and adjusting the spacing so that it fits exactly.

Text Along a Path

Now that we can draw all sorts of lines using shapes, it's worth remembering that characters are just shapes. We can produce a Stroke which draws text along a path by simply modifying ShapeStroke to draw characters and to take account of the different width of each character.


Text along a path

More fun with Strokes: Adding Randomness

Sometimes, Java2D drawing is just too regular. It would be nice to introduce a bit of randomness. WobbleStroke is a stroke which takes an existing stroke and adds random points to the resulting Shape. You can apply it to any Stroke, but here I've shown the effect it has on a BasicStroke.


Making a wobbly BasicStroke with WobbleStroke

Note that simply moving the control points of the path won't have this effect - we need to add new points to the path. This is easily implemented using the same algorithm we used for ShapeStroke, but instead of drawing a shape at each point, we add a new randomized point to the output path.

Zigzags

Suppose you're drawing a sewing pattern and you need to have some of those zigzag stiches in it. You could draw a zigzag line, but what you really want to do is draw a straight line along the stitches and have it come out zigzag on the screen. By now, you may be suspecting that I have a Stroke to do this, and you'd be right - ZigzagStroke will do this. This uses the same technique we've used everywhere before, but instead of drawing discrete shapes at each point, it make sure that the new shapes (the zigs and zags) join together correctly.


Zigzag lines with ZigzagStroke

Of course, we're not limited to zigzag - you could use the same technique to do any sort of repeating pattern, such as wavy lines, square waves or loops.

Compound Strokes

Earlier, we saw CompositeStroke, which used one Stroke to stroke the output of another. Another thing which is useful is to add strokes together. Remember the railway line example? We could use ShapeStroke to draw the railway sleepers, but it won't draw the track line very well. The answer is to draw the track line separately with BasicStroke. We'd like to have a simple Stroke to do this though - one that we can stash away in a library and use directly. CompoundStroke will do this. It takes two strokes and returns the union, interersection, or difference of the two.


Using CompoundStroke to subtract a ShapeStroke from a BasicStroke