Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Java

Fractal Tree with Lines/Polygons Written in Java

4.50/5 (5 votes)
3 Jun 2020CPOL7 min read 12K   170  
An algorithm based on geometric transformations to draw a simple fractal tree
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 JFrames.

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:

Image 1Image 2

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.

Image 3

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 textboxes 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 JFrames 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:

Java
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:

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

Java
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();
// (re)draw in loop
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) {
            // TODO Auto-generated catch block
            break;
            //e.printStackTrace();
        }
        isChanged = false;
    }
    repeat = control.getRepeatChoice();
    // check if any control input was changed
    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).

Java
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:

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

Image 4

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.

Java
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.)

Java
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:

Image 5

  • Δ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:

Image 6

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.

Java
// create rectangle
// top left point
rect.xpoints[0] = (int)(pt.x + length * 
                  Math.sin(Math.toRadians(theta)));         // rotate
rect.xpoints[0] -= (int)((thickness / 2) * 
                   Math.sin(Math.toRadians(90 - theta)));   // translate
rect.xpoints[0] += (int)((thickness * (1 - thickFactor) / 2) * 
                   Math.sin(Math.toRadians(90 - theta)));   // reduce thickness
rect.ypoints[0] = (int)(pt.y - length * 
                  Math.cos(Math.toRadians(theta)));         // rotate
rect.ypoints[0] -= (int)((thickness / 2) * 
                   Math.cos(Math.toRadians(90 - theta)));   // translate
rect.ypoints[0] += (int)((thickness * (1 - thickFactor) / 2) * 
                   Math.cos(Math.toRadians(90 - theta)));   // reduce thickness
// bottom left point
rect.xpoints[1] = pt.x - (int)((thickness / 2) * 
                  Math.sin(Math.toRadians(90 - theta)));    // translate
rect.ypoints[1] = pt.y - (int)((thickness / 2) * 
                  Math.cos(Math.toRadians(90 - theta)));    // translate
// bottom right point
rect.xpoints[2] = pt.x + (int)((thickness / 2) * 
                  Math.sin(Math.toRadians(90 - theta)));    // translate
rect.ypoints[2] = pt.y + (int)((thickness / 2) * 
                  Math.cos(Math.toRadians(90 - theta)));    // translate
// top right point
rect.xpoints[3] = (int)(pt.x + length * 
                  Math.sin(Math.toRadians(theta)));         // rotate
rect.xpoints[3] += (int)((thickness / 2) * 
                   Math.sin(Math.toRadians(90 - theta)));   // translate
rect.xpoints[3] -= (int)((thickness * (1 - thickFactor) / 2) * 
                   Math.sin(Math.toRadians(90 - theta)));   // reduce thickness
rect.ypoints[3] = (int)(pt.y - length * 
                  Math.cos(Math.toRadians(theta)));         // rotate
rect.ypoints[3] += (int)((thickness / 2) * 
                   Math.cos(Math.toRadians(90 - theta)));   // translate
rect.ypoints[3] -= (int)((thickness * (1 - thickFactor) / 2) * 
                   Math.cos(Math.toRadians(90 - theta)));   // reduce thickness
rect.invalidate();

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

Java
thickness *= thickFactor;

Here is the complete code of this method:

Java
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) {
 
        // Retrieve the graphics context
    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>();
    
    // draw initial rectangle
    if(sleepEachIter)
        try {
            Thread.sleep(sleepEachIterMillis);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            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) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        length *= lenFactor;
        tmpPoints = startPoints;
        tmpThetas = thetas;
        startPoints = new ArrayList<Point>();
        thetas = new ArrayList<Double>();
        listIterator = tmpPoints.iterator();
        while(listIterator.hasNext())     // for all starting points
        {
            pt = listIterator.next();
            rect = new Polygon();
            line = new Line2D.Double();
            rect.npoints = 4;
            for(int i = 0; i < 2; i++)    // for both directions
            {
                // raise the rotation angle from previous rotation by delta
                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];
                    
                    // create rectangle
                    // top left point
                    rect.xpoints[0] = (int)(pt.x + length * 
                                      Math.sin(Math.toRadians(theta)));       // rotate
                    rect.xpoints[0] -= (int)((thickness / 2) * 
                                      Math.sin(Math.toRadians(90 - theta)));  // translate
                    rect.xpoints[0] += (int)((thickness * (1 - thickFactor) / 2) * 
                                       Math.sin(Math.toRadians(90 - theta))); // reduce 
                                                                              // thickness
                    rect.ypoints[0] = (int)(pt.y - length * 
                                       Math.cos(Math.toRadians(theta)));      // rotate
                    rect.ypoints[0] -= (int)((thickness / 2) * 
                                       Math.cos(Math.toRadians(90 - theta))); // translate
                    rect.ypoints[0] += (int)((thickness * (1 - thickFactor) / 2) * 
                                       Math.cos(Math.toRadians(90 - theta))); // reduce 
                                                                              // thickness
                    // bottom left point
                    rect.xpoints[1] = pt.x - (int)((thickness / 2) * 
                                      Math.sin(Math.toRadians(90 - theta)));  // translate
                    rect.ypoints[1] = pt.y - (int)((thickness / 2) * 
                                      Math.cos(Math.toRadians(90 - theta)));  // translate
                    // bottom right point
                    rect.xpoints[2] = pt.x + (int)((thickness / 2) * 
                                      Math.sin(Math.toRadians(90 - theta)));  // translate
                    rect.ypoints[2] = pt.y + (int)((thickness / 2) * 
                                      Math.cos(Math.toRadians(90 - theta)));  // translate
                    // top right point
                    rect.xpoints[3] = (int)(pt.x + length * 
                                       Math.sin(Math.toRadians(theta)));      // rotate
                    rect.xpoints[3] += (int)((thickness / 2) * 
                                       Math.sin(Math.toRadians(90 - theta))); // translate
                    rect.xpoints[3] -= (int)((thickness * (1 - thickFactor) / 2) * 
                                       Math.sin(Math.toRadians(90 - theta))); // reduce 
                                                                              // thickness
                    rect.ypoints[3] = (int)(pt.y - length * 
                                      Math.cos(Math.toRadians(theta)));       // rotate
                    rect.ypoints[3] += (int)((thickness / 2) * 
                                       Math.cos(Math.toRadians(90 - theta))); // translate
                    rect.ypoints[3] -= (int)((thickness * (1 - thickFactor) / 2) * 
                                       Math.cos(Math.toRadians(90 - theta))); // reduce 
                                                                              // thickness
                    rect.invalidate();
                }
                
                // save the new starting point
                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) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                if(shape == ShapeEnum.LINE)
                {
                    g2d.drawLine((int)line.getX1(), (int)line.getY1(), 
                                 (int)line.getX2(), (int)line.getY2());
                }
                else
                {
                    // draw the rectangle
                    g2d.fillPolygon(rect);
                    g2d.draw(rect);
                }
            }
        }
        thickness *= thickFactor; // change thickness
    }
}

History

  • 3rd June, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)