Published on 12/2021

Fractals

If you haven't heard of them before, "fractals" are images where sub-components have a similar appearance to the structure as a whole, and essentially could be made infinitely complex and detailed.

Sierpiński's triangle is probably the one you've heard of, but there are many examples of fractal-like patterns in nature too, especially in plants and shells:

Snail Shell
Pretty Leaf
Romanesco Broccoli

In this project, I wanted to create an image of Sierpiński's triangle using Python and Python Turtle.

Now, decomposing the problem:

  1. Importing modules
  2. Global variables
  3. Subprograms
    • Triangle function
    • Fractal function
  4. Putting it all together

Importing Modules

Clearly, we need to import the turtle module.


python
import turtle as t
python
import turtle as t

Global Variables

As explained earlier, we could keep making our fractal more and more detailed, so we need to define a maximum depth so that the program does actually stop and doesn't crash (or reach the actual inbuilt maximum recursion depth of about 1000 recursions).



It would then also make sense to create a variable for the initial depth we have so that we can count upwards to our maximum depth, and a side length variable would also be good.


python
# GLOBAL VARIABLES
max_depth = 0
start_depth = 0
side = 300   # max side length
python
# GLOBAL VARIABLES
max_depth = 0
start_depth = 0
side = 300   # max side length

Subprograms

Triangle Function

A Sierpiński triangle is made up of a bunch of smaller triangles, so before we make our actual fractal function, to make the code more streamlined we should make a triangle function that makes a triangle of a specific side length.


python
def triangle(l):
    for x in range(3):
        t.forward(l)
        t.left(360 / 3)
python
def triangle(l):
    for x in range(3):
        t.forward(l)
        t.left(360 / 3)

Fractal Function

After this we need to make the actual fractal function.

The function needs the max_depth variable, which is set outside it, so we declare it with global.


python
def fractal(s, c):
    global max_depth

    if c != max_depth:
        for count in range(3):
            fractal(s / 2, c + 1)
            t.forward(s)
            t.left(360 / 3)

    if c == max_depth:   # then we go back and stop
        for count in range(3):
            triangle(s / 2)
            t.forward(s)
            t.left(360 / 3)
python
def fractal(s, c):
    global max_depth

    if c != max_depth:
        for count in range(3):
            fractal(s / 2, c + 1)
            t.forward(s)
            t.left(360 / 3)

    if c == max_depth:   # then we go back and stop
        for count in range(3):
            triangle(s / 2)
            t.forward(s)
            t.left(360 / 3)

Next we identify our two states: having not reached the max depth, and having already reached it.

It's actually easier to think about what happens when we have reached the max depth - in this case we just draw 3 half-size triangles, one in each corner. The gap left in the middle is the empty quarter that gives Sierpiński's triangle its pattern.

This works because every call to the fractal function makes 3 more calls to itself, one for each corner, until the max depth is reached. That means the number of calls grows exponentially - 3 to the power of max_depth - which is why higher depths take noticeably longer to draw.

python
    if c == max_depth:   # then we go back and stop
        for count in range(3):
            triangle(s / 2)
            t.forward(s)
            t.left(360 / 3)
python
    if c == max_depth:   # then we go back and stop
        for count in range(3):
            triangle(s / 2)
            t.forward(s)
            t.left(360 / 3)

Thinking that through helps to do the main chunk of the function.

We need to make triangles, but smaller ones each time, so we call the function, but half the side length and increase the depth (otherwise the function will never reach the max depth). Then we move forward by 's' to make room for the next triangles we'll draw, before rotating too.

python
    if c != max_depth:
        for count in range(3):
            fractal(s / 2, c + 1)
            t.forward(s)
            t.left(360 / 3)
python
    if c != max_depth:
        for count in range(3):
            fractal(s / 2, c + 1)
            t.forward(s)
            t.left(360 / 3)

Putting It All Together

After we've defined the functions, the following steps are pretty basic. We have the turtle, which we'll put at a very fast speed so we don't need to wait too long. Then we translate the turtle to coordinates (-200,-200) to make room for our fractal.


python
# MAIN
t.speed(0)  # max speed so we don't wait forever
t.penup()
t.goto(-200, -200)   # start at different coordinate so more space
t.pendown()
python
# MAIN
t.speed(0)  # max speed so we don't wait forever
t.penup()
t.goto(-200, -200)   # start at different coordinate so more space
t.pendown()

Then we can ask the user for the maximum depth they want, before running our fractal program. Then we tell the turtle to stop running and wait for the screen to be closed with turtle.done().


python
max_depth = int(input('Enter the maximum depth: '))
fractal(side, start_depth)
t.done()
python
max_depth = int(input('Enter the maximum depth: '))
fractal(side, start_depth)
t.done()

Bonus - Colours!

The whole point of fractals is to be pretty, but the black colour's a bit bland. We can create a list containing a set of colours - of course we'll need the random module to randomly select a colour.


python
import turtle as t
import random as r
python
import turtle as t
import random as r

python
side = 300   # max side length
colours = ["skyblue", "red", "purple", "gold", "maroon", "green"]
python
side = 300   # max side length
colours = ["skyblue", "red", "purple", "gold", "maroon", "green"]

Now to use these colours. We can change the pen colour of turtle and use the random module to randomly select a colour to use:


python
    if c != max_depth:
        for count in range(3):
            fractal(s / 2, c + 1)
            t.forward(s)
            t.left(360 / 3)
    t.color(r.choice(colours))
python
    if c != max_depth:
        for count in range(3):
            fractal(s / 2, c + 1)
            t.forward(s)
            t.left(360 / 3)
    t.color(r.choice(colours))

Best of all, the randomness of the colours actually looks nice!

Sierpiński triangle drawn in random colours, lower depth
Sierpiński triangle drawn in random colours, higher depth

And we're done! Thanks for reading, and don't forget to check out the full source code on GitHub!