JavaFX

JavaFX Complex Shape Example

This is a JavaFX Complex Shape Example. 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.
 
 
 
 
 
 
 
 

 
The following table shows an overview of the whole article:

The following examples use Java SE 7 and JavaFX 2.2.

1. The Path Class

1.1 The Code

FxComplexShapeExample1.java

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 FxComplexShapeExample1 extends Application
{
	public static void main(String[] args) 
	{
		Application.launch(args);
	}
	
	@Override
	public void start(final Stage stage) 
	{
		// Create a Triangle
		Path triangle = new Path(new MoveTo(0, 0),
				new LineTo(0, 50),
				new LineTo(50, 50),
				new ClosePath());
				
		// Create a Star
		Path star = new Path();
				star.getElements().addAll(new MoveTo(30, 0),
				new LineTo(0, 30),
				new LineTo(60, 30),
				new ClosePath(),
				new MoveTo(0, 10),
				new LineTo(60, 10),
				new LineTo(30, 40),
				new ClosePath()
				
		// Create teh HBox
		HBox root = new HBox(triangle, star);
		root.setSpacing(10);

		// Set the Style of the HBox
		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;");

		// Create the Scene
		Scene scene = new Scene(root);
		// Add the Scene to the Stage
		stage.setScene(scene);
		// Set the Title of the Stage
		stage.setTitle("A Path Example");
		// Display the Stage
		stage.show();
	}
}

A path element is an instance of the PathElement abstract class. The following subclasses of the PathElement class exist to represent specific type of path elements:

  • MoveTo
  • LineTo
  • HLineTo
  • VLineTo
  • ArcTo
  • QuadCurveTo
  • CubicCurveTo
  • ClosePath

The Path class contains three constructors:

  • Path()
  • Path(Collection 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.

1.2 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);

1.3 The LineTo Path Element

A LineTo path element draws a straight line from the current point to the specified point. It contains two double properties 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:

Path triangle = new Path(new MoveTo(0, 0),
new LineTo(0, 50),
new LineTo(50, 50),
new LineTo(0, 0));

1.4 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.

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());

1.5 The GUI

The following image shows the result of the above program. One triangle and one with two inverted triangles to give it a look of a star:

A JavaFX Path Example
A JavaFX Path Example

2. Using other Path Elements

2.1 The Code

FxComplexShapeExample2.java

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 FxComplexShapeExample2 extends Application
{
	private ArcTo arcTo;
	
	public static void main(String[] args) 
	{
		Application.launch(args);
	}
	
	@Override
	public void start(final 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);

		// Create the BorderPane
		BorderPane root = new BorderPane();
		root.setTop(this.getTopPane());
		root.setCenter(path);
		
		// Set the Style of the BorderPane
		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;");		

		// Create the Scene
		Scene scene = new Scene(root);
		// Add the Scene to the Stage
		stage.setScene(scene);
		// Set the Title of the Stage
		stage.setTitle("An ArcTo Path Example");
		// Display the Stage
		stage.show();
	}
	
	// Create the GridPane
	private GridPane getTopPane() 
	{
		// Create the CheckBoxes
		CheckBox largeArcFlagCbx = new CheckBox("largeArcFlag");
		CheckBox sweepFlagCbx = new CheckBox("sweepFlag");

		// CReate the Sliders
		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());
		
		// Create the GridPane
		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;
	}	
}

2.2 The HLineTo Path Element

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);

2.3 The VLineTo Path Element

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);

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());

2.4 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.

The following code snippet creates a path with an ArcTo path element:

// 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);

2.5 The GUI

The above program uses an ArcTo path element to build a Path object. The program lets the user change properties of the ArcTo path element:

A JavaFX ArcTo PathElement Example
A JavaFX ArcTo PathElement Example

3. The PathElement Class

3.1 The Code

FxComplexShapeExample3.java

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 FxComplexShapeExample3 extends Application
{
	public static void main(String[] args) 
	{
		Application.launch(args);
	}
	
	@Override
	public void start(final Stage stage) 
	{
		// Create the PathElements
		
		// Both triangles use a counterclockwise stroke
		PathElement[] pathEleemnts1 = {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[] pathEleemnts2 = {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)};
		
		// Create the Path
		
		// Using the NON-ZERO fill rule by default
		Path path1 = new Path(pathEleemnts1);
		path1.setFill(Color.LIGHTGRAY);
		
		Path path2 = new Path(pathEleemnts2);
		path2.setFill(Color.LIGHTGRAY);
		
		// Using the EVEN_ODD fill rule
		Path path3 = new Path(pathEleemnts1);
		path3.setFill(Color.LIGHTGRAY);
		path3.setFillRule(FillRule.EVEN_ODD);
		
		Path path4 = new Path(pathEleemnts2);
		path4.setFill(Color.LIGHTGRAY);
		path4.setFillRule(FillRule.EVEN_ODD);

		// Create the HBox
		HBox root = new HBox(path1, path2, path3, path4);
		root.setSpacing(10);
		
		// Set the Style of the HBox
		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;");

		// Create the Scene
		Scene scene = new Scene(root);
		// Add the Scene to the Stage
		stage.setScene(scene);
		// Set the Title of the Stage
		stage.setTitle("A Fill Rule Example");
		// Display the Stage
		stage.show();
	}
}

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.

3.2 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.

The direction of the stroke is the vital factor in determining whether a point is inside a shape. 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 non-zero, the point is inside. Otherwise, the point is outside.

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.

The following code snippet creates thow paths with the fill rule EVEN_ODD.

/* Using the EVEN_ODD fill rule */
Path path3 = new Path(pathEleemnts1);
path3.setFill(Color.LIGHTGRAY);
path3.setFillRule(FillRule.EVEN_ODD);

Path path4 = new Path(pathEleemnts2);
path4.setFill(Color.LIGHTGRAY);
path4.setFillRule(FillRule.EVEN_ODD);

3.3 The GUI

The following image shows paths using different fill rules:

A JavaFX Fill Rule Example
A JavaFX Fill Rule Example

4. Combining Shapes

4.1 The Code

FxComplexShapeExample4.java

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 FxComplexShapeExample4 extends Application
{
	public static void main(String[] args) 
	{
		Application.launch(args);
	}
	
	@Override
	public void start(final Stage stage) 
	{
		// Create the Circles
		Circle circle1 = new Circle (0, 0, 20);
		Circle circle2 = new Circle (15, 0, 20);
		
		// Create the Shapes
		Shape union = Shape.union(circle1, circle2);
		union.setStroke(Color.BLACK);
		union.setFill(Color.LIGHTGRAY);
		
		Shape intersection = Shape.intersect(circle1, circle2);
		intersection.setStroke(Color.BLACK);
		intersection.setFill(Color.LIGHTGRAY);
		
		Shape subtraction = Shape.subtract(circle1, circle2);
		subtraction.setStroke(Color.BLACK);
		subtraction.setFill(Color.LIGHTGRAY);
		
		// Create the HBox
		HBox root = new HBox(union, intersection, subtraction);
		root.setSpacing(20);
		
		// Set the Style of the HBox
		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;");

		// Create the Scene
		Scene scene = new Scene(root);
		// Add the Scene to the Stage
		stage.setScene(scene);
		// Set the Title of the Stage
		stage.setTitle("A Combining Path Example");
		// Display the Stage
		stage.show();
	}
}

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 following code snippet shows an example of an intersection:

Shape intersection = Shape.intersect(circle1, circle2);
intersection.setStroke(Color.BLACK);
intersection.setFill(Color.LIGHTGRAY);

4.2 The GUI

The above program combines two circles using the union, intersection, and subtraction
operations. The following image shows the result:

A JavaFX Combining Path Example
A JavaFX Combining Path Example

5. Understanding the Stroke of a Shape

5.1 The Code

FxComplexShapeExample5.java

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 FxComplexShapeExample5 extends Application
{
	public static void main(String[] args) 
	{
		Application.launch(args);
	}
	
	@Override
	public void start(final Stage stage) 
	{
		// Create the Rectangles
		Rectangle rectangle1 = new Rectangle(50, 50);
		rectangle1.setFill(Color.LIGHTGRAY);
		
		Rectangle rectangle2 = new Rectangle(50, 50);
		rectangle2.setFill(Color.LIGHTGRAY);
		rectangle2.setStroke(Color.BLACK);
		rectangle2.setStrokeWidth(4);
		rectangle2.setStrokeType(StrokeType.INSIDE);
		
		Rectangle rectangle3 = new Rectangle(50, 50);
		rectangle3.setFill(Color.LIGHTGRAY);
		rectangle3.setStroke(Color.BLACK);
		rectangle3.setStrokeWidth(4);
		
		Rectangle rectangle4 = new Rectangle(50, 50);
		rectangle4.setFill(Color.LIGHTGRAY);
		rectangle4.setStroke(Color.BLACK);
		rectangle4.setStrokeWidth(4);
		rectangle4.setStrokeType(StrokeType.OUTSIDE);
		
		// Create the HBox
		HBox root = new HBox(rectangle1, rectangle2, rectangle3, rectangle4);
		root.setAlignment(Pos.CENTER);
		root.setSpacing(10);
		
		// Set the Style of the HBox
		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;");

		// Create the Scene
		Scene scene = new Scene(root);
		// Add the Scene to the Stage
		stage.setScene(scene);
		// Set the Title of the Stage
		stage.setTitle("A Stroke Type Example");
		// Display the Stage
		stage.show();
	}
}

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, 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 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.

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.

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.

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 code snippet creates a lightgray Rectangle with a black Stroke and a strokeWidth of 4px.

Rectangle rectangle3 = new Rectangle(50, 50);
rectangle3.setFill(Color.LIGHTGRAY);
rectangle3.setStroke(Color.BLACK);
rectangle3.setStrokeWidth(4);

5.2 The GUI

The above program creates four rectangles as shown in the following image. All rectangles have the same width and height.

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 third rectangle uses a stroke width 4px and a CENTERED stroke type, which is the default. The fourth rectangle uses a 4px stroke width and an OUTSIDE stroke type.

A JavaFX Stroke Type Example
A JavaFX Stroke Type Example

6. Download

This was an example of javafx.scene.shape

Download
You can download the full source code of this example here: JavaFxComplexShapeExample.zip

Andreas Pomarolli

Andreas has graduated from Computer Science and Bioinformatics at the University of Linz. During his studies he has been involved with a large number of research projects ranging from software engineering to data engineering and at least web engineering. His scientific focus includes the areas of software engineering, data engineering, web engineering and project management. He currently works as a software engineer in the IT sector where he is mainly involved with projects based on Java, Databases and Web Technologies.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button