Chapter 9 introduced you to programs that draw many well-known fractals with the turtle
Python module, but you can also make your own fractal art with the project in this chapter. The Fractal Art Maker program uses Python’s turtle
module to turn simple shapes into complex designs with minimal additional code.
The project in this chapter comes with nine example fractals, although you can also write new functions to create fractals of your design. Modify the example fractals to produce radically different artwork or write code from scratch to implement your own creative vision.
You can direct your computer to create an unlimited number of fractals. Figure 13-1 shows the nine fractals that come with the Fractal Art Maker program that we’ll use in this chapter. These are produced from functions that draw a simple square or equilateral triangle as the base shape, then introduce slight differences in their recursive configuration to produce completely different images.
You can produce all of these fractals by setting the DRAW_FRACTAL
constant at the top of the program to an integer from 1
to 9
and then running the Fractal Art Maker program. You can also set DRAW_FRACTAL
to 10
or 11
to draw the basic square and triangle shapes, respectively, that compose these fractals, as shown in Figure 13-2.
These shapes are fairly simple: a square filled with either white or gray, and a simple outline of a triangle. The drawFractal()
function uses these basic shapes to create amazing 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 shown previously in Figure 13-2, drawFilledSquare()
and drawTriangleOutline()
, but you can also create your own. We pass a shape-drawing function to the drawFractal()
function as an argument, just as we passed the match functions to the file finder’s walk()
function in Chapter 10.
The drawFractal()
function also has a parameter indicating changes to the size, position, and angle of the shapes between recursive calls to drawFractal()
. We’ll cover these specific details later in this chapter, but let’s look at one example: fractal 7, which draws a wave-like image.
The program produces the Wave fractal by calling the drawTriangleOutline()
shape-drawing function, which creates a single triangle. The additional arguments to drawFractal()
tell it to make three recursive calls to drawFractal()
. Figure 13-3 shows the triangle produced by the original call to drawFractal()
and the triangles produced by the three recursive calls.
The first recursive call tells drawFractal()
to call drawTriangleOutline()
but with a triangle that is half the size and positioned to the top left of the previous triangle. The second recursive call produces a triangle to the top right of the previous triangle that is 30 percent of its size. The third recursive call produces a triangle below the previous triangle that is half its size and rotated 15 degrees compared to it.
Each of these three recursive calls to drawFractal()
makes three more recursive calls to drawFractal()
, producing nine new triangles. The new triangles have the same changes to their size, position, and angle relative to their previous triangle. The top-left triangle is always half the size of the previous triangle, while the bottom triangle is always rotated 15 degrees more. Figure 13-4 shows the triangles produced by the first and second levels of recursion.
The nine calls to drawFractal()
that produce these nine new triangles each make three recursive calls to drawFractal()
, producing 27 new triangles at the next level of recursion. As this pattern of recursion continues, eventually the triangles become so small that drawFractal()
stops making new recursive calls. This is one of the base cases for the recursive drawFractal()
function. The other occurs when the recursive depth reaches a specified level. Either way, these recursive calls produce the final Wave fractal in Figure 13-5.
The nine example fractals in Figure 13-1 that come with the Fractal Art Maker are made with just two shape-drawing functions and a few changes to the arguments to drawFractal()
. Let’s take a look at the Fractal Art Maker’s code to see how it accomplishes this.
Enter the following code into a new file and save it as fractalArtMaker.py. This program relies on Python’s built-in turtle
module, so no JavaScript code is used for this chapter’s project:
Python
import turtle, math
DRAW_FRACTAL = 1 # Set to 1 through 11 and run the program.
turtle.tracer(5000, 0) # Increase the first argument to speed up the drawing.
turtle.hideturtle()
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 upward.
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)
def drawFractal(shapeDrawFunction, size, specs, maxDepth=8, depth=0):
if depth > maxDepth or size < 1:
return # BASE CASE
# Save the position and heading at the start of this function call:
initialX = turtle.xcor()
initialY = turtle.ycor()
initialHeading = turtle.heading()
# Call the draw function to draw the shape:
turtle.pendown()
shapeDrawFunction(size, depth)
turtle.penup()
# RECURSIVE CASE
for spec in specs:
# Each dictionary in specs has keys 'sizeChange', 'xChange',
# 'yChange', and 'angleChange'. The size, x, and y changes
# are multiplied by the size parameter. The x change and y
# change are added to the turtle's current position. The angle
# change is added to the turtle's current heading.
sizeCh = spec.get('sizeChange', 1.0)
xCh = spec.get('xChange', 0.0)
yCh = spec.get('yChange', 0.0)
angleCh = spec.get('angleChange', 0.0)
# Reset the turtle to the shape's starting point:
turtle.goto(initialX, initialY)
turtle.setheading(initialHeading + angleCh)
turtle.forward(size * xCh)
turtle.left(90)
turtle.forward(size * yCh)
turtle.right(90)
# Make the recursive call:
drawFractal(shapeDrawFunction, size * sizeCh, specs, maxDepth,
depth + 1)
if DRAW_FRACTAL == 1:
# 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)
elif DRAW_FRACTAL == 2:
# Spiral Squares:
drawFractal(drawFilledSquare, 600, [{'sizeChange': 0.95,
'angleChange': 7}], 50)
elif DRAW_FRACTAL == 3:
# Double Spiral Squares:
drawFractal(drawFilledSquare, 600,
[{'sizeChange': 0.8, 'yChange': 0.1, 'angleChange': -10},
{'sizeChange': 0.8, 'yChange': -0.1, 'angleChange': 10}])
elif DRAW_FRACTAL == 4:
# Triangle Spiral:
drawFractal(drawTriangleOutline, 20,
[{'sizeChange': 1.05, 'angleChange': 7}], 80)
elif DRAW_FRACTAL == 5:
# 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}])
elif DRAW_FRACTAL == 6:
# Sierpiński 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}])
elif DRAW_FRACTAL == 7:
# 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}])
elif DRAW_FRACTAL == 8:
# Horn:
drawFractal(drawFilledSquare, 100,
[{'sizeChange': 0.96, 'yChange': 0.5, 'angleChange': 11}], 100)
elif DRAW_FRACTAL == 9:
# 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}])
elif DRAW_FRACTAL == 10:
# The filled square shape:
turtle.tracer(1, 0)
drawFilledSquare(400, 0)
elif DRAW_FRACTAL == 11:
# The triangle outline shape:
turtle.tracer(1, 0)
drawTriangleOutline(400, 0)
else:
assert False, 'Set DRAW_FRACTAL to a number from 1 to 11.'
turtle.exitonclick() # Click the window to exit.
When you run this program, it will show the first of nine fractal images from Figure 13-1. You can change the DRAW_FRACTAL
constant at the beginning of the source code to any integer from 1
to 9
and run the program again to see a new fractal. After learning how the program works, you’ll also be able to create your own shape-drawing functions and call drawFractal()
to produce fractals of your own design.
The first lines of the program cover basic setup steps for our turtle-based program:
Python
import turtle, math
DRAW_FRACTAL = 1 # Set to 1 through 11 and run the program.
turtle.tracer(5000, 0) # Increase the first argument to speed up the drawing.
turtle.hideturtle()
The program imports the turtle
module for drawing. It also imports the math
module for the math.sqrt()
function, which the Sierpiński Triangle fractal will use, and the math.cos()
and math.sin()
functions, for the Snowflake fractal.
The DRAW_FRACTAL
constant can be set to any integer from 1
to 9
to draw one of the nine built-in fractals the program produces. You can also set it to 10
or 11
to show the output of the square or triangle shape-drawing function, respectively.
We also call some turtle functions to prepare for drawing. The turtle.tracer(5000, 0)
call speeds up the drawing of the fractal. The 5000
argument tells the turtle
module to wait until 5,000 turtle drawing instructions have been processed before rendering the drawing on the screen, and the 0
argument tells it to pause for 0 milliseconds after each drawing instruction. Otherwise, the turtle
module would render the image after each drawing instruction, which significantly slows the program if we want only the final image.
You can change this call to turtle.tracer(1, 10)
if you want to slow the drawing and watch the lines as they’re produced. This can be useful when making your own fractals to debug any problems with the drawing.
The turtle.hideturtle()
call hides the triangle shape on the screen that represents the turtle’s current position and heading. (Heading is another term for direction.) We call this function so that the marker doesn’t appear in the final image.
The 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 drawFractal()
recursively calling this function for each individual component of the whole fractal.
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 drawFractal()
. The original call to drawFractal()
has the depth
parameter set to 0
. Recursive calls to drawFractal()
use depth + 1
as the depth
parameter. In the Wave fractal, the first triangle in the center of the window has a depth argument of 0
. The three triangles created next have a depth of 1
. The nine triangles around those three triangles have a depth of 2
, and so on.
Your shape-drawing function can ignore this argument, but using it can cause interesting variations to the basic shape. For example, the 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 drawFilledSquare()
function draws a filled-in square with sides of length size
. To color the square, we use the turtle
module’s turtle.begin_fill()
and turtle.end_fill()
functions to make the square either white or gray, with a black border, depending on whether the depth
argument is even or odd. Because these squares are filled in, any squares drawn on top of them later will cover them.
Like all shape-drawing functions for the Fractal Art Maker program, drawFilledSquare()
accepts a size
and depth
parameter:
def drawFilledSquare(size, depth):
size = int(size)
The size
argument could be a floating-point number with a fractional part, which sometimes causes the turtle
module to make slightly asymmetrical and uneven drawings. To prevent this, the first line of the function rounds size
down to an integer.
When the function draws the square, it assumes the turtle is in the center of the square. Thus, the turtle must first move to the top-right corner of the square, relative to its initial heading:
Python
# 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()
The drawFractal()
function always has the pen down and ready to draw when the shape-drawing function is called, so drawFilledSquare()
must call turtle.penup() to avoid drawing a line as it moves to the starting position. To find the starting position relative to the middle of the square, the turtle must first move half of the square’s length (that is,
size // 2
) forward, to the future right edge of the square. Next the turtle turns 90 degrees to face up and then moves size // 2
units forward to the top-right corner. The turtle is now facing the wrong way, so it turns around 180 degrees and places the pen down so that it can begin drawing.
Note that top-right and up are relative to the direction the turtle is originally facing. This code works just as well if the turtle begins facing to the right at 0 degrees or has a heading of 90, 42, or any other number of degrees. When you create your own shape-drawing functions, stick to the relative turtle movement functions like turtle.forward()
, turtle.left()
, and turtle.right()
instead of absolute turtle movement functions like turtle.goto()
.
Next, the depth
argument tells the function whether it should draw a white square or a gray one:
Python
# 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')
If depth
is even, the depth % 2 == 0
condition is True
, and the square’s fill color is white. Otherwise, the code sets the fill color to gray. Either way, the border of the square, determined by the pen color, is set to black. To change either of these colors, use strings of common color names, like red
or yellow
, or an HTML color code comprising a hash mark and six hexadecimal digits, like #24FF24
for lime green or #AD7100
for brown.
The website https://html-color.codes has charts for many HTML color codes. The fractals in this black-and-white book lack color, but your computer can render your own fractals in a bright range of colors!
With the colors set, we can finally draw the four lines of the actual square:
Python
# Draw a square:
turtle.begin_fill()
for i in range(4): # Draw four lines.
turtle.forward(size)
turtle.right(90)
turtle.end_fill()
To tell the turtle
module that we intend to draw a filled-in shape and not just the outline, we call the turtle.begin_fill()
function. Next is a for
loop that draws a line of length size
and turns the turtle 90 degrees to the right. The for
loop repeats this four times to create the square. When the function finally calls turtle.end_fill()
, the filled-in square appears on the screen.
The second shape-drawing function draws the outline of an equilateral triangle whose sides have a length of size
. The function draws the triangle oriented with one corner at the top and two corners at the bottom. Figure 13-6 illustrates the various dimensions of an equilateral triangle.
Before we begin drawing, we must determine the triangle’s height based on the length of its sides. Geometry tells us that, for equilateral triangles with sides of length L, the height h of the triangle is L times the square root of 3 divided by 2. In our function, L corresponds to the size
parameter, so our code sets the height variable as follows:
height = size * math.sqrt(3) / 2
Geometry also tells us that the center of the triangle is one-third of the height from the bottom side and two-thirds of the height from the top point. This gives us the information we need to move the turtle to its starting position:
Python
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 upward.
turtle.forward(height * (2/3)) # Move to the top corner.
turtle.right(150) # Turn to face the bottom-right corner.
turtle.pendown()
To reach the top corner, we turn the turtle 90 degrees left to face up (relative to the turtle’s original heading right at 0 degrees) and then move forward a number of units equal to height * (2/3)
. The turtle is still facing up, so to begin drawing the line on the right side, the turtle must turn 90 degrees right to face rightward, then an additional 60 degrees to face the bottom-right corner of the triangle. This is why we call turtle.right(150)
.
At this point, the turtle is ready to start drawing the triangle, so we lower the pen by calling turtle.pendown()
. A for
loop will handle drawing the three sides:
Python
# Draw the three sides of the triangle:
for i in range(3):
turtle.forward(size)
turtle.right(120)
Drawing the actual triangle is a matter of moving forward by size
units, and then turning 120 degrees to the right, three separate times. The third and final 120-degree turn leaves the turtle facing its original direction. You can see these movements and turns in Figure 13-7.
The drawTriangleOutline()
function draws only the outline and not a filled-in shape, so it doesn’t call turtle.begin_fill()
and turtle.end_fill()
as drawFilledSquare()
does.
Now that we have two sample drawing functions to work with, let’s examine the main function in the Fractal Art Maker project, drawFractal()
. This function has three required parameters and one optional one: shapeDrawFunction
, size
, specs
, and maxDepth
.
The shapeDrawFunction
parameter expects a function, like drawFilledSquare()
or 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 drawFractal()
recursively calls itself. These specifications are described later in this section.
To prevent drawFractal()
from recursing until it causes a stack overflow, the maxDepth
parameter holds the number of times drawFractal()
should recursively call itself. By default, maxDepth
has a value of 8
, but you can provide a different value if you want more or fewer recursive shapes.
A fifth parameter, depth
, is handled by drawFractal()
’s recursive call to itself and defaults to 0
. You don’t need to specify it when you call drawFractal()
.
The first thing the drawFractal()
function does is check for its two base cases:
Python
def drawFractal(shapeDrawFunction, size, specs, maxDepth=8, depth=0):
if depth > maxDepth or size < 1:
return # BASE CASE
If depth
is greater than maxDepth
, the function will stop the recursion and return. The other base case occurs if size
is less than 1
, at which point the shapes being drawn would be too small to be seen on the screen and so the function should simply return.
We keep track of the turtle’s original position and heading in three variables: initialX
, initialY
, and initialHeading
. This way, no matter where the shape-drawing function leaves the turtle positioned or what direction it is headed, drawFractal()
can revert the turtle back to the original position and heading for the next recursive call:
Python
# Save the position and heading at the start of this function call:
initialX = turtle.xcor()
initialY = turtle.ycor()
initialHeading = turtle.heading()
The turtle.xcor()
and turtle.ycor()
functions return the absolute x- and y-coordinates of the turtle on the screen. The turtle.heading()
function returns the direction in which the turtle is pointed in degrees.
The next few lines call the shape-drawing function passed to the shapeDrawFunction
parameter:
Python
# Call the draw function to draw the shape:
turtle.pendown()
shapeDrawFunction(size, depth)
turtle.penup()
Because the value passed as the argument for the shapeDrawFunction
parameter is a function, the code shapeDrawFunction(size, depth)
calls this function with the values in size
and depth
. The pen is lowered before and raised after the shapeDrawFunction()
call to ensure that the shape-drawing function can consistently expect the pen to be down when the drawing begins.
After the call to shapeDrawFunction()
, the rest of drawFractal()
’s code is devoted to making recursive calls to drawFractal()
based on the specification in the specs
list’s dictionaries. For each dictionary, drawFractal()
makes one recursive call to drawFractal()
. If specs
is a list with one dictionary, every call to drawFractal() results in only one recursive call to
drawFractal()
. If specs
is a list with three dictionaries, every call to drawFractal()
results in three recursive calls to 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 drawFractal()
call. Table 13-1 describes the four keys in a specification.
Table 13-1: Keys in the Specification Dictionaries
Key | Default value | Description |
sizeChange |
1.0 |
The next recursive shape’s size value is the current size multiplied by this value. |
xChange |
0.0 |
The next recursive shape’s x-coordinate is the current x-coordinate plus the current size multiplied by this value. |
yChange |
0.0 |
The next recursive shape’s y-coordinate is the current y-coordinate plus the current size multiplied by this value. |
angleChange |
0.0 |
The next recursive shape’s starting angle is the current starting angle plus this value, in degrees. |
Let’s take a look at the specification dictionary for the Four Corners fractal, which produces the top-left image shown previously in Figure 13-1. The call to drawFractal()
for the Four Corners fractal passes the following list of dictionaries for the specs
parameter:
Python
[{'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}]
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. Figure 13-8 shows this progression of squares (which alternate between white and gray).
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.
To determine the x-coordinate of the next square, the first dictionary’s xChange
value, -0.5
in this case, is multiplied by the size. When size
is 350
, this means the next square has an x-coordinate of -175
units relative to the turtle’s current position. This xChange
value and the yChange
key’s value of 0.5
places the next square’s position a distance of 50 percent of the current square’s size, to the left and above the current square’s position. This happens to center it on the top-left corner of the current square.
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.
Each dictionary represents a separate square to be drawn each time the recursive function is called. If we were to remove the first dictionary from the specs
list, each drawFractal()
call would produce only three squares, as in Figure 13-9.
Let’s look at how the code in drawFractal()
actually does everything we’ve described:
Python
# RECURSIVE CASE
for spec in specs:
# Each dictionary in specs has keys 'sizeChange', 'xChange',
# 'yChange', and 'angleChange'. The size, x, and y changes
# are multiplied by the size parameter. The x change and y
# change are added to the turtle's current position. The angle
# change is added to the turtle's current heading.
sizeCh = spec.get('sizeChange', 1.0)
xCh = spec.get('xChange', 0.0)
yCh = spec.get('yChange', 0.0)
angleCh = spec.get('angleChange', 0.0)
The for
loop assigns an individual specification dictionary in the specs
list to the loop variable spec
on each iteration of the loop. The get()
dictionary method calls pull the values for the sizeChange
, xChange
, yChange
, and angleChange
keys from this dictionary and assign them to the shorter-named sizeCh
, xCh
, yCh
, and angleCh
variables. The get()
method substitutes a default value if the key doesn’t exist in the dictionary.
Next, the turtle’s position and heading are reset to the values indicated when drawFractal()
was first called. This ensures that the recursive calls from previous loop iterations don’t leave the turtle in some other place. Then the heading and position are changed according to the angleCh
, xCh
, and yCh
variables:
Python
# Reset the turtle to the shape's starting point:
turtle.goto(initialX, initialY)
turtle.setheading(initialHeading + angleCh)
turtle.forward(size * xCh)
turtle.left(90)
turtle.forward(size * yCh)
turtle.right(90)
The x-change and y-change positions are expressed relative to the turtle’s current heading. If the turtle’s heading is 0
, the turtle’s relative x-axis is the same as the actual x-axis on the screen. However, if the turtle’s heading is, say, 45
, the turtle’s relative x-axis is at a 45-degree tilt. Moving “right” along the turtle’s relative x-axis would then move at an up-right angle.
This is why moving forward by size * xCh
moves the turtle along its relative x-axis. If xCh
is negative, turtle.forward()
moves left along the turtle’s relative x-axis. The turtle.left(90)
call points the turtle along the turtle’s relative y-axis, and turtle.forward(size * yCh)
moves the turtle to the next shape’s starting position. However, the turtle.left(90)
call changed the turtle’s heading, so turtle.right(90)
is called to reset it back to its original direction.
Figure 13-10 shows how these four lines of code move the turtle to the right along its relative x-axis and up along its relative y-axis and leave it in the correct heading, no matter what its initial heading was.
Finally, with the turtle in the correct position and heading for the next shape, we make the recursive call to drawFractal()
:
Python
# Make the recursive call:
drawFractal(shapeDrawFunction, size * sizeCh, specs, maxDepth,
depth + 1)
The shapeDrawFunction
, specs
, and maxDepth
arguments are passed to the recursive drawFractal()
call unchanged. However, size * sizeCh
is passed for the next size
parameter to reflect the change in the size
of the recursive shape, and depth + 1
is passed for the depth
parameter to increment it for the next shape-drawing function call.
Now that we’ve covered how the shape-drawing functions and recursive drawFractal()
function work, let’s look at the nine example fractals that come with the Fractal Art Maker. You can see these examples in Figure 13-1.
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:
Python
if DRAW_FRACTAL == 1:
# 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. This fractal appears in Figure 13-8.
The Spiral Squares fractal also starts as a large square, but it creates just one new square on each recursive call:
Python
elif DRAW_FRACTAL == 2:
# 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 yChange
keys 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.
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:
Python
elif DRAW_FRACTAL == 3:
# 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.
The Triangle Spiral fractal, another variation of Spiral Squares, uses the drawTriangleOutline()
shape-drawing function instead of drawFilledSquare()
:
Python
elif DRAW_FRACTAL == 4:
# Triangle Spiral:
drawFractal(drawTriangleOutline, 20,
[{'sizeChange': 1.05, 'angleChange': 7}], 80)
Unlike the Spiral Squares fractal, the Triangle Spiral fractal 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.
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 2D grid. One such pattern is a Glider consisting of five cells in a 3 × 3 space:
Python
elif DRAW_FRACTAL == 5:
# 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 in my book The Big Book of Small Python Projects (No Starch Press, 2021) and 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.
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:
Python
elif DRAW_FRACTAL == 6:
# Sierpiński 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.
We discussed the Wave fractal at the start of this chapter, and you can see it in Figure 13-5. This relatively simple fractal creates three smaller and distinct recursive triangles:
Python
elif DRAW_FRACTAL == 7:
# 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}])
The Horn fractal resembles a ram’s horn:
Python
elif DRAW_FRACTAL == 8:
# 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.
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:
Python
elif DRAW_FRACTAL == 9:
# 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.
For completion, you can also set DRAW_FRACTAL
to 10
or 11
to view what a single call to drawFilledSquare()
and drawTriangleOutline()
produce in the turtle window. These shapes are drawn with a size of 600
:
Python
elif DRAW_FRACTAL == 10:
# The filled square shape:
turtle.tracer(1, 0)
drawFilledSquare(400, 0)
elif DRAW_FRACTAL == 11:
# The triangle outline shape:
turtle.tracer(1, 0)
drawTriangleOutline(400, 0)
turtle.exitonclick() # Click the window to exit.
After drawing the fractal or shape based on the value in DRAW_FRACTAL
, the program calls turtle.exitonclick()
so that the turtle window stays open until the user clicks it. Then the program terminates.
You can create your own fractals by changing the specification passed to the drawFractal()
function. Start by thinking about how many recursive calls you’d like each call to drawFractal()
to generate, and how the size, position, and heading of the shapes should change. You can use the existing shape-drawing functions or create your own.
For example, Figure 13-11 shows the nine built-in fractals, except the square and triangle functions have been swapped. Some of these produce bland shapes, but others can result in unexpected beauty.
The Fractal Art Maker projects demonstrate the endless possibilities of recursion. A simple recursive drawFractal()
function, paired with a shape-drawing function, can create a large variety of detailed geometric art.
At the core of Fractal Art Maker is the recursive drawFractal()
function, which accepts another function as an argument. This second function draws a basic shape repeatedly by using the size, position, and heading given in the list of specification dictionaries.
You can test an unlimited number of shape-drawing functions and specification settings. Let your creativity drive your fractal projects as you experiment with the code in this program.
There are websites that allow you to create fractals. Interactive Fractal Tree at https://www.visnos.com/demos/fractal has sliders to change a binary tree fractal’s angle and size parameters. Procedural Snowflake at https://procedural-snowflake.glitch.me generates new snowflakes in your browser. Nico’s Fractal Machine at https://sciencevsmagic.net/fractal creates animated drawings of fractals. You can find others by searching the web for fractal maker or fractal generator online.