Make Fractals in Python Turtle with the FractalArtMaker Module
Wed 09 March 2022 Al Sweigart
FractalArtMaker is a module for creating fractal art in Python's turtle
module. You can install the module by running pip3 install FractalArtMaker
This module is explored in the book "The Recursive Book of Recursion" by Al Sweigart from No Starch Press. This blog post will show you how to make fractals like the following:
You can purchase this book directly from the publisher at https://nostarch.com/recursive-book-recursion or read it online at https://inventwithpython.com/recursion/
Quickstart
To view some example fractals, run the following from the interactive shell:
>>> import fractalartmaker
>>> fractalartmaker.fast() # draw the fractals quickly
>>> fractalartmaker.example(1) # pass 1 to 9
Making Fractals
The Fractal Art Maker's algorithm has two major components: a shape-drawing function and the recursive drawFractal()
function.
The shape-drawing function draws a basic shape. The Fractal Art Maker program comes with the two shape-drawing functions, fractalartmaker.drawFilledSquare()
and fractalartmaker.drawTriangleOutline()
, but you can also create your own. We pass a shape-drawing function to the fractalartmaker.drawFractal()
function as an argument.
The fractalartmaker.drawFractal()
function also has a parameter indicating changes to the size, position, and angle of the shapes between recursive calls to fractalartmaker.drawFractal()
.
The fractalartmaker.drawFractal()
function uses a shape-drawing function passed to it to draw the individual parts of the fractal. This is usually a simple shape, such as a square or triangle. The beautiful complexity of the fractals emerges from fractalartmaker.drawFractal()
recursively calling this function for each individual component of the whole fractal.
Here's the two shape-drawing functions that come in the fractalartmaker
module:
def drawFilledSquare(size, depth):
size = int(size)
# Move to the top-right corner before drawing:
turtle.penup()
turtle.forward(size // 2)
turtle.left(90)
turtle.forward(size // 2)
turtle.left(180)
turtle.pendown()
# Alternate between white and gray (with black border):
if depth % 2 == 0:
turtle.pencolor('black')
turtle.fillcolor('white')
else:
turtle.pencolor('black')
turtle.fillcolor('gray')
# Draw a square:
turtle.begin_fill()
for i in range(4): # Draw four lines.
turtle.forward(size)
turtle.right(90)
turtle.end_fill()
def drawTriangleOutline(size, depth):
size = int(size)
# Move the turtle to the top of the equilateral triangle:
height = size * math.sqrt(3) / 2
turtle.penup()
turtle.left(90) # Turn to face upwards.
turtle.forward(height * (2/3)) # Move to the top corner.
turtle.right(150) # Turn to face the bottom-right corner.
turtle.pendown()
# Draw the three sides of the triangle:
for i in range(3):
turtle.forward(size)
turtle.right(120)
The shape-drawing functions for the Fractal Art Maker have two parameters: size
and depth
. The size parameter is the length of the sides of the square or triangle it draws. The shape-drawing functions should always use arguments to turtle.forward()
that are based on size
so that the lengths will be proportionate to size at each level of recursion. Avoid code like turtle.forward(100)
or turtle.forward(200)
; instead, use code that is based on the size
parameter, like turtle.forward(size)
or turtle.forward(size * 2)
. In Python's turtle
module, turtle.forward(1)
moves the turtle by one unit, which is not necessarily the same as one pixel.
The shape-drawing functions' second parameter is the recursive depth of fractalartmaker.drawFractal()
. Your shape-drawing function can ignore this argument, but using it can cause interesting variations to the basic shape. For example, the fractalartmaker.drawFilledSquare()
shape-drawing function uses depth to alternate between drawing white squares and gray squares. Keep this in mind if you'd like to create your own shape-drawing functions for the Fractal Art Maker program, as they must accept a size
and depth
argument.
The fractalartmaker.drawFractal()
function has three required parameters and one optional one: shapeDrawFunction
, size
, specs
, and optionally maxDepth
. The shapeDrawFunction
parameter expects a function, like fractalartmaker.drawFilledSquare
or fractalartmaker.drawTriangleOutline
. The size
parameter expects the starting size passed to the drawing function. Often, a value between 100
and 500
is a good starting size, though this depends on the code in your shape-drawing function, and finding the right value may require experimentation.
The specs
parameter expects a list of dictionaries that specify how the recursive shapes should change their size, position, and angle as fractalartmaker.drawFractal()
recursively calls itself. These specifications are described later in this section. To prevent fractalartmaker.drawFractal()
from recursing until it causes a stack overflow, the maxDepth
parameter holds the number of times fractalartmaker.drawFractal()
should recursively call itself. By default, maxDepth
has a value of 8
, but you can provide a different value if you want more recursive shapes or fewer.
The recursive calls to fractalartmaker.drawFractal()
are based on the specification in the specs
list's dictionaries. For each dictionary, fractalartmaker.drawFractal()
makes one recursive call to fractalartmaker.drawFractal()
. If specs is a list with one dictionary, every call to fractalartmaker.drawFractal()
results in only one recursive call to fractalartmaker.drawFractal()
. If specs is a list with three dictionaries, every call to fractalartmaker.drawFractal()
results in three recursive calls to fractalartmaker.drawFractal()
.
The dictionaries in the specs parameter provide specifications for each recursive call. Each of these dictionaries has the keys sizeChange
, xChange
, yChange
, and angleChange
. These dictate how the size of the fractal, the position of the turtle, and the heading of the turtle change for a recursive fractalartmaker.drawFractal()
call.
sizeChange
(default is 1.0
) - The next recursive shape's size value is the current size multiplied by this value.
* xChange
(default is 0.0
) - The next recursive shape's x-coordinate is the current x-coordinate plus the current size multiplied by this value.
* yChange
(default is 0.0
) - The next recursive shape's y-coordinate is the current y-coordinate plus the current size multiplied by this value.
* angleChange
(default is 0.0
) - The next recursive shape's starting angle is the current starting angle plus this value.
Let's take a look at the specification dictionary for the Four Corners fractal, which is drawn when you call fractalartmaker.example(1)
. The call to fractalartmaker.drawFractal()
for the Four Corners fractal passes the following list of dictionaries for the specs
parameter:
fractalartmaker.drawFractal(fractalartmaker.drawFilledSquare, 350,
[{'sizeChange': 0.5, 'xChange': -0.5, 'yChange': 0.5},
{'sizeChange': 0.5, 'xChange': 0.5, 'yChange': 0.5},
{'sizeChange': 0.5, 'xChange': -0.5, 'yChange': -0.5},
{'sizeChange': 0.5, 'xChange': 0.5, 'yChange': -0.5}], 5)
The specs
list has four dictionaries, so each call to drawFractal()
that draws a square will, in turn, recursively call drawFractal()
four more times to draw four more squares.
To determine the size of the next square to be drawn, the value for the sizeChange
key is multiplied by the current size parameter. The first dictionary in the specs list has a sizeChange
value of 0.5
, which makes the next recursive call have a size argument of 350 * 0.5
, or 175
units. This makes the next square half the size of the previous square. A sizeChange
value of 2.0
would, for example, double the size of the next square. If the dictionary has no sizeChange
key, the value defaults to 1.0
for no change to the size.
If you look at the three other dictionaries in the specs
list, you'll notice they all have a sizeChange
value of 0.5
. The difference between them is that their xChange
and yChange
values place them in the other three corners of the current square. As a result, the next four squares are drawn centered on the four corners of the current square.
The dictionaries in the specs
list for this example don't have an angleChange
value, so this value defaults to 0.0
degrees. A positive angleChange
value indicates a counterclockwise rotation, while a negative value indicates a clockwise rotation.
Take a look at the code in the module's example()
function for more examples.
The fractalartmaker
module also has a fractalartmaker.fast()
function you can call to make the fractals draw quickly, and a fractalartmaker.clear()
to clear the turtle drawing window.
Let's examine the code for each of the 9 example fractals that come with the module.
Example 1 - Four Corners
The first fractal is Four Corners, which begins as a large square. As the function calls itself, the fractal's specifications cause four smaller squares to be drawn in the four corners of the square:
# Four Corners:
drawFractal(drawFilledSquare, 350,
[{'sizeChange': 0.5, 'xChange': -0.5, 'yChange': 0.5},
{'sizeChange': 0.5, 'xChange': 0.5, 'yChange': 0.5},
{'sizeChange': 0.5, 'xChange': -0.5, 'yChange': -0.5},
{'sizeChange': 0.5, 'xChange': 0.5, 'yChange': -0.5}], 5)
The call to drawFractal()
here limits the maximum depth to 5
, as any more tends to make the fractal so dense that the fine detail becomes hard to see.
Example 2 - Spiral Squares
The Spiral Squares fractal also starts as a large square, but it creates just one new square on each recursive call:
# Spiral Squares:
drawFractal(drawFilledSquare, 600, [{'sizeChange': 0.95, 'angleChange': 7}], 50)
This square is slightly smaller and rotated by 7 degrees. The centers of all the squares are unchanged, so there's no need to add xChange
and
keys yChange
to
the specification. The default maximum depth of 8
is too
small to get an interesting fractal, so we increase it to 50
to produce
a hypnotic spiral pattern.
Example 3 - Double Spiral Squares
The Double Spiral Squares fractal is similar to Spiral Squares, except each square creates two smaller squares. This creates an interesting fan effect, as the second square is drawn later and tends to cover up previously drawn squares:
# Double Spiral Squares:
drawFractal(drawFilledSquare, 600,
[{'sizeChange': 0.8, 'yChange': 0.1, 'angleChange': -10},
{'sizeChange': 0.8, 'yChange': -0.1, 'angleChange': 10}])
The squares are created slightly higher or lower than their previous square and rotated either 10
or -10
degrees.
Example 4 - Triangle Spiral
The Triangle Spiral fractal, another variation of the Spiral Square, uses the drawTriangleOutline()
shape-drawing function instead of drawFilledSquare()
:
# Triangle Spiral:
drawFractal(drawTriangleOutline, 20, [{'sizeChange': 1.05, 'angleChange': 7}], 80)
Unlike the Spiral Square, the Triangle Spiral begins at the small size of 20
units and slightly increases in size for each level of recursion. The sizeChange
key is greater than 1.0
, so the shapes are always increasing in size.
This means the base case occurs when the recursion reaches a depth of 80
, because the base case of size becoming less than 1
is never reached.
Example 5 - Conway's Game of Life Glider
Conway's Game of Life is a famous example of cellular automata. The game's simple rules cause interesting and wildly chaotic patterns to emerge on a two-dimensional grid. One such pattern is a Glider consisting of five cells in a 3 × 3 space:
# Conway's Game of Life Glider:
third = 1 / 3
drawFractal(drawFilledSquare, 600,
[{'sizeChange': third, 'yChange': third},
{'sizeChange': third, 'xChange': third},
{'sizeChange': third, 'xChange': third, 'yChange': -third},
{'sizeChange': third, 'yChange': -third},
{'sizeChange': third, 'xChange': -third, 'yChange': -third}])
The Glider fractal here has additional Gliders drawn inside each of its five cells. The third variable helps precisely set the position of the recursive shapes in the 3 × 3 space.
You can find a Python implementation of Conway's Game of Life online at https://inventwithpython.com/bigbookpython/project13.html. Tragically, John Conway, the mathematician and professor who developed Conway's Game of Life, passed away of complications from COVID-19 in April 2020.
Example 6 - Sierpiński Triangle
We created the Sierpiński Triangle fractal in Chapter 9, but our Fractal Art Maker can re-create it as well by using the drawTriangleOutline()
shape function. After all, a Sierpiński triangle is an equilateral triangle with three smaller equilateral triangles drawn in its interior:
# Sierpinski Triangle:
toMid = math.sqrt(3) / 6
drawFractal(drawTriangleOutline, 600,
[{'sizeChange': 0.5, 'yChange': toMid, 'angleChange': 0},
{'sizeChange': 0.5, 'yChange': toMid, 'angleChange': 120},
{'sizeChange': 0.5, 'yChange': toMid, 'angleChange': 240}])
The center of these smaller triangles is size * math.sqrt(3) / 6
units from the center of the previous triangle. The three calls adjust the heading of the turtle to 0, 120, and 240 degrees before moving up on the turtle's relative y-axis.
Example 7 - Wave
We discussed the Wave fractal at the start of this chapter. This relatively simple fractal creates three smaller and distinct recursive triangles:
# Wave:
drawFractal(drawTriangleOutline, 280,
[{'sizeChange': 0.5, 'xChange': -0.5, 'yChange': 0.5},
{'sizeChange': 0.3, 'xChange': 0.5, 'yChange': 0.5},
{'sizeChange': 0.5, 'yChange': -0.7, 'angleChange': 15}])
Example 8 - Horn
The Horn fractal resembles a ram's horn:
# Horn:
drawFractal(drawFilledSquare, 100, [{'sizeChange': 0.96, 'yChange': 0.5, 'angleChange': 11}], 100)
This simple fractal is made up of squares, each of which is slightly smaller, moved up, and rotated 11 degrees from the previous square. We increase the maximum recursion depth to 100 to extend the horn into a tight spiral.
Example 9 - Snowflake
The final fractal, Snowflake, is composed of squares laid out in a pentagon pattern. This is similar to the Four Corners fractal, but it uses five evenly spaced recursive squares instead of four:
# Snowflake:
drawFractal(drawFilledSquare, 200,
[{'xChange': math.cos(0 * math.pi / 180),
'yChange': math.sin(0 * math.pi / 180), 'sizeChange': 0.4},
{'xChange': math.cos(72 * math.pi / 180),
'yChange': math.sin(72 * math.pi / 180), 'sizeChange': 0.4},
{'xChange': math.cos(144 * math.pi / 180),
'yChange': math.sin(144 * math.pi / 180), 'sizeChange': 0.4},
{'xChange': math.cos(216 * math.pi / 180),
'yChange': math.sin(216 * math.pi / 180), 'sizeChange': 0.4},
{'xChange': math.cos(288 * math.pi / 180),
'yChange': math.sin(288 * math.pi / 180), 'sizeChange': 0.4}])
This fractal uses the cosine and sine functions from trigonometry, implemented in Python's math.cos()
and math.sin()
functions, to determine how to shift the squares along the x-axis and y-axis. A full circle has 360 degrees, so to evenly space out the five recursive squares in this circle, we place them at intervals of 0, 72, 144, 216, and 288 degrees. The math.cos()
and math.sin()
functions expect the angle argument to be in radians instead of degrees, so we must multiply these numbers by math.pi / 180.
The end result is that each square is surrounded by five other squares, which are surrounded by five other squares, and so on, to form a crystal-like fractal that resembles a snowflake.