Fractal Tree

Tags

Plotting Programming

Table of Contents


Motivation

In programming, recursion is a technique where you define a function (i.e., a task that a computer should perform) that contains itself as one of the tasks that should be executed when executing the function. This technique is widely used in programming, but many new programmers struggle to grasp how it works. Visual aids can often help one to understand such concepts, and in the case of recursion, they also tend to look quite pleasing. Therefore, I decided to visualize this concept by writing a piece of Python code that uses recursion to generate a fractal tree.

The Artwork

The artwork consists of two parts. The left side shows a fractal tree that is composed of two initial executions of the recursive function to generate both the root and the canopy of the tree. In the canopy, the recursion depth is visualized through different shades of green. The right side shows the corresponding Python code that can generate the exact tree shown on the left side as a vector graphics file in the SVG (Scalable Vector Graphics) file format.

Technical Details

The artwork is drawn on two DIN A3 canvases by a plotter that I built myself. A plotter is a CNC-machine that can move a drawing device, such as a pen, to draw on a canvas. CNC-machines that work in two dimensions are usually controlled based on vector graphics. In simple terms, vector graphics are images that are made up of lines defined by coordinates instead of pixels that are used in raster graphics.

The Python script I have written for this artwork can generate a vector graphics file that can be used to control my plotter. However, since the original code that I had written for this project had too many lines to fit onto the canvas, I had to shorten it significantly. For example, the original code used the group functionality of SVG files to group the different colors of the tree canopy. This made it easier to control the plotter by drawing each group with a different colored pen.

For plotting text, you want to use a single line font. However, since most operating systems and software do not support single line fonts, so you cannot just paste the text of the Python program into a vector graphics program such as Adobe Illustrator if you want to plot them. For this purpose, I have written another Python script that can generate single line paths for a given text and export them as an SVG file. I used that script to generate the right side of the artwork that shows the shortened code.

You can find both the shortened code and the original code in the appendix below. You are welcome to use and modify this code to generate your own fractal tree artworks. I would, thereby, recommend you to use the full version. For example, you could start by modifying the split angle and observing how it changes the tree. However, be aware that you need to have the svgwrite library (v. ~=1.4.3) installed in the environment that you want to execute the script in.

Appendix

Shortened-Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
__author__ = 'David Jilg'
import math
import svgwrite


def branch(prev_point, prev_angle, cur_length, cur_depth=0):
    if cur_depth <= max_depth:
        cur_depth += 1
        cur_length *= shrink_factor

        a1, a2 = prev_angle + split_angle, prev_angle - split_angle

        if root:
            p1 = (prev_point[0] + cur_length * math.cos(a1),
                  prev_point[1] + cur_length * math.sin(a1))

            p2 = (prev_point[0] + cur_length * math.cos(a2),
                  prev_point[1] + cur_length * math.sin(a2))
        else:
            p1 = (prev_point[0] - cur_length * math.cos(a1),
                  prev_point[1] - cur_length * math.sin(a1))

            p2 = (prev_point[0] - cur_length * math.cos(a2),
                  prev_point[1] - cur_length * math.sin(a2))

        draw_line(prev_point, p1, cur_depth)
        draw_line(prev_point, p2, cur_depth)

        branch(p1, a1, cur_length, cur_depth)
        branch(p2, a2, cur_length, cur_depth)


def draw_line(p1, p2, depth):
    if depth < 8:
        color = 'saddlebrown'
    elif depth < 10:
        color = 'darkolivegreen'
    elif depth < 12:
        color = 'forestgreen'
    else:
        color = 'yellowgreen'
    line = svg_doc.line(start=mm_to_pixel(p1), end=mm_to_pixel(p2),
                        stroke=color, stroke_width='0.75')
    svg_doc.add(line)


def mm_to_pixel(p, dpi=72):
    return (dpi * p[0] / 25.4), (dpi * p[1] / 25.4)


svg_doc = svgwrite.Drawing(filename='fractal_tree.svg',
                           size=(708, 1048))
split_angle = 0.52
length = 39
shrink_factor = 0.8
max_depth = 12
root = False
branch(prev_point=(125, 175), prev_angle=1.57, cur_length=length)

split_angle = 0.2
length = 52
max_depth = 6
root = True
branch(prev_point=(125, 175), prev_angle=1.57, cur_length=length)

svg_doc.save()

Full-Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
__author__ = 'David Jilg'
# Requires svgwrite~=1.4.3

import math
import svgwrite
from svgwrite.container import Group
from svgwrite.shapes import Polyline


def branch(previous_point, cur_angle, cur_length, cur_depth=0):
    if cur_depth > max_depth:
        return

    cur_depth += 1
    cur_length /= shrink_factor

    if root:
        p1 = (previous_point[0] + cur_length *
              math.cos(cur_angle + split_angle),

              previous_point[1] + cur_length *
              math.sin(cur_angle + split_angle))

        p2 = (previous_point[0] + cur_length *
              math.cos(cur_angle - split_angle),

              previous_point[1] + cur_length *
              math.sin(cur_angle - split_angle))
    else:
        p1 = (previous_point[0] - cur_length *
              math.cos(cur_angle + split_angle),

              previous_point[1] - cur_length *
              math.sin(cur_angle + split_angle))

        p2 = (previous_point[0] - cur_length *
              math.cos(cur_angle - split_angle),

              previous_point[1] - cur_length *
              math.sin(cur_angle - split_angle))

    if twin_lines:
        draw_polyline([p2, previous_point, p1], cur_depth)
    else:
        draw_polyline([previous_point, p2], cur_depth)
        draw_polyline([previous_point, p1], cur_depth)

    branch(p1, cur_angle + split_angle, cur_length, cur_depth)
    branch(p2, cur_angle - split_angle, cur_length, cur_depth)


def draw_polyline(points, depth):
    new_points = []
    for point in points:
        new_points.append(mm_to_pixel(point))
    if depth < 8:
        color = "saddlebrown"
        path = Polyline(points=new_points, stroke=color,
                        stroke_width="0.75", fill="none")
        svg_group_brown.add(path)
    elif depth < 10:
        color = "darkolivegreen"
        path = Polyline(points=new_points, stroke=color,
                        stroke_width="0.75", fill="none")
        svg_group_olive.add(path)
    elif depth < 12:
        color = "forestgreen"
        path = Polyline(points=new_points, stroke=color,
                        stroke_width="0.75", fill="none")
        svg_group_forest.add(path)
    else:
        color = "yellowgreen"
        path = Polyline(points=new_points, stroke=color,
                        stroke_width="0.75", fill="none")
        svg_group_yellow.add(path)


def mm_to_pixel(p, dpi=72):
    return (dpi * p[0] / 25.4), (dpi * p[1] / 25.4)


svg_group_brown = Group()
svg_group_olive = Group()
svg_group_forest = Group()
svg_group_yellow = Group()

twin_lines = False
split_angle = math.radians(29.8)
length = 39
shrink_factor = 1.25
max_depth = 12
root = False

branch(previous_point=(125, 175), cur_angle=1.57, cur_length=length)

split_angle = math.radians(11.5)
length = 52
max_depth = 6
root = True

branch(previous_point=(125, 175), cur_angle=1.57, cur_length=length)

svg_doc = svgwrite.Drawing(filename="output/fractral_tree.svg",
                           size=(708, 1048))

svg_doc.add(svg_group_brown)
svg_doc.add(svg_group_olive)
svg_doc.add(svg_group_forest)
svg_doc.add(svg_group_yellow)

svg_doc.save()