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
- 3rd June, 2020: Initial version