Copyright Derek O'Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.
Stroking is the process of drawing a shape's outline.
Java provides a BasicStroke, which allows you to adjust a line's thickness, end shape and whether it is dashed or not. A BasicStroke object holds information about the line width, join style, end-cap style, and dash style.
BasicStroke(float width, // width of the line int cap, // end of line segment CAP_BUTT, CAP_ROUND or CAP_SQUARE int join, // between line segments JOIN_BEVEL, JOIN_MITER or JOIN_ROUND float miterlimit, // float[] dash // an array showing the size of dashes and gaps float dash_phase) // an index into dash, stateing start of sequence
There are various other, simplier, BasicStroke constructors.
BasicStrokeDemo Example: (Run Applet)
import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Stroke_BasicStroke_Demo extends JApplet { @Override public void init() { this.setContentPane(new View()); } public class View extends JPanel implements ActionListener { private final float THICK_LINE = 30.0f; private final float THIN_LINE = 10.0f; private final float dash[] = { 30.0f, 50.0f }; // interval for dashed lines private int cap = BasicStroke.CAP_SQUARE; private int join = BasicStroke.JOIN_ROUND; private float lineThickness = THICK_LINE; private boolean isDashed = false; // dashed or solid line private final JMenuItem capButt = new JMenuItem("CAP_BUTT"); private final JMenuItem capRound = new JMenuItem("CAP_ROUND"); private final JMenuItem capSquare = new JMenuItem("CAP_SQUARE"); private final JMenuItem joinMiter = new JMenuItem("JOIN_MITER"); private final JMenuItem joinBevel = new JMenuItem("JOIN_BEVEL"); private final JMenuItem joinRound = new JMenuItem("JOIN_ROUND"); private final JCheckBoxMenuItem dashed = new JCheckBoxMenuItem("Dashed", this.isDashed); private final JCheckBoxMenuItem thickLine = new JCheckBoxMenuItem("Thick Lines", true); public View() { super(); final JMenuBar menuBar = new JMenuBar(); final JMenu capMenu = new JMenu("CAP"); final JMenu joinMenu = new JMenu("JOIN"); final JMenu otherMenu = new JMenu("Other"); setJMenuBar(menuBar); menuBar.add(capMenu); menuBar.add(joinMenu); menuBar.add(otherMenu); capMenu.add(capButt); capMenu.add(capRound); capMenu.add(capSquare); joinMenu.add(this.joinMiter); joinMenu.add(this.joinBevel); joinMenu.add(this.joinRound); otherMenu.add(this.dashed); otherMenu.add(this.thickLine); this.capButt.addActionListener(this); this.capRound.addActionListener(this); this.capSquare.addActionListener(this); this.joinBevel.addActionListener(this); this.joinMiter.addActionListener(this); this.joinRound.addActionListener(this); this.dashed.addActionListener(this); this.thickLine.addActionListener(this); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); final Graphics2D g2 = (Graphics2D) g; BasicStroke basicStroke = null; if (this.isDashed) { basicStroke = new BasicStroke(this.lineThickness, this.cap, this.join, 1.0f, dash, 0.0f); } else { basicStroke = new BasicStroke(this.lineThickness, this.cap, this.join); } g2.setStroke(basicStroke); g2.drawLine(30, 30, 150, 30); g2.drawLine(150, 30, 30, 150); g2.drawRect(200, 30, 250, 100); } @Override public void actionPerformed(ActionEvent e) { final Object source = e.getSource(); if (source == this.capButt) { this.cap = BasicStroke.CAP_BUTT; } else if (source == this.capRound) { this.cap = BasicStroke.CAP_ROUND; } else if (source == this.capSquare) { this.cap = BasicStroke.CAP_SQUARE; } else if (source == this.joinBevel) { this.join = BasicStroke.JOIN_BEVEL; } else if (source == this.joinMiter) { this.join = BasicStroke.JOIN_MITER; } else if (source == this.joinRound) { this.join = BasicStroke.JOIN_ROUND; } else if (source == this.thickLine) { if (this.thickLine.isSelected()) { this.lineThickness = THICK_LINE; } else { this.lineThickness = THIN_LINE; } } else if (source == this.dashed) { this.isDashed = this.dashed.isSelected(); } this.repaint(); } } }
Textures can be placed onto strokes, as shown in the example below.
Stroke_Texture_Demo Example: (Run Applet)
import java.awt.*; import java.awt.image.*; import javax.swing.*; public class Stroke_Texture_Demo extends JApplet { @Override public void init() { this.setContentPane(new View()); } public class View extends JPanel { @Override protected void paintComponent(Graphics g) { super.paintComponent(g); final Image image = new ImageIcon(getClass().getClassLoader().getResource("images/smiley.jpg")).getImage(); final Graphics2D g2 = (Graphics2D) g; // texture final int textureWidth = 30; final int textureHeight = 30; final BufferedImage textureImg = new BufferedImage(textureWidth, textureHeight, BufferedImage.TYPE_INT_RGB); final Graphics2D textureG = textureImg.createGraphics(); textureG.drawImage(image, 0, 0, textureWidth, textureHeight, this); final Rectangle rectangle = new Rectangle(0, 0, textureWidth, textureHeight); final TexturePaint texturePaint = new TexturePaint(textureImg, rectangle); g2.setPaint(texturePaint); // stroke final BasicStroke stroke = new BasicStroke(15f); g2.setStroke(stroke); g2.drawArc(30, 50, 250, 100, 180, -360); } } }
Gradients can be placed onto strokes, as shown in the example below.
Stroke_Gradient_Demo Example: (Run Applet)
import java.awt.*; import javax.swing.*; public class Stroke_Gradient_Demo extends JApplet { @Override public void init() { this.setContentPane(new View()); } public class View extends JPanel { @Override protected void paintComponent(Graphics g) { super.paintComponent(g); final Graphics2D g2 = (Graphics2D) g; // gradient final GradientPaint gradient = new GradientPaint(0, 0, Color.yellow, getWidth(), getHeight(), Color.blue); g2.setPaint(gradient); // stroke final BasicStroke stroke = new BasicStroke(15f); g2.setStroke(stroke); g2.drawArc(30, 50, 250, 100, 180, -360); } } }
Java provides a Stroke interface. This comes with one method:
public Shape createStrokedShape(Shape shape)
This method returns a Shape that can be used as a stroke.
Stroke_Composite_Demo Example: (Run Applet)
import java.awt.*; import java.awt.geom.*; import javax.swing.*; public class Stroke_Composite_Demo extends JApplet { @Override public void init() { this.setContentPane(new View()); } public class View extends JPanel { @Override protected void paintComponent(Graphics g) { super.paintComponent(g); final Graphics2D g2 = (Graphics2D) g; final CompositeStroke compositeStroke = new CompositeStroke(new BasicStroke(15f), new BasicStroke(5.0f)); g2.setStroke(compositeStroke); g2.drawArc(30, 50, 250, 100, 180, -180); g2.drawArc(30, 90, 250, 100, 180, 180); } } public class CompositeStroke implements Stroke { private Stroke mainStroke; private Stroke minusStroke; public CompositeStroke(Stroke mainStroke, Stroke minusStroke) { this.mainStroke = mainStroke; this.minusStroke = minusStroke; } @Override public Shape createStrokedShape(Shape shape) { final Area mainArea = new Area(this.mainStroke.createStrokedShape(shape)); final Area minusArea = new Area(this.minusStroke.createStrokedShape(shape)); mainArea.subtract(minusArea); return mainArea; } } }
Stroke_Pattern_Demo Example: (Run Applet)
import java.awt.*; import java.awt.geom.*; import javax.swing.*; public class Stroke_Pattern_Demo extends JApplet { @Override public void init() { this.setContentPane(new View()); } public class View extends JPanel { @Override protected void paintComponent(Graphics g) { super.paintComponent(g); final Graphics2D g2 = (Graphics2D) g; // set up the shapeStroke final Rectangle rectangle = new Rectangle(0, 0, 20, 20); final Ellipse2D ellipse = new Ellipse2D.Float(0, 0, 20, 50); final Shape shape[] = new Shape[] { rectangle, ellipse }; final PatternStroke shapeStroke = new PatternStroke(shape, 15.0f); g2.setStroke(shapeStroke); g2.drawArc(30, 50, 250, 100, 180, -180); g2.drawArc(30, 90, 250, 100, 180, 180); } } public class PatternStroke implements Stroke { private float resolution; // the distance between shapes private boolean repeat = true; private final Shape shapes[]; private final AffineTransform affineTransform = new AffineTransform(); public PatternStroke(Shape shapes[], float resolution) { this.resolution = resolution; // the distance between shapes this.shapes = new Shape[shapes.length]; for (int i = 0; i < this.shapes.length; i++) { final Rectangle2D bounds = shapes[i].getBounds2D(); this.affineTransform.setToTranslation(-bounds.getCenterX(), -bounds.getCenterY()); this.shapes[i] = this.affineTransform.createTransformedShape(shapes[i]); } } @Override public Shape createStrokedShape(Shape shape) { final GeneralPath result = new GeneralPath(); final PathIterator pathIterator = new FlatteningPathIterator(shape.getPathIterator(null), 1.0); float points[] = new float[2]; float moveX = 0; float moveY = 0; float curX = 0; float curY = 0; float nextX = 0; float nextY = 0; float distanceToNext = 0; final int length = this.shapes.length; int segmentType = 0; int currentPos = 0; while ((currentPos < length) && (!pathIterator.isDone())) { segmentType = pathIterator.currentSegment(points); switch (segmentType) { case PathIterator.SEG_MOVETO: // starting location for a new subpath { moveX = nextX = points[0]; moveY = nextY = points[1]; result.moveTo(moveX, moveY); distanceToNext = 0; break; } case PathIterator.SEG_LINETO: // reached end of a subpath { curX = points[0]; curY = points[1]; // each subpath needs to be closed, so do not break here // instead fall into SEG_CLOSE code below } case PathIterator.SEG_CLOSE: // close off the subpath { points[0] = moveX; points[1] = moveY; final float dx = curX - nextX; final float dy = curY - nextY; final float distance = (float) (Math.sqrt((dx * dx) + (dy * dy))); if (distance >= distanceToNext) { final float ratio = 1.0f / distance; final float angle = (float) (Math.atan2(dy, dx)); while ((currentPos < length) && (distance >= distanceToNext)) { final float x = nextX + distanceToNext * dx * ratio; final float y = nextY + distanceToNext * dy * ratio; this.affineTransform.setToTranslation(x, y); this.affineTransform.rotate(angle); result.append(this.affineTransform.createTransformedShape(this.shapes[currentPos]), false); distanceToNext += this.resolution; currentPos++; if (this.repeat) { currentPos %= length; } } } distanceToNext -= distance; nextX = curX; nextY = curY; break; } } pathIterator.next(); } return result; } } }
Stroke_CompositeWithPattern_Demo Example: (Run Applet)
import java.awt.*; import java.awt.geom.*; import javax.swing.*; public class Stroke_CompositeWithPattern_Demo extends JApplet { @Override public void init() { this.setContentPane(new View()); } public class View extends JPanel { @Override protected void paintComponent(Graphics g) { super.paintComponent(g); final Graphics2D g2 = (Graphics2D) g; final Rectangle rectangle = new Rectangle(0, 0, 15, 15); final Ellipse2D ellipse = new Ellipse2D.Float(0, 0, 15, 15); final Shape shape[] = new Shape[] { rectangle, ellipse }; final PatternStroke shapeStroke = new PatternStroke(shape, 20.0f); final CompositeStroke compositeStroke = new CompositeStroke(new BasicStroke(25f), shapeStroke); g2.setStroke(compositeStroke); g2.drawArc(30, 50, 250, 100, 180, -180); g2.drawArc(30, 90, 250, 100, 180, 180); } } public class CompositeStroke implements Stroke { private Stroke mainStroke; private Stroke minusStroke; public CompositeStroke(Stroke mainStroke, Stroke minusStroke) { this.mainStroke = mainStroke; this.minusStroke = minusStroke; } public Shape createStrokedShape(Shape shape) { final Area mainArea = new Area(this.mainStroke.createStrokedShape(shape)); final Area minusArea = new Area(this.minusStroke.createStrokedShape(shape)); mainArea.subtract(minusArea); return mainArea; } } public class PatternStroke implements Stroke { private float resolution; // the distance between shapes private boolean repeat = true; private final Shape shapes[]; private final AffineTransform affineTransform = new AffineTransform(); public PatternStroke(Shape shapes[], float resolution) { this.resolution = resolution; // the distance between shapes this.shapes = new Shape[shapes.length]; for (int i = 0; i < this.shapes.length; i++) { final Rectangle2D bounds = shapes[i].getBounds2D(); this.affineTransform.setToTranslation(-bounds.getCenterX(), -bounds.getCenterY()); this.shapes[i] = this.affineTransform.createTransformedShape(shapes[i]); } } public Shape createStrokedShape(Shape shape) { final GeneralPath result = new GeneralPath(); final PathIterator pathIterator = new FlatteningPathIterator(shape.getPathIterator(null), 1.0); float points[] = new float[2]; float moveX = 0; float moveY = 0; float curX = 0; float curY = 0; float nextX = 0; float nextY = 0; float distanceToNext = 0; int length = this.shapes.length; int segmentType = 0; int currentPos = 0; while ((currentPos < length) && (!pathIterator.isDone())) { segmentType = pathIterator.currentSegment(points); switch (segmentType) { case PathIterator.SEG_MOVETO: // starting location for a new subpath { moveX = nextX = points[0]; moveY = nextY = points[1]; result.moveTo(moveX, moveY); distanceToNext = 0; break; } case PathIterator.SEG_LINETO: // reached end of a subpath { curX = points[0]; curY = points[1]; // each subpath needs to be closed, so do not break here // instead fall into SEG_CLOSE code below } case PathIterator.SEG_CLOSE: // close off the subpath { points[0] = moveX; points[1] = moveY; final float dx = curX - nextX; final float dy = curY - nextY; final float distance = (float) (Math.sqrt((dx * dx) + (dy * dy))); if (distance >= distanceToNext) { final float ratio = 1.0f / distance; final float angle = (float) (Math.atan2(dy, dx)); while ((currentPos < length) && (distance >= distanceToNext)) { final float x = nextX + distanceToNext * dx * ratio; final float y = nextY + distanceToNext * dy * ratio; this.affineTransform.setToTranslation(x, y); this.affineTransform.rotate(angle); result.append(this.affineTransform.createTransformedShape(this.shapes[currentPos]), false); distanceToNext += this.resolution; currentPos++; if (this.repeat) { currentPos %= length; } } } distanceToNext -= distance; nextX = curX; nextY = curY; break; } } pathIterator.next(); } return result; } } }
Stroke_Rough_Demo Example: (Run Applet)
import java.awt.*; import java.awt.geom.*; import javax.swing.*; public class Stroke_Rough_Demo extends JApplet { @Override public void init() { this.setContentPane(new View()); } public class View extends JPanel { @Override protected void paintComponent(Graphics g) { super.paintComponent(g); final Graphics2D g2 = (Graphics2D) g; g2.setStroke(new RoughStroke(20, 10, 2)); g2.drawArc(30, 50, 250, 100, 180, -180); g2.drawArc(30, 90, 250, 100, 180, 180); } } public class RoughStroke implements Stroke { private int strokeWidth; private float resolution; private float amplitude; public RoughStroke(int strokeSize, float resolution, float amplitude) { this.strokeWidth = strokeSize; this.resolution = resolution; this.amplitude = amplitude; } public Shape createStrokedShape(Shape shape) { shape = new BasicStroke(this.strokeWidth).createStrokedShape(shape); final GeneralPath result = new GeneralPath(); final PathIterator pathIterator = new FlatteningPathIterator(shape.getPathIterator(null), 1.0); float points[] = new float[2]; float moveX = 0; float moveY = 0; float curX = 0; float curY = 0; float nextX = 0; float nextY = 0; float distanceToNext = 0; int segmentType = 0; while (!pathIterator.isDone()) { segmentType = pathIterator.currentSegment(points); switch (segmentType) { case PathIterator.SEG_MOVETO: // starting location for a new subpath { moveX = nextX = randomize(points[0]); moveY = nextY = randomize(points[1]); result.moveTo(moveX, moveY); distanceToNext = 0; break; } case PathIterator.SEG_LINETO: { curX = randomize(points[0]); curY = randomize(points[1]); // each subpath needs to be closed, so do not break here // instead fall into SEG_CLOSE code below } case PathIterator.SEG_CLOSE: // { points[0] = moveX; points[1] = moveY; final float dx = curX - nextX; final float dy = curY - nextY; final float distance = (float) (Math.sqrt(dx * dx + dy * dy)); if (distance >= distanceToNext) { final float ratio = 1.0f / distance; while (distance >= distanceToNext) { final float x = nextX + distanceToNext * dx * ratio; final float y = nextY + distanceToNext * dy * ratio; result.lineTo(randomize(x), randomize(y)); distanceToNext += this.resolution; } } distanceToNext -= distance; nextX = curX; nextY = curY; break; } } pathIterator.next(); } return result; } private float randomize(float x) { return x + (float) (Math.random() * this.amplitude * 2 - 1); } } }
Stroke_String_Demo Example: (Run Applet)
import java.awt.*; import java.awt.font.*; import java.awt.geom.*; import javax.swing.*; public class Stroke_String_Demo extends JApplet { @Override public void init() { this.setContentPane(new View()); } public class View extends JPanel { @Override protected void paintComponent(Graphics g) { super.paintComponent(g); final Graphics2D g2 = (Graphics2D) g; g2.setStroke(new StringStroke("Dundalk I.T.", new Font("Times New Roman", Font.BOLD + Font.ITALIC, 50), StringStroke.ALIGN_CENTER)); g2.drawArc(30, 50, 250, 100, 180, -180); g2.setStroke(new StringStroke("DkIT", new Font("Times New Roman", Font.BOLD, 30), StringStroke.REPEAT)); g2.drawArc(30, 90, 250, 100, 180, 180); } } public class StringStroke implements Stroke { public final static int ALIGN_LEFT = 0; public final static int ALIGN_RIGHT = 1; public final static int ALIGN_CENTER = 2; public final static int REPEAT = 3; public final static int STRETCH = 4; private int layout = 0; private String text; private Font font; public StringStroke(String text, Font font, int layout) { this.text = text; this.font = font; this.layout = layout; if (this.layout == StringStroke.REPEAT) { this.text += " "; // place gap between repeating text } else if ((this.layout == StringStroke.ALIGN_CENTER)) { this.text = " " + this.text + " "; } else if ((this.layout == StringStroke.ALIGN_RIGHT)) { this.text = " " + this.text; } } public Shape createStrokedShape(Shape shape) { final FontRenderContext fontRenderContext = new FontRenderContext(null, true, true); final GlyphVector glyphVector = this.font.createGlyphVector(fontRenderContext, this.text); final GeneralPath result = new GeneralPath(); int length = glyphVector.getNumGlyphs(); if (length == 0) // string length == 0 { return result; } final PathIterator pathIterator = new FlatteningPathIterator(shape.getPathIterator(null), 1.0); final AffineTransform affineTransform = new AffineTransform(); float points[] = new float[2]; float moveX = 0; float moveY = 0; float curX = 0; float curY = 0; float nextX = 0; float nextY = 0; float distanceToNext = 0; int segmentType = 0; int curCharacterPos = 0; // calculate the length of the path final float pathLength = measurePathLength(shape); final double stringLength = glyphVector.getLogicalBounds().getWidth(); float factor; if (this.layout == StringStroke.STRETCH) { factor = pathLength / (float) stringLength; } else { factor = 1.0f; } float offset = 0; if (this.layout == StringStroke.ALIGN_CENTER) { offset = (pathLength - (float) stringLength) / 2.0f; } else if (this.layout == StringStroke.ALIGN_RIGHT) { offset = (pathLength - (float) stringLength); } float nextAdvance = 0; while ((curCharacterPos < length) && (!pathIterator.isDone())) { segmentType = pathIterator.currentSegment(points); switch (segmentType) { case PathIterator.SEG_MOVETO: // starting location for a new subpath { curX = points[0]; curY = points[1]; moveX = curX; moveY = curY; result.moveTo(moveX, moveY); nextAdvance = glyphVector.getGlyphMetrics(curCharacterPos).getAdvance() * 0.5f; distanceToNext = nextAdvance; break; } case PathIterator.SEG_LINETO: // reached end of a subpath { nextX = points[0]; nextY = points[1]; // each subpath needs to be closed, so do not break here // instead fall into SEG_CLOSE code below } case PathIterator.SEG_CLOSE: // close off the subpath { points[0] = moveX; points[1] = moveY; final float dx = nextX - curX; final float dy = nextY - curY; final float distance = (float) Math.sqrt(dx * dx + dy * dy); if (distance >= distanceToNext) { final float ratio = 1.0f / distance; final float angle = (float) Math.atan2(dy, dx); while ((curCharacterPos < length) && (distance >= distanceToNext)) { final Shape glyph = glyphVector.getGlyphOutline(curCharacterPos); final Point2D p = glyphVector.getGlyphPosition(curCharacterPos); final float px = (float) p.getX(); final float py = (float) p.getY(); final float x = curX + distanceToNext * dx * ratio; final float y = curY + distanceToNext * dy * ratio; float advance = nextAdvance; if ((curCharacterPos < length - 1)) { nextAdvance = glyphVector.getGlyphMetrics(curCharacterPos + 1).getAdvance() * 0.5f; } else { nextAdvance = 0.0f; } // allow for offset to CENTER or RIGHT if (curCharacterPos == 0) { advance += offset; } affineTransform.setToTranslation(x, y); affineTransform.rotate(angle); affineTransform.translate(-px - advance, -py); result.append(affineTransform.createTransformedShape(glyph), false); distanceToNext += (advance + nextAdvance) * factor; curCharacterPos++; if ((this.layout == StringStroke.REPEAT) && (curCharacterPos == length)) { curCharacterPos = 0; nextAdvance = glyphVector.getGlyphMetrics(curCharacterPos).getAdvance() * 0.5f; } } } distanceToNext -= distance; curX = nextX; curY = nextY; break; } } pathIterator.next(); } return result; } public float measurePathLength(Shape shape) { final PathIterator pathIterator = new FlatteningPathIterator(shape.getPathIterator(null), 1.0); float points[] = new float[2]; float moveX = 0; float moveY = 0; float nextX = 0; float nextY = 0; float curX = 0; float curY = 0; float total = 0; int segmentType = 0; while (!pathIterator.isDone()) { segmentType = pathIterator.currentSegment(points); switch (segmentType) { case PathIterator.SEG_MOVETO: // starting location for a new subpath { moveX = nextX = points[0]; moveY = nextY = points[1]; break; } case PathIterator.SEG_LINETO: // reached end of a subpath { curX = points[0]; curY = points[1]; // each subpath needs to be closed, so do not break here // instead fall into SEG_CLOSE code below } case PathIterator.SEG_CLOSE: // close off the subpath { points[0] = moveX; points[1] = moveY; final float dx = curX - nextX; final float dy = curY - nextY; total += (float) Math.sqrt((dx * dx) + (dy * dy)); nextX = curX; nextY = curY; break; } } pathIterator.next(); } return total; } } }
Copyright Derek O' Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.