This is a simple Java program that draws a fractal tree by using either lines or filled polygons. It contains two JFrames, the first one for drawing the shape, and the second one for changing some control parameters.

## Introduction

This is a simple Java program that draws a fractal tree by using either lines (classic fractal tree) or filled polygons ("upgraded" fractal tree). I got the idea from a Wikipedia page about fractals.

The tree is drawn in a way that for each iteration, each shape from the previous iteration is split into two new shapes, always at the same angle, and the length and thickness are multiplied by a factor in each iteration.

The program consists of two `JFrame`

s.

The first one is where the drawing happens; it is controlled from the `main`

method - the redrawing is done in an infinite loop which is conditioned by some parameters, and it is calling the `paint`

method to draw the tree.

Here are some screenshots of the tree:

There is also the second `JFrame`

- this one contains components like `JRadioButton`

, `JTextBox`

and `JCheckBox`

. All of these components accept inputs for the parameters that control the way the tree is drawn.

## Usage

### The Control Frame Parameters

There are altogether 11 parameters that control the drawing of the tree. All the parameters that get changed will be applied as soon as the current drawing is complete; if the tree is already fully drawn, then a new drawing loop will be started, using the changed parameters immediately upon the change.

**Repeat** - determines whether the tree should be redrawn in a loop indefinitely, or drawn just once (and redrawn only upon eventual change of the parameters) **Shape** - determines the basic shape used for drawing the tree (line or polygon) **Color** - opens a `JColorChooser`

control - it affects the color of the tree (only for polygons) **Iterations** - determines the depth level of the algorithm (how many levels of branches is drawn) **Thickness (base)** - thickness of the first (base) branch of the tree **Thickness factor** - factor by which the thickness of each next level of branches is multiplied (will accept > 1, but it is advisable to use < 1) **Length (base)** - length of the first (base) branch of the tree **Length factor** - factor by which the length of each next level of branches is multiplied (will accept > 1, but it should be < 1) **Split angle °** - angle (in degrees) by which each branch is split into the next two branches **Sleep after each shape** - means that the algorithm will pause for the entered number of ms before each next shape **Sleep after each iteration** - means that the algorithm will pause for the entered number of ms before each next iteration

### Setting the Parameters

In the default setup (after opening the app), the tree will be drawn with lines, in a loop. The splitting angle will be 60°, and the tree will be drawn in 10 iterations (levels of branches), with each next level's lines shortened by the factor of 0.8. The default speed of the drawing will be set to 500ms pause between iterations.

If you choose the "`No`

" option on Repeat, the tree will finish drawing, and will not redraw any further. While in this state, the tree will be redrawn only if any of the parameters are changed.

If you wish to play around with the parameters, note that the program will immediately pick up the change after the whole tree is drawn. There is no "confirmation" of change (i.e., "`lostfocus`

" event or "`apply`

" button).

There is no limitation to the input into textboxes; if an invalid value is entered (i.e., NaN value), then the change will not be taken into consideration, until valid text is entered. Be careful of what numbers you are putting into the `textbox`

es because the tree can become distorted if some implied logical limitations are not considered:

- Very high number of iterations could slow down the app and will result in too short branches once it reaches higher iterations.
- Take into consideration the size of the frame (600x600) when choosing base thickness and length.
- Thickness and length factor should be kept under 1.
- Don't turn off both sleeps as it will result in tree being drawn so fast it would become invisible.

## The Code

Both `JFrame`

s are drawn from the `main`

method, but the control frame is built from a custom class `MyControlFrame`

which extends the `JFrame`

class. All the components of this frame, including the action listeners, are instantiated inside its constructor method.

The `MyControlFrame`

class contains `private`

variables that hold the control parameters used in the drawing algorithm. These variables are both populated and pulled from the `main`

method by calling the frame's `public`

getter methods:

int iter = 10;
int thick = 50;
double thickF = 0.6;
int len = 100;
double lenF = 0.8;
double angle = 60;
int sleepEachShapeMillis = 20;
int sleepEachIterMillis = 500;

i.e., method for getting the angle:

public double getAngleChoice()
{
try
{
angle = Double.parseDouble(txtAngle.getText());
}
catch(NumberFormatException ex)
{
}
return angle;
}

### The Main Loop

After creating both frames, the program runs an infinite loop, in which all of the variables are constantly read from the control frame, and checked against the previous value after each iteration. If there was any change in the parameters, the `boolean isChanged`

shall be set, telling the program that in the next iteration, the tree should be redrawn with the new parameters. If the parameter repeat is set to "`Yes`

", then the tree will be redrawn in every iteration of the main loop.

boolean isChanged = true;
repeat = control.getRepeatChoice();
shape = control.getShapeChoice();
color = control.getColorChoice();
iter = control.getIterChoice();
thick = control.getThickChoice();
thickFactor = control.getThickFChoice();
len = control.getLenChoice();
lenFactor = control.getLenFChoice();
angle = control.getAngleChoice();
sleepEachShapeMillis = control.getSleepEachShapeChoice();
sleepEachIterMillis = control.getSleepEachIterChoice();
while(true)
{
if((repeat == RepeatEnum.YES) || isChanged)
{
try {
frame.repaint();
paint(panel, shape, color, iter, thick, thickFactor,
len, lenFactor, angle, sleepEachShapeMillis, sleepEachIterMillis);
Thread.sleep(10);
} catch (Exception e) {
break;
}
isChanged = false;
}
repeat = control.getRepeatChoice();
shapeNEW = control.getShapeChoice();
colorNEW = control.getColorChoice();
iterNEW = control.getIterChoice();
thickNEW = control.getThickChoice();
thickFactorNEW = control.getThickFChoice();
lenNEW = control.getLenChoice();
lenFactorNEW = control.getLenFChoice();
angleNEW = control.getAngleChoice();
sleepEachShapeMillisNEW = control.getSleepEachShapeChoice();
sleepEachIterMillisNEW = control.getSleepEachIterChoice();
if(shapeNEW != shape || colorNEW != color || iterNEW != iter ||
thick != thickNEW || thickFactor != thickFactorNEW || len != lenNEW ||
lenFactor != lenFactorNEW || angle != angleNEW ||
sleepEachShapeMillis != sleepEachShapeMillisNEW ||
sleepEachIterMillis != sleepEachIterMillisNEW)
isChanged = true;
shape = shapeNEW;
color = colorNEW;
iter = iterNEW;
thick = thickNEW;
thickFactor = thickFactorNEW;
len = lenNEW;
lenFactor = lenFactorNEW;
angle = angleNEW;
sleepEachShapeMillis = sleepEachShapeMillisNEW;
sleepEachIterMillis = sleepEachIterMillisNEW;
}

### paint Method

This method redraws the tree in the main frame. It takes as argument the `JPanel`

component from the main frame, in which the tree is drawn, and it also takes all the parameters from the control frame (10 in total).

public static void paint(JPanel panel, ShapeEnum shape, Color color, int maxIter,
int thickness, double thickFactor, int length, double lenFactor, double angle,
int sleepEachShapeMillis, int sleepEachIterMillis);

The method first retrieves the graphics context from the panel, and applies some rendering hints to make the drawing nicer:

Graphics2D g2d = (Graphics2D) panel.getGraphics();
RenderingHints rh = new RenderingHints(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
);
g2d.addRenderingHints(rh);
g2d.setColor(color);

Two arraylists are used to hold a collection of last points (of the current branch level - iteration) and also a collection of last angles. The angles in this context mark the angle between each line of the last iteration and the ordinate (vertical) line.

Before the loop, the initial line (or polygon) is drawn, with the initial parameters. (For the polygon tree, the initial polygon is slightly different than the other polygons (it is a rectangle instead of trapesoid).)

Then, a `for`

loop is entered which will run through the set number of iterations.

First, length of the line/polygon is adjusted.

length *= lenFactor;

Next, all the last points are read in a `while`

loop, and for each of these points, two additional lines/polygons will be drawn - one in positive, and one in negative direction. To achieve this, an angle from the angle `ArrayList`

(`theta`

) corresponding to the current point will be increased (or decreased) by `delta`

, which is half of the splitting angle. (If you take a look at the picture above, you can see that for an angle of 60°, each consequent branch is added (or deducted) 30° delta.)

theta = (int)(tmpThetas.get(tmpPoints.indexOf(pt)) + Math.pow(direction, i) * delta);

At this point, a line, or a polygon is drawn.

The `x`

and `y`

parameters for the upper point of the line are calculated using the Pythagoras' theorem; since we know the coordinates of the point vertically up, and the angle by which we need to rotate it:

- Δx = length * cos(90°-theta) = length * sin(theta)
- Δy = length * sin(90°-theta) = length * cos(theta)
- x' = x + Δx = x + length * sin(theta)
- y' = y - Δy = y - length * cos(theta)

In case of polygon, the points that are saved in each iteration are the points in the middle of the upper side of the polygon:

All the four points of the polygon are calculated by applying rotation to upper middle point (like with the line), and then translating upper and lower middle point to the "left" or to the "right" for `thickness / 2`

. The thickness of each polygon is additionally reduced in the final transform of each upper point to get a trapezoid shape.

rect.xpoints[0] = (int)(pt.x + length *
Math.sin(Math.toRadians(theta)));
rect.xpoints[0] -= (int)((thickness / 2) *
Math.sin(Math.toRadians(90 - theta)));
rect.xpoints[0] += (int)((thickness * (1 - thickFactor) / 2) *
Math.sin(Math.toRadians(90 - theta)));
rect.ypoints[0] = (int)(pt.y - length *
Math.cos(Math.toRadians(theta)));
rect.ypoints[0] -= (int)((thickness / 2) *
Math.cos(Math.toRadians(90 - theta)));
rect.ypoints[0] += (int)((thickness * (1 - thickFactor) / 2) *
Math.cos(Math.toRadians(90 - theta)));
rect.xpoints[1] = pt.x - (int)((thickness / 2) *
Math.sin(Math.toRadians(90 - theta)));
rect.ypoints[1] = pt.y - (int)((thickness / 2) *
Math.cos(Math.toRadians(90 - theta)));
rect.xpoints[2] = pt.x + (int)((thickness / 2) *
Math.sin(Math.toRadians(90 - theta)));
rect.ypoints[2] = pt.y + (int)((thickness / 2) *
Math.cos(Math.toRadians(90 - theta)));
rect.xpoints[3] = (int)(pt.x + length *
Math.sin(Math.toRadians(theta)));
rect.xpoints[3] += (int)((thickness / 2) *
Math.sin(Math.toRadians(90 - theta)));
rect.xpoints[3] -= (int)((thickness * (1 - thickFactor) / 2) *
Math.sin(Math.toRadians(90 - theta)));
rect.ypoints[3] = (int)(pt.y - length *
Math.cos(Math.toRadians(theta)));
rect.ypoints[3] += (int)((thickness / 2) *
Math.cos(Math.toRadians(90 - theta)));
rect.ypoints[3] -= (int)((thickness * (1 - thickFactor) / 2) *
Math.cos(Math.toRadians(90 - theta)));
rect.invalidate();

In the last step, thickness is multiplied by the thickness reduction factor.

thickness *= thickFactor;

Here is the complete code of this method:

public static void paint(JPanel panel, ShapeEnum shape, Color color,
int maxIter, int thickness, double thickFactor, int length, double lenFactor,
double angle, int sleepEachShapeMillis, int sleepEachIterMillis) {
Graphics2D g2d = (Graphics2D) panel.getGraphics();
RenderingHints rh = new RenderingHints(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
);
g2d.addRenderingHints(rh);
g2d.setColor(color);
int w = panel.getSize().width;
int h = panel.getSize().height;
boolean sleepEachShape = sleepEachShapeMillis == 0 ? false : true;
boolean sleepEachIter = sleepEachIterMillis == 0 ? false : true;
double delta, theta;
Line2D line = new Line2D.Double();
Polygon rect = new Polygon();
Point pt = new Point();
Point pt2;
ArrayList<Point> startPoints, tmpPoints;
ArrayList<Double> thetas, tmpThetas;
Iterator<Point> listIterator;
int direction = -1;
delta = angle / 2;
theta = 0;
startPoints = new ArrayList<Point>();
tmpPoints = new ArrayList<Point>();
thetas = new ArrayList<Double>();
tmpThetas = new ArrayList<Double>();
if(sleepEachIter)
try {
Thread.sleep(sleepEachIterMillis);
} catch (InterruptedException e) {
e.printStackTrace();
}
rect.npoints = 4;
rect.xpoints = new int[4];
rect.ypoints = new int[4];
rect.xpoints[0] = w / 2 - thickness / 2;
rect.xpoints[1] = w / 2 + thickness / 2;
rect.xpoints[2] = w / 2 + thickness / 2;
rect.xpoints[3] = w / 2 - thickness / 2;
rect.ypoints[0] = h;
rect.ypoints[1] = h;
rect.ypoints[2] = (int)(h - length);
rect.ypoints[3] = (int)(h - length);
if(shape == ShapeEnum.LINE)
{
line.setLine(w / 2, h, w / 2, h - length);
g2d.drawLine((int)line.getX1(),
(int)line.getY1(), (int)line.getX2(), (int)line.getY2());
}
else
{
g2d.fillPolygon(rect);
}
startPoints.add(new Point(w / 2, (int)(h - length)));
thetas.add(theta);
direction = -1;
for(int iteration = 0; iteration < maxIter; iteration++)
{
if(sleepEachIter)
try {
Thread.sleep(sleepEachIterMillis);
} catch (InterruptedException e) {
e.printStackTrace();
}
length *= lenFactor;
tmpPoints = startPoints;
tmpThetas = thetas;
startPoints = new ArrayList<Point>();
thetas = new ArrayList<Double>();
listIterator = tmpPoints.iterator();
while(listIterator.hasNext())
{
pt = listIterator.next();
rect = new Polygon();
line = new Line2D.Double();
rect.npoints = 4;
for(int i = 0; i < 2; i++)
{
theta = (int)(tmpThetas.get(tmpPoints.indexOf(pt)) +
Math.pow(direction, i) * delta);
if(shape == ShapeEnum.LINE)
{
pt2 = new Point();
pt2.x = (int)(pt.x + length * Math.sin(Math.toRadians(theta)));
pt2.y = (int)(pt.y - length * Math.cos(Math.toRadians(theta)));
line.setLine(pt.x, pt.y, pt2.x, pt2.y);
}
else
{
rect.xpoints = new int[4];
rect.ypoints = new int[4];
rect.xpoints[0] = (int)(pt.x + length *
Math.sin(Math.toRadians(theta)));
rect.xpoints[0] -= (int)((thickness / 2) *
Math.sin(Math.toRadians(90 - theta)));
rect.xpoints[0] += (int)((thickness * (1 - thickFactor) / 2) *
Math.sin(Math.toRadians(90 - theta)));
rect.ypoints[0] = (int)(pt.y - length *
Math.cos(Math.toRadians(theta)));
rect.ypoints[0] -= (int)((thickness / 2) *
Math.cos(Math.toRadians(90 - theta)));
rect.ypoints[0] += (int)((thickness * (1 - thickFactor) / 2) *
Math.cos(Math.toRadians(90 - theta)));
rect.xpoints[1] = pt.x - (int)((thickness / 2) *
Math.sin(Math.toRadians(90 - theta)));
rect.ypoints[1] = pt.y - (int)((thickness / 2) *
Math.cos(Math.toRadians(90 - theta)));
rect.xpoints[2] = pt.x + (int)((thickness / 2) *
Math.sin(Math.toRadians(90 - theta)));
rect.ypoints[2] = pt.y + (int)((thickness / 2) *
Math.cos(Math.toRadians(90 - theta)));
rect.xpoints[3] = (int)(pt.x + length *
Math.sin(Math.toRadians(theta)));
rect.xpoints[3] += (int)((thickness / 2) *
Math.sin(Math.toRadians(90 - theta)));
rect.xpoints[3] -= (int)((thickness * (1 - thickFactor) / 2) *
Math.sin(Math.toRadians(90 - theta)));
rect.ypoints[3] = (int)(pt.y - length *
Math.cos(Math.toRadians(theta)));
rect.ypoints[3] += (int)((thickness / 2) *
Math.cos(Math.toRadians(90 - theta)));
rect.ypoints[3] -= (int)((thickness * (1 - thickFactor) / 2) *
Math.cos(Math.toRadians(90 - theta)));
rect.invalidate();
}
startPoints.add(new Point(
(int)(pt.x + length * Math.sin(Math.toRadians(theta))),
(int)(pt.y - length * Math.cos(Math.toRadians(theta)))
));
thetas.add(theta);
if(sleepEachShape)
try {
Thread.sleep(sleepEachShapeMillis);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(shape == ShapeEnum.LINE)
{
g2d.drawLine((int)line.getX1(), (int)line.getY1(),
(int)line.getX2(), (int)line.getY2());
}
else
{
g2d.fillPolygon(rect);
g2d.draw(rect);
}
}
}
thickness *= thickFactor;
}
}

## History

- 3
^{rd} June, 2020: Initial version