What 2D shapes are and how they are represented in JavaFX
How to draw 2D shapes
How to draw complex shapes using the Path class
How to draw shapes using the Scalable Vector Graphics (SVG)
How to combine shapes to build another shape
How to use strokes for a shape
How to style shapes using Cascading Style Sheets (CSS)
The examples of this chapter lie in the com.jdojo.shape package. In order for them to work, you must add a corresponding line to the module-info.java file:
...
opens com.jdojo.shape to javafx.graphics, javafx.base;
...
What Are 2D Shapes?
Any shape that can be drawn in a two-dimensional plane is called a 2D shape. JavaFX offers a variety of nodes to draw different types of shapes (lines, circles, rectangles, etc.). You can add shapes to a scene graph.
Shapes can be two-dimensional or three-dimensional. In this chapter, I will discuss 2D shapes. Chapter 16 discusses 3D shapes.
All shape classes are in the javafx.scene.shape package. Classes representing 2D shapes are inherited from the abstract Shape class as shown in Figure 14-1.
A shape has a size and a position, which are defined by their properties. For example, the width and height properties define the size of a rectangle, the radius property defines the size of a circle, the x and y properties define the position of the upper-left corner of a rectangle, the centerX and centerY properties define the center of a circle, etc.
Shapes are not resized by their parents during layout. The size of a shape changes only when its size-related properties are changed. You may find a phrase like “JavaFX shapes are nonresizable.” It means shapes are nonresizable by their parent during layout. They can be resized only by changing their properties.
Shapes have an interior and a stroke. The properties for defining the interior and stroke of a shape are declared in the Shape class. The fill property specifies the color to fill the interior of the shape. The default fill is Color.BLACK. The stroke property specifies the color for the outline stroke, which is null by default, except for Line, Polyline, and Path, which have Color.BLACK as the default stroke. The strokeWidth property specifies the width of the outline, which is 1.0px by default. The Shape class contains other stroke-related properties that I will discuss in the section “Understanding the Stroke of a Shape.”
The Shape class contains a smooth property, which is true by default. Its true value indicates that an antialiasing hint should be used to render the shape. If it is set to false, the antialiasing hint will not be used, which may result in the edges of shapes being not crisp.
The program in Listing 14-1 creates two circles. The first circle has a light gray fill and no stroke, which is the default. The second circle has a yellow fill and a 2.0px wide black stroke. Figure 14-2 shows the two circles.
// ShapeTest.java
package com.jdojo.shape;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class ShapeTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
// Create a circle with a light gray fill and no stroke
Circle c1 = new Circle(40, 40, 40);
c1.setFill(Color.LIGHTGRAY);
// Create a circle with an yellow fill and a black stroke
// of 2.0px
Circle c2 = new Circle(40, 40, 40);
c2.setFill(Color.YELLOW);
c2.setStroke(Color.BLACK);
c2.setStrokeWidth(2.0);
HBox root = new HBox(c1, c2);
root.setSpacing(10);
root.setStyle("""
-fx-padding: 10;
-fx-border-style: solid inside;
-fx-border-width: 2;
-fx-border-insets: 5;
-fx-border-radius: 5;
-fx-border-color: blue;""");
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Using Shapes");
stage.show();
}
}
Listing 14-1
Using fill and stroke Properties of the Shape Class
Drawing 2D Shapes
The following sections describe in detail how to use the JavaFX classes representing 2D shapes to draw those shapes.
Drawing Lines
An instance of the Line class represents a line node. A Line has no interior. By default, its fill property is set to null. Setting fill has no effects. The default stroke is Color.BLACK, and the default strokeWidth is 1.0. The Line class contains four double properties:
startX
startY
endX
endY
The Line represents a line segment between (startX, startY) and (endX, endY) points. The Line class has a no-args constructor, which defaults all its four properties to zero resulting in a line from (0, 0) to (0, 0), which represents a point. Another constructor takes values for startX, startY, endX, and endY. After you create a Line, you can change its location and length by changing any of the four properties.
The program in Listing 14-2 creates some Lines and sets their stroke and strokeWidth properties. The first Line will appear as a point. Figure 14-3 shows the line.
// LineTest.java
package com.jdojo.shape;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class LineTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
// It will be just a point at (0, 0)
Line line1 = new Line();
Line line2 = new Line(0, 0, 50, 0);
line2.setStrokeWidth(1.0);
Line line3 = new Line(0, 50, 50, 0);
line3.setStrokeWidth(2.0);
line3.setStroke(Color.RED);
Line line4 = new Line(0, 0, 50, 50);
line4.setStrokeWidth(5.0);
line4.setStroke(Color.BLUE);
HBox root = new HBox(line1, line2, line3, line4);
root.setSpacing(10);
root.setStyle("""
-fx-padding: 10;
-fx-border-style: solid inside;
-fx-border-width: 2;
-fx-border-insets: 5;
-fx-border-radius: 5;
-fx-border-color: blue;""");
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Using Lines");
stage.show();
}
}
Listing 14-2
Using the Line Class to Create Line Nodes
Drawing Rectangles
An instance of the Rectangle class represents a rectangle node. The class uses six properties to define the rectangle:
x
y
width
height
arcWidth
arcHeight
The x and y properties are the x and y coordinates of the upper-left corner of the rectangle in the local coordinate system of the node. The width and height properties are the width and height of the rectangle, respectively. Specify the same width and height to draw a square.
By default, the corners of a rectangle are sharp. A rectangle can have rounded corners by specifying the arcWidth and arcHeight properties. You can think of one of the quadrants of an ellipse positioned at the four corners to make them round. The arcWidth and arcHeight properties are the horizontal and vertical diameters of the ellipse. By default, their values are zero, which makes a rectangle have sharp corners. Figure 14-4 shows two rectangles—one with sharp corners and one with rounded corners. The ellipse is shown to illustrate the relationship between the arcWidth and arcHeight properties for a rounded rectangle.
The Rectangle class contains several constructors. They take various properties as arguments. The default values for x, y, width, height, arcWidth, and arcHeight properties are zero. The constructors are
Rectangle()
Rectangle(double width, double height)
Rectangle(double x, double y, double width, double height)
You will not see effects of specifying the values for the x and y properties for a Rectangle when you add it to most of the layout panes as they place their children at (0, 0). A Pane uses these properties. The program in Listing 14-3 adds two rectangles to a Pane. The first rectangle uses the default values of zero for the x and y properties. The second rectangle specifies 120 for the x property and 20 for the y property. Figure 14-5 shows the positions of the two rectangles inside the Pane. Notice that the upper-left corner of the second rectangle (on the right) is at (120, 20).
Rectangle rect2 = new Rectangle(120, 20, 100, 50);
rect2.setFill(Color.WHITE);
rect2.setStroke(Color.BLACK);
rect2.setArcWidth(10);
rect2.setArcHeight(10);
Pane root = new Pane();
root.getChildren().addAll(rect1, rect2);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Using Rectangles");
stage.show();
}
}
Listing 14-3
Using the Rectangle Class to Create Rectangle Nodes
Drawing Circles
An instance of the Circle class represents a circle node. The class uses three properties to define the circle:
centerX
centerY
radius
The centerX and centerY properties are the x and y coordinates of the center of the circle in the local coordinate system of the node. The radius property is the radius of the circle. The default values for these properties are zero.
The program in Listing 14-4 adds two circles to an HBox. Notice that the HBox does not use centerX and centerY properties of the circles. Add them to a Pane to see the effects. Figure 14-6 shows the two circles.
An instance of the Ellipse class represents an ellipse node. The class uses four properties to define the ellipse:
centerX
centerY
radiusX
radiusY
The centerX and centerY properties are the x and y coordinates of the center of the circle in the local coordinate system of the node. The radiusX and radiusY are the radii of the ellipse in the horizontal and vertical directions. The default values for these properties are zero. A circle is a special case of an ellipse when radiusX and radiusY are the same.
The program in Listing 14-5 creates three instances of the Ellipse class. The third instance draws a circle as the program sets the same value for the radiusX and radiusY properties. Figure 14-7 shows the three ellipses.
// EllipseTest.java
package com.jdojo.shape;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Ellipse;
import javafx.stage.Stage;
public class EllipseTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Ellipse e1 = new Ellipse(50, 30);
e1.setFill(Color.LIGHTGRAY);
Ellipse e2 = new Ellipse(60, 30);
e2.setFill(Color.YELLOW);
e2.setStroke(Color.BLACK);
e2.setStrokeWidth(2.0);
// Draw a circle using the Ellipse class (radiusX=radiusY=30)
Ellipse e3 = new Ellipse(30, 30);
e3.setFill(Color.YELLOW);
e3.setStroke(Color.BLACK);
e3.setStrokeWidth(2.0);
HBox root = new HBox(e1, e2, e3);
root.setSpacing(10);
root.setStyle("""
-fx-padding: 10;
-fx-border-style: solid inside;
-fx-border-width: 2;
-fx-border-insets: 5;
-fx-border-radius: 5;
-fx-border-color: blue;""");
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Using Ellipses");
stage.show();
}
}
Listing 14-5
Using the Ellipse Class to Create Ellipse Nodes
Drawing Polygons
An instance of the Polygon class represents a polygon node. The class does not define any public properties. It lets you draw a polygon using an array of (x, y) coordinates defining the vertices of the polygon. Using the Polygon class, you can draw any type of geometric shape that is created using connected lines (triangles, pentagons, hexagons, parallelograms, etc.).
The Polygon class contains two constructors:
Polygon()
Polygon(double... points)
The no-args constructor creates an empty polygon. You need add the (x, y) coordinates of the vertices of the shape. The polygon will draw a line from the first vertex to the second vertex, from the second to the third, and so on. Finally, the shape is closed by drawing a line from the last vertex to the first vertex.
The Polygon class stores the coordinates of the vertices in an ObservableList<Double>. You can get the reference of the observable list using the getPoints() method. Notice that it stores the coordinates in a list of Double, which is simply a number. It is your job to pass the numbers in pairs, so they can be used as (x, y) coordinates of vertices. If you pass an odd number of numbers, no shape is created. The following snippet of code creates two triangles—one passes the coordinates of the vertices in the constructor, and another adds them to the observable list later. Both triangles are geometrically the same:
// Create an empty triangle and add vertices later
Polygon triangle1 = new Polygon();
triangle1.getPoints().addAll(50.0, 0.0,
0.0, 100.0,
100.0, 100.0);
// Create a triangle with vertices
Polygon triangle2 = new Polygon(50.0, 0.0,
0.0, 100.0,
100.0, 100.0);
The program in Listing 14-6 creates a triangle, a parallelogram, and a hexagon using the Polygon class as shown in Figure 14-8.
// PolygonTest.java
package com.jdojo.shape;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.stage.Stage;
public class PolygonTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Polygon triangle1 = new Polygon();
triangle1.getPoints().addAll(50.0, 0.0,
0.0, 50.0,
100.0, 50.0);
triangle1.setFill(Color.WHITE);
triangle1.setStroke(Color.RED);
Polygon parallelogram = new Polygon();
parallelogram.getPoints().addAll(
30.0, 0.0,
130.0, 0.0,
100.00, 50.0,
0.0, 50.0);
parallelogram.setFill(Color.YELLOW);
parallelogram.setStroke(Color.BLACK);
Polygon hexagon = new Polygon(
100.0, 0.0,
120.0, 20.0,
120.0, 40.0,
100.0, 60.0,
80.0, 40.0,
80.0, 20.0);
hexagon.setFill(Color.WHITE);
hexagon.setStroke(Color.BLACK);
HBox root = new HBox(triangle1, parallelogram, hexagon);
root.setSpacing(10);
root.setStyle("""
-fx-padding: 10;
-fx-border-style: solid inside;
-fx-border-width: 2;
-fx-border-insets: 5;
-fx-border-radius: 5;
-fx-border-color: blue;""");
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Using Polygons");
stage.show();
}
}
Listing 14-6
Using the Polygon Class to Create a Triangle, a Parallelogram, and a Hexagon
Drawing Polylines
A polyline is similar to a polygon, except that it does not draw a line between the last and first points. That is, a polyline is an open polygon. However, the fill color is used to fill the entire shape as if the shape was closed.
An instance of the Polyline class represents a polyline node. The class does not define any public properties. It lets you draw a polyline using an array of (x, y) coordinates defining the vertices of the polyline. Using the Polyline class, you can draw any type of geometric shape that is created using connected lines (triangles, pentagons, hexagons, parallelograms, etc.).
The Polyline class contains two constructors:
Polyline()
Polyline(double... points)
The no-args constructor creates an empty polyline. You need add (x, y) coordinates of the vertices of the shape. The polygon will draw a line from the first vertex to the second vertex, from the second to the third, and so on. Unlike a Polygon, the shape is not closed automatically. If you want to close the shape, you need to add the coordinates of the first vertex as the last pair of numbers.
If you want to add coordinates of vertices later, add them to the ObservableList<Double> returned by the getPoints() method of the Polyline class. The following snippet of code creates two triangles with the same geometrical properties using different methods. Notice that the first and the last pairs of numbers are the same in order to close the triangle:
// Create an empty triangle and add vertices later
Polygon triangle1 = new Polygon();
triangle1.getPoints().addAll(
50.0, 0.0,
0.0, 100.0,
100.0, 100.0,
50.0, 0.0);
// Create a triangle with vertices
Polygon triangle2 = new Polygon(
50.0, 0.0,
0.0, 100.0,
100.0, 100.0,
50.0, 0.0);
The program in Listing 14-7 creates a triangle, an open parallelogram, and a hexagon using the Polyline class as shown in Figure 14-9.
// PolylineTest.java
package com.jdojo.shape;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polyline;
import javafx.stage.Stage;
public class PolylineTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Polyline triangle1 = new Polyline();
triangle1.getPoints().addAll(
50.0, 0.0,
0.0, 50.0,
100.0, 50.0,
50.0, 0.0);
triangle1.setFill(Color.WHITE);
triangle1.setStroke(Color.RED);
// Create an open parallelogram
Polyline parallelogram = new Polyline();
parallelogram.getPoints().addAll(
30.0, 0.0,
130.0, 0.0,
100.00, 50.0,
0.0, 50.0);
parallelogram.setFill(Color.YELLOW);
parallelogram.setStroke(Color.BLACK);
Polyline hexagon = new Polyline(
100.0, 0.0,
120.0, 20.0,
120.0, 40.0,
100.0, 60.0,
80.0, 40.0,
80.0, 20.0,
100.0, 0.0);
hexagon.setFill(Color.WHITE);
hexagon.setStroke(Color.BLACK);
HBox root = new HBox(triangle1, parallelogram, hexagon);
root.setSpacing(10);
root.setStyle("""
-fx-padding: 10;
-fx-border-style: solid inside;
-fx-border-width: 2;
-fx-border-insets: 5;
-fx-border-radius: 5;
-fx-border-color: blue;""");
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Using Polylines");
stage.show();
}
}
Listing 14-7
Using the Polyline Class to Create a Triangle, an Open Parallelogram, and a Hexagon
Drawing Arcs
An instance of the Arc class represents a sector of an ellipse. The class uses seven properties to define the ellipse:
centerX
centerY
radiusX
radiusY
startAngle
length
type
The first four properties define an ellipse. Please refer to the section “Drawing Ellipses” for how to define an ellipse. The last three properties define a sector of the ellipse that is the Arc node. The startAngle property specifies the start angle of the section in degrees measured counterclockwise from the positive x-axis. It defines the beginning of the arc. The length is an angle in degrees measured counterclockwise from the start angle to define the end of the sector. If the length property is set to 360, the Arc is a full ellipse. Figure 14-10 illustrates the properties.
The type property specifies the way the Arc is closed. It is one of the constants, OPEN, CHORD, and ROUND, defined in the ArcType enum:
The ArcType.OPEN does not close the arc.
The ArcType.CHORD closes the arc by joining the starting and ending points by a straight line.
The ArcType.ROUND closes the arc by joining the starting and ending points to the center of the ellipse.
Figure 14-11 shows the three closure types for an arc. The default type for an Arc is ArcType.OPEN. If you do not apply a stroke to an Arc, both ArcType.OPEN and ArcType.CHORD look the same.
The program in Listing 14-8 shows how to create Arc nodes. The resulting window is shown in Figure 14-12.
// ArcTest.java
package com.jdojo.shape;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.stage.Stage;
public class ArcTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
// An OPEN arc with a fill
Arc arc1 = new Arc(0, 0, 50, 100, 0, 90);
arc1.setFill(Color.LIGHTGRAY);
// An OPEN arc with no fill and a stroke
Arc arc2 = new Arc(0, 0, 50, 100, 0, 90);
arc2.setFill(Color.TRANSPARENT);
arc2.setStroke(Color.BLACK);
// A CHORD arc with no fill and a stroke
Arc arc3 = new Arc(0, 0, 50, 100, 0, 90);
arc3.setFill(Color.TRANSPARENT);
arc3.setStroke(Color.BLACK);
arc3.setType(ArcType.CHORD);
// A ROUND arc with no fill and a stroke
Arc arc4 = new Arc(0, 0, 50, 100, 0, 90);
arc4.setFill(Color.TRANSPARENT);
arc4.setStroke(Color.BLACK);
arc4.setType(ArcType.ROUND);
// A ROUND arc with a gray fill and a stroke
Arc arc5 = new Arc(0, 0, 50, 100, 0, 90);
arc5.setFill(Color.GRAY);
arc5.setStroke(Color.BLACK);
arc5.setType(ArcType.ROUND);
HBox root = new HBox(arc1, arc2, arc3, arc4, arc5);
root.setSpacing(10);
root.setStyle("""
-fx-padding: 10;
-fx-border-style: solid inside;
-fx-border-width: 2;
-fx-border-insets: 5;
-fx-border-radius: 5;
-fx-border-color: blue;""");
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Using Arcs");
stage.show();
}
}
Listing 14-8
Using the Arc Class to Create Arcs, Which Are Sectors of Ellipses
Drawing Quadratic Curves
Bezier curves are used in computer graphics to draw smooth curves. An instance of the QuadCurve class represents a quadratic Bezier curve segment intersecting two specified points using a specified Bezier control point. The QuadCurve class contains six properties to specify the three points:
The program in Listing 14-9 draws the same quadratic Bezier curve twice—once with a stroke and a transparent fill and once with no stroke and a light gray fill. Figure 14-13 shows the two curves.
// QuadCurveTest.java
package com.jdojo.shape;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.QuadCurve;
import javafx.stage.Stage;
public class QuadCurveTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
QuadCurve qc1 = new QuadCurve(0, 100, 20, 0, 150, 100);
qc1.setFill(Color.TRANSPARENT);
qc1.setStroke(Color.BLACK);
QuadCurve qc2 = new QuadCurve(0, 100, 20, 0, 150, 100);
qc2.setFill(Color.LIGHTGRAY);
HBox root = new HBox(qc1, qc2);
root.setSpacing(10);
root.setStyle("""
-fx-padding: 10;
-fx-border-style: solid inside;
-fx-border-width: 2;
-fx-border-insets: 5;
-fx-border-radius: 5;
-fx-border-color: blue;""");
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Using QuadCurves");
stage.show();
}
}
Listing 14-9
Using the QuadCurve Class to Draw Quadratic BezierCurve
Drawing Cubic Curves
An instance of the CubicCurve class represents a cubic Bezier curve segment intersecting two specified points using two specified Bezier control points. Please refer to the Wikipedia article at http://en.wikipedia.org/wiki/Bezier_curves for a detailed explanation and demonstration of Bezier curves. The CubicCurve class contains eight properties to specify the four points:
The program in Listing 14-10 draws the same cubic Bezier curve twice—once with a stroke and a transparent fill and once with no stroke and a light gray fill. Figure 14-14 shows the two curves.
Using the CubicCurve Class to Draw a Cubic Bezier Curve
Building Complex Shapes Using the Path Class
I discussed several shape classes in the previous sections. They are used to draw simple shapes. It is not convenient to use them for complex shapes. You can draw complex shapes using the Path class. An instance of the Path class defines the path (outline) of a shape. A path consists of one or more subpaths. A subpath consists of one or more path elements. Each subpath has a starting point and an ending point.
A path element is an instance of the PathElement abstract class. The following subclasses of the PathElement class exist to represent a specific type of path elements:
MoveTo
LineTo
HLineTo
VLineTo
ArcTo
QuadCurveTo
CubicCurveTo
ClosePath
Before you see an example, let us outline the process of creating a shape using the Path class. The process is similar to drawing a shape on a paper with a pencil. First, you place the pencil on the paper. You can restate it, “You move the pencil to a point on the paper.” Regardless of what shape you want to draw, moving the pencil to a point must be the first step. Now, you start moving your pencil to draw a path element (e.g., a horizontal line). The starting point of the current path element is the same as the ending point of the previous path element. Keep drawing as many path elements as needed (e.g., a vertical line, an arc, and a quadratic Bezier curve). At the end, you can end the last path element at the same point where you started or somewhere else.
The coordinates defining a PathElement can be absolute or relative. By default, coordinates are absolute. It is specified by the absolute property of the PathElement class. If it is true, which is the default, the coordinates are absolute. If it is false, the coordinates are relative. The absolute coordinates are measured relative to the local coordinate system of the node. Relative coordinates are measured treating the ending point of the previous PathElement as the origin.
The Path class contains three constructors:
Path()
Path(Collection<? extends PathElement> elements)
Path(PathElement... elements)
The no-args constructor creates an empty shape. The other two constructors take a list of path elements as arguments. A Path stores path elements in an ObservableList<PathElement>. You can get the reference of the list using the getElements() method. You can modify the list of path elements to modify the shape. The following snippet of code shows two ways of creating shapes using the Path class:
// Pass the path elements to the constructor
Path shape1 = new Path(pathElement1, pathElement2, pathElement3);
// Create an empty path and add path elements to the elements list
An instance of the PathElement may be added as a path element to Path objects simultaneously. A Path uses the same fill and stroke for all its path elements.
The MoveTo Path Element
A MoveTo path element is used to make the specified x and y coordinates as the current point. It has the effect of lifting and placing the pencil at the specified point on the paper. The first path element of a Path object must be a MoveTo element, and it must not use relative coordinates. The MoveTo class defines two double properties that are the x and y coordinates of the point:
x
y
The MoveTo class contains two constructors. The no-args constructor sets the current point to (0.0, 0.0). The other constructor takes the x and y coordinates of the current point as arguments:
// Create a MoveTo path element to move the current point to (0.0, 0.0)
MoveTo mt1 = new MoveTo();
// Create a MoveTo path element to move the current point to (10.0, 10.0)
MoveTo mt2 = new MoveTo(10.0, 10.0);
Tip
A path must start with a MoveTo path element. You can have multiple MoveTo path elements in a path. A subsequent MoveTo element denotes the starting point of a new subpath.
The LineTo Path Element
A LineTo path element draws a straight line from the current point to the specified point. It contains two doubleproperties that are the x and y coordinates of the end of the line:
x
y
The LineTo class contains two constructors. The no-args constructor sets the end of the line to (0.0, 0.0). The other constructor takes the x and y coordinates of the end of the line as arguments:
// Create a LineTo path element with its end at (0.0, 0.0)
LineTo lt1 = new LineTo();
// Create a LineTo path element with its end at (10.0, 10.0)
LineTo lt2 = new LineTo(10.0, 10.0);
With the knowledge of the MoveTo and LineTo path elements, you can construct shapes that are made of lines only. The following snippet of code creates a triangle as shown in Figure 14-15. The figure shows the triangle and its path elements. The arrows show the flow of the drawing. Notice that the drawing starts at (0.0) using the first MoveTo path element.
Path triangle = new Path(
new MoveTo(0, 0),
new LineTo(0, 50),
new LineTo(50, 50),
new LineTo(0, 0));
The ClosePath path element closes a path by drawing a straight line from the current point to the starting point of the path. If multiple MoveTo path elements exist in a path, a ClosePath draws a straight line from the current point to the point identified by the last MoveTo. You can rewrite the path for the previous triangle example using a ClosePath:
Path triangle = new Path(
new MoveTo(0, 0),
new LineTo(0, 50),
new LineTo(50, 50),
new ClosePath());
The program in Listing 14-11 creates two Path nodes: one triangle and one with two inverted triangles to give it a look of a star as shown in Figure 14-16. In the second shape, each triangle is created as a subpath—each subpath starting with a MoveTo element. Notice the two uses of the ClosePath elements. Each ClosePath closes its subpath.
// PathTest.java
package com.jdojo.shape;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
public class PathTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Path triangle = new Path(
new MoveTo(0, 0),
new LineTo(0, 50),
new LineTo(50, 50),
new ClosePath());
Path star = new Path();
star.getElements().addAll(
new MoveTo(30, 0),
new LineTo(0, 30),
new LineTo(60, 30),
new ClosePath(),/* new LineTo(30, 0), */
new MoveTo(0, 10),
new LineTo(60, 10),
new LineTo(30, 40),
new ClosePath() /*new LineTo(0, 10)*/);
HBox root = new HBox(triangle, star);
root.setSpacing(10);
root.setStyle("""
-fx-padding: 10;
-fx-border-style: solid inside;
-fx-border-width: 2;
-fx-border-insets: 5;
-fx-border-radius: 5;
-fx-border-color: blue;""");
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Using Paths");
stage.show();
}
}
Listing 14-11
Using the Path Class to Create a Triangle and a Star
The HLineTo and VLineTo Path Elements
The HLineTo path element draws a horizontal line from the current point to the specified x coordinate. The y coordinate of the ending point of the line is the same as the y coordinate of the current point. The x property of the HLineTo class specifies the x coordinate of the ending point:
// Create an horizontal line from the current point (x, y) to (50, y)
HLineTo hlt = new HLineTo(50);
The VLineTo path element draws a vertical line from the current point to the specified y coordinate. The x coordinate of the ending point of the line is the same as the x coordinate of the current point. The y property of the VLineTo class specifies the y coordinate of the ending point:
// Create a vertical line from the current point (x, y) to (x, 50)
VLineTo vlt = new VLineTo(50);
Tip
The LineTo path element is the generic version of HLineTo and VLineTo.
The following snippet of code creates the same triangle as discussed in the previous section. This time, you use HLineTo and VLineTo path elements to draw the base and height sides of the triangle instead of the LineTo path elements:
Path triangle = new Path(
new MoveTo(0, 0),
new VLineTo(50),
new HLineTo(50),
new ClosePath());
The ArcTo Path Element
An ArcTo path element defines a segment of ellipse connecting the current point and the specified point. It contains the following properties:
radiusX
radiusY
x
y
XAxisRotation
largeArcFlag
sweepFlag
The radiusX and radiusY properties specify the horizontal and vertical radii of the ellipse. The x and y properties specify the x and y coordinates of the ending point of the arc. Note that the starting point of the arc is the current point of the path.
The XAxisRotation property specifies the rotation of the x-axis of the ellipse in degrees. Note that the rotation is for the x-axis of the ellipse from which the arc is obtained, not the x-axis of the coordinate system of the node. A positive value rotates the x-axis counterclockwise.
The largeArcFlag and sweepFlag properties are Boolean type, and by default, they are set to false. Their uses need a detailed explanation. Two ellipses can pass through two given points as shown in Figure 14-17 giving us four arcs to connect the two points.
Figure 14-17 shows starting and ending points labeled Start and End, respectively. Two points on an ellipse can be traversed through the larger arc or smaller arc. If the largeArcFlag is true, the larger arc is used. Otherwise, the smaller arc is used.
When it is decided that the larger or smaller arc is used, you still have two choices: which ellipse of the two possible ellipses will be used? This is determined by the sweepFlag property. Try drawing the arc from the starting point to the ending point using two selected arcs—the two larger arcs or the two smaller arcs. For one arc, the traversal will be clockwise and for the other counterclockwise. If the sweepFlag is true, the ellipse with the clockwise traversal is used. If the sweepFlag is false, the ellipse with the counterclockwise traversal is used. Table 14-1 shows which type of arc from which ellipse will be used based on the two properties.
Table 14-1
Choosing the Arc Segment and the Ellipse Based on the largeArcFlag and sweepFlag Properties
largeArcFlag
sweepFlag
Arc Type
Ellipse
true
true
Larger
Ellipse-2
true
false
Larger
Ellipse-1
false
true
Smaller
Ellipse-1
false
false
Smaller
Ellipse-2
The program in Listing 14-12 uses an ArcTo path element to build a Path object. The program lets the user change properties of the ArcTo path element. Run the program and change largeArcFlag, sweepFlag, and other properties to see how they affect the ArcTo path element.
// ArcToTest.java
package com.jdojo.shape;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.shape.ArcTo;
import javafx.scene.shape.HLineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.VLineTo;
import javafx.stage.Stage;
public class ArcToTest extends Application {
private ArcTo arcTo;
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
// Create the ArcTo path element
arcTo = new ArcTo();
// Use the arcTo element to build a Path
Path path = new Path(
new MoveTo(0, 0),
new VLineTo(100),
new HLineTo(100),
new VLineTo(50),
arcTo);
BorderPane root = new BorderPane();
root.setTop(this.getTopPane());
root.setCenter(path);
root.setStyle("""
-fx-padding: 10;
-fx-border-style: solid inside;
-fx-border-width: 2;
-fx-border-insets: 5;
-fx-border-radius: 5;
-fx-border-color: blue;""");
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Using ArcTo Path Elements");
stage.show();
}
private GridPane getTopPane() {
CheckBox largeArcFlagCbx = new CheckBox("largeArcFlag");
CheckBox sweepFlagCbx = new CheckBox("sweepFlag");
Slider xRotationSlider = new Slider(0, 360, 0);
xRotationSlider.setPrefWidth(300);
xRotationSlider.setBlockIncrement(30);
xRotationSlider.setShowTickMarks(true);
xRotationSlider.setShowTickLabels(true);
Slider radiusXSlider = new Slider(100, 300, 100);
radiusXSlider.setBlockIncrement(10);
radiusXSlider.setShowTickMarks(true);
radiusXSlider.setShowTickLabels(true);
Slider radiusYSlider = new Slider(100, 300, 100);
radiusYSlider.setBlockIncrement(10);
radiusYSlider.setShowTickMarks(true);
radiusYSlider.setShowTickLabels(true);
// Bind ArcTo properties to the control data
arcTo.largeArcFlagProperty().bind(
largeArcFlagCbx.selectedProperty());
arcTo.sweepFlagProperty().bind(
sweepFlagCbx.selectedProperty());
arcTo.XaxisRotationProperty().bind(
xRotationSlider.valueProperty());
arcTo.radiusXProperty().bind(
radiusXSlider.valueProperty());
arcTo.radiusYProperty().bind(
radiusYSlider.valueProperty());
GridPane pane = new GridPane();
pane.setHgap(5);
pane.setVgap(10);
pane.addRow(0, largeArcFlagCbx, sweepFlagCbx);
pane.addRow(1, new Label("XAxisRotation"), xRotationSlider);
pane.addRow(2, new Label("radiusX"), radiusXSlider);
pane.addRow(3, new Label("radiusY"), radiusYSlider);
return pane;
}
}
Listing 14-12
Using ArcTo Path Elements
The QuadCurveTo Path Element
An instance of the QuadCurveTo class draws a quadratic Bezier curve from the current point to the specified ending point (x, y) using the specified control point (controlX, controlY). It contains four properties to specify the ending and control points.
x
y
controlX
controlY
The x and y properties specify the x and y coordinates of the ending point. The controlX and controlY properties specify the x and y coordinates of the control point.
The following snippet of code uses a QuadCurveTo with the (10, 100) control point and (0, 0) ending point. Figure 14-18 shows the resulting path.
Path path = new Path(
new MoveTo(0, 0),
new VLineTo(100),
new HLineTo(100),
new VLineTo(50),
new QuadCurveTo(10, 100, 0, 0));
The CubicCurveTo Path Element
An instance of the CubicCurveTo class draws a cubic Bezier curve from the current point to the specified ending point (x, y) using the specified control points (controlX1, controlY1) and (controlX2, controlY2). It contains six properties to specify the ending and control points:
x
y
controlX1
controlY1
controlX2
controlY2
The x and y properties specify the x and y coordinates of the ending point. The controlX1 and controlY1 properties specify the x and y coordinates of the first control point. The controlX2 and controlY2 properties specify the x and y coordinates of the second control point.
The following snippet of code uses a CubicCurveTo with the (10, 100) and (40, 80) as control points and (0, 0) as the ending point. Figure 14-19 shows the resulting path.
Path path = new Path(
new MoveTo(0, 0),
new VLineTo(100),
new HLineTo(100),
new VLineTo(50),
new CubicCurveTo(10, 100, 40, 80, 0, 0));
The ClosePath Path Element
The ClosePath path element closes the current subpath. Note that a Path may consist of multiple subpaths, and, therefore, it is possible to have multiple ClosePath elements in a Path. A ClosePath element draws a straight line from the current point to the initial point of the current subpath and ends the subpath. A ClosePath element may be followed by a MoveTo element, and in that case, the MoveTo element is the starting point of the next subpath. If a ClosePath element is followed by a path element other than a MoveTo element, the next subpath starts at the starting point of the subpath that was closed by the ClosePath element.
The following snippet of code creates a Path object, which uses two subpaths. Each subpath draws a triangle. The subpaths are closed using ClosePath elements. Figure 14-20 shows the resulting shape.
Path p1 = new Path(
new MoveTo(50, 0),
new LineTo(0, 50),
new LineTo(100, 50),
new ClosePath(),
new MoveTo(90, 15),
new LineTo(40, 65),
new LineTo(140, 65),
new ClosePath());
p1.setFill(Color.LIGHTGRAY);
The Fill Rule for a Path
A Path can be used to draw very complex shapes. Sometimes, it is hard to determine whether a point is inside or outside the shape. The Path class contains a fillRule property that is used to determine whether a point is inside a shape. Its value could be one of the constants of the FillRule enum: NON_ZERO and EVEN_ODD. If a point is inside the shape, it will be rendered using the fill color. Figure 14-21 shows two triangles created by a Path and a point in the area common to both triangles. I will discuss whether the point is considered inside the shape.
The direction of the stroke is the vital factor in determining whether a point is inside a shape. The shape in Figure 14-21 can be drawn using strokes in different directions. Figure 14-22 shows two of them. In Shape-1, both triangles use counterclockwise strokes. In Shape-2, one triangle uses a counterclockwise stroke, and another uses a clockwise stroke.
The fill rule of a Path draws rays from the point to infinity, so they can intersect all path segments. In the NON_ZERO fill rule, if the number of path segments intersected by rays is equal in counterclockwise and clockwise directions, the point is outside the shape. Otherwise, the point is inside the shape. You can understand this rule by using a counter, which starts with zero. Add one to the counter for every ray intersecting a path segment in the counterclockwise direction. Subtract one from the counter for every ray intersecting a path segment in the clockwise direction. At the end, if the counter is nonzero, the point is inside; otherwise, the point is outside. Figure 14-23 shows the same two paths made of two triangular subpaths with their counter values when the NON_ZERO fill rule is applied. The rays drawn from the point are shown in dashed lines. The point in the first shape scores six (a nonzero value), and it is inside the path. The point in the second shape scores zero, and it is outside the path.
Like the NON_ZERO fill rule, the EVEN_ODD fill rule also draws rays from a point in all directions extending to infinity, so all path segments are intersected. It counts the number of intersections between the rays and the path segments. If the number is odd, the point is inside the path. Otherwise, the point is outside the path. If you set the fillRule property to EVEN_ODD for the two shapes shown in Figure 14-23, the point is outside the path for both shapes because the number of intersections between rays and path segments is six (an even number) in both cases. The default value for the fillRule property of a Path is FillRule.NON_ZERO.
The program in Listing 14-13 is an implementation of the examples discussed in this section. It draws four paths: the first two (counting from the left) with NON_ZERO fill rules and the last two with EVEN_ODD fill rules. Figure 14-24 shows the paths. The first and third paths use a counterclockwise stroke for drawing both triangular subpaths. The second and fourth paths are drawn using a counterclockwise stroke for one triangle and a clockwise stroke for another.
// PathFillRule.java
package com.jdojo.shape;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.FillRule;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.stage.Stage;
public class PathFillRule extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
// Both triangles use a counterclockwise stroke
PathElement[] pathElements1 = {
new MoveTo(50, 0),
new LineTo(0, 50),
new LineTo(100, 50),
new LineTo(50, 0),
new MoveTo(90, 15),
new LineTo(40, 65),
new LineTo(140, 65),
new LineTo(90, 15)};
// One triangle uses a clockwise stroke and
// another uses a counterclockwise stroke
PathElement[] pathElements2 = {
new MoveTo(50, 0),
new LineTo(0, 50),
new LineTo(100, 50),
new LineTo(50, 0),
new MoveTo(90, 15),
new LineTo(140, 65),
new LineTo(40, 65),
new LineTo(90, 15)};
/* Using the NON-ZERO fill rule by default */
Path p1 = new Path(pathElements1);
p1.setFill(Color.LIGHTGRAY);
Path p2 = new Path(pathElements2);
p2.setFill(Color.LIGHTGRAY);
/* Using the EVEN_ODD fill rule */
Path p3 = new Path(pathElements1);
p3.setFill(Color.LIGHTGRAY);
p3.setFillRule(FillRule.EVEN_ODD);
Path p4 = new Path(pathElements2);
p4.setFill(Color.LIGHTGRAY);
p4.setFillRule(FillRule.EVEN_ODD);
HBox root = new HBox(p1, p2, p3, p4);
root.setSpacing(10);
root.setStyle("""
-fx-padding: 10;
-fx-border-style: solid inside;
-fx-border-width: 2;
-fx-border-insets: 5;
-fx-border-radius: 5;
-fx-border-color: blue;""");
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Using Fill Rules for Paths");
stage.show();
}
}
Listing 14-13
Using Fill Rules for Paths
Drawing Scalable Vector Graphics
An instance of the SVGPath class draws a shape from path data in an encoded string. You can find the SVG specification at www.w3.org/TR/SVG. You can find the detailed rules of constructing the path data in string format at www.w3.org/TR/SVG/paths.html. JavaFX partially supports SVG specification.
The SVGPath class contains a no-args constructor to create its object:
// Create a SVGPath object
SVGPath sp = new SVGPath();
The SVGPath class contains two properties:
content
fillRule
The content property defines the encoded string for the SVG path. The fillRule property specifies the fill rule for the interior of the shape, which could be FillRule.NON_ZERO or FillRule.EVEN_ODD. The default value for the fillRule property is FillRule.NON_ZERO. Please refer to the section “The Fill Rule for a Path” for more details on fill rules. Fill rules for a Path and a SVGPath work the same.
The following snippet of code sets “M50, 0 L0, 50 L100, 50 Z” encoded string as the content for a SVGPath object to draw a triangle as shown in Figure 14-25:
SVGPath sp2 = new SVGPath();
sp2.setContent("M50, 0 L0, 50 L100, 50 Z");
sp2.setFill(Color.LIGHTGRAY);
sp2.setStroke(Color.BLACK);
The content of a SVGPath is an encoded string following some rules:
The string consists of a series of commands.
Each command name is exactly one letter long.
A command is followed by its parameters.
Parameter values for a command are separated by a comma or a space. For example, “M50, 0 L0, 50 L100, 50 Z” and “M50 0 L0 50 L100 50 Z” represent the same path. For readability, you will use a comma to separate two values.
You do not need to add spaces before or after the command character. For example, “M50 0 L0 50 L100 50 Z” can be rewritten as “M50 0L0 50L100 50Z”.
Let us consider the SVG content used in the previous example:
M50, 0 L0, 50 L100, 50 Z
The content consists of four commands:
M50, 0
L0, 50
L100, 50
Z
Comparing the SVG path commands with the Path API, the first command is “MoveTo (50, 0)”; the second command is “LineTo(0, 50)”; the third command is “LineTo(100, 50)”; and the fourth command is “ClosePath”.
Tip
The command name in SVGPath content is the first letter of the classes representing path elements in a Path object. For example, an absolute MoveTo in the Path API becomes M in SVGPath content, an absolute LineTo becomes L, and so on.
The parameters for the commands are coordinates, which can be absolute or relative. When the command name is in uppercase (e.g., M), its parameters are considered absolute. When the command name is in lowercase (e.g., m), its parameters are considered relative. The “closepath” command is Z or z. Because the “closepath” command does not take any parameters, both uppercase and lowercase versions behave the same.
Consider the content of two SVG paths:
M50, 0 L0, 50 L100, 50 Z
M50, 0 l0, 50 l100, 50 Z
The first path uses absolute coordinates. The second path uses absolute and relative coordinates. Like a Path, a SVGPath must start with a “moveTo” command, which must use absolute coordinates. If a SVGPath starts with a relative “moveTo” command (e.g., "m 50, 0"), its parameters are treated as absolute coordinates. In the foregoing SVG paths, you can start the string with "m50, 0", and the result will be the same.
The previous two SVG paths will draw two different triangles, as shown in Figure 14-26, even though both use the same parameters. The first path draws the triangle on the left, and the second one draws the triangle on the right. The commands in the second path are interpreted as follows:
Move to (50, 0).
Draw a line from the current point (50, 0) to (50, 50). The ending point (50, 50) is derived by adding the x and y coordinates of the current point to the relative “lineto” command (l) parameters. The ending point becomes (50, 50).
Draw a line from the current point (50, 50) to (150, 100). Again, the coordinates of the ending point are derived by adding the x and y coordinates of the current point (50, 50) to the command parameter “l100, 50” (the first character in “l100, 50” is the lowercase L, not the digit 1).
Then close the path (Z).
Table 14-2 lists the commands used in the content of the SVGPath objects. It also lists the equivalent classes used in the Path API. The table lists the command, which uses absolute coordinates. The relative versions of the commands use lowercase letters. The plus sign (+) in the parameter column indicates that multiple parameters may be used.
The “moveTo” commandM starts a new subpath at the specified (x, y) coordinates. It may be followed by one or multiple pairs of coordinates. The first pair of coordinates is considered the x and y coordinates of the point, which the command will make the current point. Each additional pair is treated as a parameter for a “lineto” command. If the “moveTo” command is relative, the “lineto” command will be relative. If the “moveTo” command is absolute, the “lineto” command will be absolute. For example, the following two SVG paths are the same:
M50, 0 L0, 50 L100, 50 Z
M50, 0, 0, 50, 100, 50 Z
The “lineto” Commands
There are three “lineto” commands: L, H, and V. They are used to draw straight lines.
The command L is used to draw a straight line from the current point to the specified (x, y) point. If you specify multiple pairs of (x, y) coordinates, it draws a polyline. The final pair of the (x, y) coordinate becomes the new current point. The following SVG paths will draw the same triangle. The first one uses two L commands, and the second one uses only one:
M50, 0 L0, 50 L100, 50 L50, 0
M50, 0 L0, 50, 100, 50, 50, 0
The H and V commands are used to draw horizontal and vertical lines from the current point. The command H draws a horizontal line from the current point (cx, cy) to (x, cy). The command V draws a vertical line from the current point (cx, cy) to (cx, y). You can pass multiple parameters to them. The final parameter value defines the current point. For example, “M0, 0H200, 100 V50Z” will draw a line from (0, 0) to (200, 0), from (200, 0) to (100, 0). The second command will make (100, 0) as the current point. The third command will draw a vertical line from (100, 0) to (100, 50). The z command will draw a line from (100, 50) to (0, 0). The following snippet of code draws a SVG path as shown in Figure 14-27:
The “arcto” commandA draws an elliptical arc from the current point to the specified (x, y) point. It uses rx and ry as the radii along x-axis and y-axis. The x-axis-rotation is a rotation angle in degrees for the x-axis of the ellipse. The large-arc-flag and sweep-flag are the flags used to select one arc out of four possible arcs. Use 0 and 1 for flag values, where 1 means true and 0 means false. Please refer to the section “The ArcTo Path Element” for a detailed explanation of all its parameters. You can pass multiple arcs parameters, and in that case, the ending point of an arc becomes the current point for the subsequent arc. The following snippet of code draws two SVG paths with arcs. The first path uses one parameter for the “arcTo” command, and the second path uses two parameters. Figure 14-28 shows the paths.
Both commandsQ and T are used to draw quadratic Bezier curves.
The command Q draws a quadratic Bezier curve from the current point to the specified (x, y) point using the specified (x1, y1) as the control point.
The command T draws a quadratic Bezier curve from the current point to the specified (x, y) point using a control point that is the reflection of the control point on the previous command. The current point is used as the control point if there was no previous command or the previous command was not Q, q, T, or t.
The command Q takes the control point as parameters, whereas the command T assumes the control point. The following snippet of code uses the commands Q and T to draw quadratic Bezier curves as shown in Figure 14-29:
SVGPath p1 = new SVGPath();
p1.setContent("M0, 50 Q50, 0, 100, 50");
p1.setFill(Color.LIGHTGRAY);
p1.setStroke(Color.BLACK);
SVGPath p2 = new SVGPath();
p2.setContent("M0, 50 Q50, 0, 100, 50 T200, 50");
p2.setFill(Color.LIGHTGRAY);
p2.setStroke(Color.BLACK);
The “Cubic Bezier curveto” Command
The commandsC and S are used to draw cubic Bezier curves.
The command C draws a cubic Bezier curve from the current point to the specified point (x, y) using the specified control points (x1, y1) and (x2, y2).
The command S draws a cubic Bezier curve from the current point to the specified point (x, y). It assumes the first control point to be the reflection of the second control point on the previous command. The current point is used as the first control point if there was no previous command or the previous command was not C, c, S, or s. The specified point (x2, y2) is the second control point. Multiple sets of coordinates draw a polybezier.
The following snippet of code uses the commands C and S to draw cubic Bezier curves as shown in Figure 14-30. The second path uses the command S to use the reflection of the second control point of the previous command C as its first control point:
The “closepath” commandsZ and z draw a straight line from the current point to the starting point of the current subpath and end the subpath. Both uppercase and lowercase versions of the command work the same.
Combining Shapes
The Shape class provides three static methods that let you perform union, intersection, and subtraction of shapes:
union(Shape shape1, Shape shape2)
intersect(Shape shape1, Shape shape2)
subtract(Shape shape1, Shape shape2)
The methods return a new Shape instance. They operate on the areas of the input shapes. If a shape does not have a fill and a stroke, its area is zero. The new shape has a stroke and a fill. The union() method combines the areas of two shapes. The intersect() method uses the common areas between the shapes to create the new shape. The subtract() method creates a new shape by subtracting the specified second shape from the first shape.
The program in Listing 14-14 combines two circles using the union, intersection, and subtraction operations. Figure 14-31 shows the resulting shapes.
// CombiningShapesTest.java
package com.jdojo.shape;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Shape;
import javafx.stage.Stage;
public class CombiningShapesTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Circle c1 = new Circle (0, 0, 20);
Circle c2 = new Circle (15, 0, 20);
Shape union = Shape.union(c1, c2);
union.setStroke(Color.BLACK);
union.setFill(Color.LIGHTGRAY);
Shape intersection = Shape.intersect(c1, c2);
intersection.setStroke(Color.BLACK);
intersection.setFill(Color.LIGHTGRAY);
Shape subtraction = Shape.subtract(c1, c2);
subtraction.setStroke(Color.BLACK);
subtraction.setFill(Color.LIGHTGRAY);
HBox root = new HBox(union, intersection, subtraction);
root.setSpacing(20);
root.setStyle("""
-fx-padding: 10;
-fx-border-style: solid inside;
-fx-border-width: 2;
-fx-border-insets: 5;
-fx-border-radius: 5;
-fx-border-color: blue;""");
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Combining Shapes");
stage.show();
}
}
Listing 14-14
Combining Shapes to Create New Shapes
Understanding the Stroke of a Shape
Stroking is the process of painting the outline of a shape. Sometimes, the outline of a shape is also known as stroke. The Shape class contains several properties to define the appearance of the stroke of a shape:
stroke
strokeWidth
strokeType
strokeLineCap
strokeLineJoin
strokeMiterLimit
strokeDashOffset
The stroke property specifies the color of the stroke. The default stroke is set to null for all shapes except Line, Path, and Polyline, which have Color.BLACK as their default stroke.
The strokeWidth property specifies the width of the stroke. It is 1.0px by default.
The stroke is painted along the boundary of a shape. The strokeType property specifies the distribution of the width of the stroke on the boundary. Its value is one of the three constants, CENTERED, INSIDE, and OUTSIDE, of the StrokeType enum. The default value is CENTERED. The CENTERED stroke type draws a half of the stroke width outside and half inside the boundary. The INSIDE stroke type draws the stroke inside the boundary. The OUTSIDE stroke draws the stroke outside the boundary. The stroke width of a shape is included in its layout bounds.
The program in Listing 14-15 creates four rectangles as shown in Figure 14-32. All rectangles have the same width and height (50px and 50px). The first rectangle, counting from the left, has no stroke, and it has layout bounds of 50px X 50px. The second rectangle uses a stroke of width 4px and an INSIDE stroke type. The INSIDE stroke type is drawn inside the width and height boundary, and the rectangle has the layout bounds of 50px X 50px. The third rectangle uses a stroke width 4px and a CENTERED stroke type, which is the default. The stroke is drawn 2px inside the boundary and 2px outside the boundary. The 2px outside stroke is added to the dimensions of all four making the layout bounds to 54px X 54px. The fourth rectangle uses a 4px stroke width and an OUTSIDE stroke type. The entire stroke width falls outside the width and height of the rectangle making the layouts to 58px X 58px.
// StrokeTypeTest.java
package com.jdojo.shape;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;
public class StrokeTypeTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Rectangle r1 = new Rectangle(50, 50);
r1.setFill(Color.LIGHTGRAY);
Rectangle r2 = new Rectangle(50, 50);
r2.setFill(Color.LIGHTGRAY);
r2.setStroke(Color.BLACK);
r2.setStrokeWidth(4);
r2.setStrokeType(StrokeType.INSIDE);
Rectangle r3 = new Rectangle(50, 50);
r3.setFill(Color.LIGHTGRAY);
r3.setStroke(Color.BLACK);
r3.setStrokeWidth(4);
Rectangle r4 = new Rectangle(50, 50);
r4.setFill(Color.LIGHTGRAY);
r4.setStroke(Color.BLACK);
r4.setStrokeWidth(4);
r4.setStrokeType(StrokeType.OUTSIDE);
HBox root = new HBox(r1, r2, r3, r4);
root.setAlignment(Pos.CENTER);
root.setSpacing(10);
root.setStyle("""
-fx-padding: 10;
-fx-border-style: solid inside;
-fx-border-width: 2;
-fx-border-insets: 5;
-fx-border-radius: 5;
-fx-border-color: blue;""");
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Using Different Stroke Types for Shapes");
stage.show();
}
}
Listing 14-15
Effects of Applying Different Stroke Types on a Rectangle
The strokeLineCap property specifies the ending decoration of a stroke for unclosed subpaths and dash segments. Its value is one of the constants of the StrokeLineCap enum: BUTT, SQUARE, and ROUND. The default is BUTT. The BUTT line cap adds no decoration to the end of a subpath; the stroke starts and ends exactly at the starting and ending points. The SQUARE line cap extends the end by half the stroke width. The ROUND line cap adds a round cap to the end. The round cap uses a radius equal to half the stroke width. Figure 14-33 shows three lines, which are unclosed subpaths. All lines are 100px wide using 10px stroke width. The figure shows the strokeLineCap they use. The width of the layout bounds of the line using the BUTT line cap remains 100px. However, for other two lines, the width of the layout bounds increases to 110px—increasing by 10px at both ends.
Note that the strokeLineCap properties are applied to the ends of a line segment of unclosed subpaths. Figure 14-34 shows three triangles created by unclosed subpaths. They use different stroke line caps. The SVG path data “M50, 0L0, 50 M0, 50 L100, 50 M100, 50 L50, 0” was used to draw the triangles. The fill was set to null and the stroke width to 10px.
The strokeLineJoin property specifies how two successive path elements of a subpath are joined. Its value is one of the constants of the StrokeLineJoin enum: BEVEL, MITER, and ROUND. The default is MITER. The BEVEL line join connects the outer corners of path elements by a straight line. The MITER line join extends the outer edges of two path elements until they meet. The ROUND line join connects two path elements by rounding their corners by half the stroke width. Figure 14-35 shows three triangles created with the SVG path data “M50, 0L0, 50 L100, 50 Z”. The fill color is null, and the stroke width is 10px. The triangles use different line joins as shown in the figure.
A MITER line join joins two path elements by extending their outer edges. If the path elements meet at a smaller angle, the length of the join may become very big. You can limit the length of the join using the strokeMiterLimit property. It specifies the ratio of the miter length and the stroke width. The miter length is the distance between the most inside point and the most outside point of the join. If the two path elements cannot meet by extending their outer edges within this limit, a BEVEL join is used instead. The default value is 10.0. That is, by default, the miter length may be up to ten times the stroke width.
The following snippet of code creates two triangles as shown in Figure 14-36. Both use a MITER line join by default. The first triangle uses 2.0 as the miter limit. The second triangle uses the default miter limit, which is 10.0. The stroke width is 10px. The first triangle tries to join the corners by extending two lines up to 20px, which is computed by multiplying the 10px stroke width by the miter limit of 2.0. The corners cannot be joined using the MITER join within 20px, so a BEVEL join is used.
SVGPath t1 = new SVGPath();
t1.setContent("M50, 0L0, 50 L100, 50 Z");
t1.setStrokeWidth(10);
t1.setFill(null);
t1.setStroke(Color.BLACK);
t1.setStrokeMiterLimit(2.0);
SVGPath t2 = new SVGPath();
t2.setContent("M50, 0L0, 50 L100, 50 Z");
t2.setStrokeWidth(10);
t2.setFill(null);
t2.setStroke(Color.BLACK);
By default, the stroke draws a solid outline. You can also have a dashed outline. You need to provide a dashing pattern and a dash offset. The dashing pattern is an array of double that is stored in an ObservableList<Double>. You can get the reference of the list using the getStrokeDashArray() method of the Shape class. The elements of the list specify a pattern of dashes and gaps. The first element is the dash length, the second gap, the third dash length, the fourth gap, and so on. The dashing pattern is repeated to draw the outline. The strokeDashOffset property specifies the offset in the dashing pattern where the stroke begins.
The following snippet of code creates two instances of Polygon as shown in Figure 14-37. Both use the same dashing patterns but a different dash offset. The first one uses the dash offset of 0.0, which is the default. The stroke of the first rectangle starts with a 15.0px dash, which is the first element of the dashing pattern, which can be seen in the dashed line drawn from the (0, 0) to (100, 0). The second Polygon uses a dash offset of 20.0, which means the stroke will start 20.0px inside the dashing pattern. The first two elements 15.0 and 3.0 are inside the dash offset 20.0. Therefore, the stroke for the second Polygon starts at the third element, which is a 5.0px dash.
All shapes do not have a default style class name. If you want to apply styles to shapes using CSS, you need to add style class names to them. All shapes can use the following CSS properties:
-fx-fill
-fx-smooth
-fx-stroke
-fx-stroke-type
-fx-stroke-dash-array
-fx-stroke-dash-offset
-fx-stroke-line-cap
-fx-stroke-line-join
-fx-stroke-miter-limit
-fx-stroke-width
All CSS properties correspond to the properties in the Shape class, which I have discussed at length in the previous section. Rectangle supports two additional CSS properties to specify arc width and height for rounded rectangles:
-fx-arc-height
-fx-arc-width
The following snippet of code creates a Rectangle and adds rectangle as its style class name:
Rectangle r1 = new Rectangle(200, 50);
r1.getStyleClass().add("rectangle");
The following style will produce a rectangle as shown in Figure 14-38:
.rectangle {
-fx-fill: lightgray;
-fx-stroke: black;
-fx-stroke-width: 4;
-fx-stroke-dash-array: 15 5 5 10;
-fx-stroke-dash-offset: 20;
-fx-stroke-line-cap: round;
-fx-stroke-line-join: bevel;
}
Summary
Any shape that can be drawn in a two-dimensional plane is called a 2D shape. JavaFX offers various nodes to draw different types of shapes (lines, circles, rectangles, etc.). You can add shapes to a scene graph. All shape classes are in the javafx.scene.shape package. Classes representing 2D shapes are inherited from the abstract Shape class. A shape can have a stroke that defines the outline of the shape. A shape may have a fill.
An instance of the Line class represents a line node. A Line has no interior. By default, its fill property is set to null. Setting fill has no effect. The default stroke is Color.BLACK, and the default strokeWidth is 1.0.
An instance of the Rectangle class represents a rectangle node. The class uses six properties to define the rectangle: x, y, width, height, arcWidth, and arcHeight. The x and y properties are the x and y coordinates of the upper-left corner of the rectangle in the local coordinate system of the node. The width and height properties are the width and height of the rectangle, respectively. Specify the same width and height to draw a square. By default, the corners of a rectangle are sharp. A rectangle can have rounded corners by specifying the arcWidth and arcHeight properties.
An instance of the Circle class represents a circle node. The class uses three properties to define the circle: centerX, centerY, and radius. The centerX and centerY properties are the x and y coordinates of the center of the circle in the local coordinate system of the node. The radius property is the radius of the circle. The default values for these properties are zero.
An instance of the Ellipse class represents an ellipse node. The class uses four properties to define the ellipse: centerX, centerY, radiusX, radiusY. The centerX and centerY properties are the x and y coordinates of the center of the circle in the local coordinate system of the node. The radiusX and radiusY are the radii of the ellipse in the horizontal and vertical directions. The default values for these properties are zero. A circle is a special case of an ellipse when radiusX and radiusY are the same.
An instance of the Polygon class represents a polygon node. The class does not define any public properties. It lets you draw a polygon using an array of (x, y) coordinates defining the vertices of the polygon. Using the Polygon class, you can draw any type of geometric shape that is created using connected lines (triangles, pentagon, hexagon, parallelogram, etc.).
A polyline is similar to a polygon, except that it does not draw a line between the last and first points. That is, a polyline is an open polygon. However, the fill color is used to fill the entire shape as if the shape was closed. An instance of the Polyline class represents a polyline node.
An instance of the Arc class represents a sector of an ellipse. The class uses seven properties to define the ellipse: centerX, centerY, radiusX, radiusY, startAngle, length, and type. The first four properties define an ellipse. The last three properties define a sector of the ellipse that is the Arc node. The startAngle property specifies the start angle of the section in degrees measured counterclockwise from the positive x-axis. It defines the beginning of the arc. The length is an angle in degrees measured counterclockwise from the start angle to define the end of the sector. If the length property is set to 360, the Arc is a full ellipse.
Bezier curves are used in computer graphics to draw smooth curves. An instance of the QuadCurve class represents a quadratic Bezier curve segment intersecting two specified points using a specified Bezier control point.
An instance of the CubicCurve class represents a cubic Bezier curve segment intersecting two specified points using two specified Bezier control points.
You can draw complex shapes using the Path class. An instance of the Path class defines the path (outline) of a shape. A path consists of one or more subpaths. A subpath consists of one or more path elements. Each subpath has a starting point and an ending point. A path element is an instance of the PathElement abstract class. Several subclasses of the PathElement class exist to represent a specific type of path elements; those classes are MoveTo, LineTo, HLineTo, VLineTo, ArcTo, QuadCurveTo, CubicCurveTo, and ClosePath.
JavaFX partially supports SVG specification. An instance of the SVGPath class draws a shape from path data in an encoded string.
JavaFX lets you create a shape by combining multiple shapes. The Shape class provides three static methods named union(), intersect(), and subtract() that let you perform union, intersection, and subtraction of two shapes that are passed as the arguments to these methods. The methods return a new Shape instance. They operate on the areas of the input shapes. If a shape does not have a fill and a stroke, its area is zero. The new shape has a stroke and a fill. The union() method combines the areas of two shapes. The intersect() method uses the common areas between the shapes to create the new shape. The subtract() method creates a new shape by subtracting the specified second shape from the first shape.
Stroking is the process of painting the outline of a shape. Sometimes, the outline of a shape is also known as stroke. The Shape class contains several properties such as stroke, strokeWidth, and so on to define the appearance of the stroke of a shape.
JavaFX lets you style 2D shapes with CSS.
The next chapter will discuss how to handle text drawing.