INST326 Homework 02 Framework

Art with Turtles

The following code lets you experiment with making images using recursion and the Python Turtle library.

Assignment

Write functions according to the following specifications.

Turtle Setup and Configuration

In [1]:
# Import the turtle package for drawing
import turtle
import random
In [2]:
# Create a screen on which we draw
screen = turtle.Screen()

# Configure a default size
dimX = 400
dimY = 400
screen.setup(dimX, dimY)

Koch Curve Fractal

Write a recursive function drawKoch(ttl, length, generations) that draws a fractal shape called the Koch Curve. The Koch curve is a fractal – a shape that is self-similar, that is, it can be decomposed into smaller versions of the same shape.

Many of the techniques to generate fractals computationally rely on the repetition of a set of steps, so we tend to talk about generations, which are the number of iterations that we used to produce the drawing. The Koch curve is a fairly simple one, and we can see its structure most clearly, by looking at how it changes from one generation to the next.

The starting place (generation 0) is a straight line.

line

For the first generation, we cut the line into thirds and replace the middle segment with two segments of the same length, forming two sides of an equilateral triangle.

g1

The second generation is formed by applying this same process to every line in generation 1.

g2

Further generations are formed by repeating this process, taking each line, breaking it into thirds and replacing the middle segment with a bump. Conceptually, if you take this out to infinity, you will have a very curious mathematical phenomenon – an infinitely long line between two fixed points a finite distance away that also occupies a finite area. In practical terms, however, when the length of the line becomes 1 pixel long, that is pretty much as small as we can get.

g3

To actually draw the shape, our process will really be the opposite of what is described above. We don’t want to draw a line and then go back and remove a piece. Instead, we will start with the highest generation of the fractal first. If you think about a generation 2 curve, it is really made up of four generation 1 curves that are each 1/3 the length of the generation 2 curve. But each of those generation 1 curves are made up of generation 0 curves 1/3 of that length. What that means is that we don’t actually draw any of the higher generation curves, the only thing we draw is a whole bunch of generation 0 curves (straight lines!) in different locations.

Recursive Drawing Assignment

Step 1: I've given you a template for the drawKock() function. We will be drawing this shape using recursion in this function. The generations parameter of the function will control the recursion. The base case is when generations is 0. In that case, the turtle should just draw a straight line of length length (it doesn’t matter which way the turtle is facing, we just go forward).

Step 2: Write the base case. When generations is 0, draw a straight line of length length.

Step 3: Test this by running the cell below that sets up the scene and calls drawKoch(). It should draw three lines that are 300 pixels long.

Step 4: We are going to start work on the recursive case, but it won’t be recursive yet. When generations is greater than 0, draw a first generation curve. We want the curve to fit within length pixels. So, move forward length/3 pixels. Turn left by 60 degrees. Move forward length/3 pixels (putting you at the apex of the triangle). Turn right and draw the second leg of the triangle. Turn left again and draw the final base line. You should now have a generation 1 curve that fits on a length length line

Step 5: Test, and make sure that when you change the number of generations in the cell below that you get a generation one curve.

Step 6: Time to go recursive. Instead of drawing straight lines in the recursive case, each call to forward should be replaced with a call that draws a Koch curve of the same length (i.e., one third length), and one generation less.

In [8]:
def drawKoch(ttl, length, generation):
    if ( generation == 0 ):
        # Base case
        ttl.fd(length)
        return

    # Recursive case here
    # call drawKoch(), rotate, call drawKoch(), rotate back, drawKoch()

    stepSize = length / 3
    
    drawKoch(ttl, stepSize, generation-1)
    ttl.left(60)
    drawKoch(ttl, stepSize, generation-1)
    ttl.right(120)
    drawKoch(ttl, stepSize, generation-1)
    ttl.left(60)
    drawKoch(ttl, stepSize, generation-1)
    
    return
In [10]:
# Reset the screen
turtle.clearscreen()
screen.reset()
screen.screensize(dimX, dimY)

# Create a turtle
ttl = turtle.Turtle()
ttl.speed(10)
ttl.penup()
ttl.setposition(-dimX/2 + 10, -dimY/2 + 50)
ttl.pendown()

gens = 3
length = 300


# drawKoch(ttl, length, gens)

sides = 3
angle = ((sides - 2) * 180) / sides

for i in range(sides):
    drawKoch(ttl, length, gens)
    ttl.left(180-angle)

Fractal Trees

Trees are another self-similar shape we can draw with the turtle.

Write a function drawTree() to draw a simple recursive tree having the important feature that the turtle is left in exactly the same position and orientation after drawing a tree. Using this, the basic tree shape can be accomplished by following these steps:

  1. Move forward by some amount (this draws the trunk)

  2. Turn left by 45 degrees

  3. Draw a smaller version of the tree (e.g., perhaps half size)

  4. Turn right by 90 degrees (you should actually think of this as undo-ing the previous 45 degrees and then turning right 45 degrees)

  5. Draw a smaller version of the tree

  6. Turn left by 45 degrees (i.e., undo the turn to the right)

  7. Move backward by the length of the trunk (the turtle should now be in its original position and orientation)

Here is what that tree looks like taken out to three generations:

img1

And ehre is what it will look like after 8 generations

img2

Tree Drawing Assignment

Write a function called drawTree(ttl, length, generations) that follows the above algorithm. You can write this like the Koch curve. For the base case, where generations is zero, draw a line forward and backward by the same length to keep the turtle cursor at its starting point. Use length for the length of the trunk, and pass length/2 as the length for the smaller trees.

In [13]:
def drawTree(ttl, height, generation):  
    if ( generation == 0 ):
        # Base case
        # draw a line and return cursor to start
        ttl.fd(height)
        ttl.back(height)
        return
    
    # Recursive case
    # Draw line, rotate left 45, call drawTree(), 
    #  rotate right 45+45, call drawTree(), rotate left 45, 
    #  move backward to origin
    
    topSize = height/2
    botSize = height/2
    
    randAngle = 45

    ttl.fd(botSize)
    ttl.left(randAngle)
    drawTree(ttl, topSize, generation-1)
    ttl.right(randAngle + randAngle)
    drawTree(ttl, topSize, generation-1)
    ttl.left(randAngle)

    ttl.back(botSize)
    
    return

import math
def drawTreeV2(ttl, height, generation):  
    newWidth = math.sqrt(generation)
    ttl.pensize(newWidth)
    
    if ( generation == 0 ):
        # Base case
        # draw a line and return cursor to start
        ttl.fd(height)
        ttl.back(height)
        return
    
    # Recursive case
    # Draw line, rotate left 45, call drawTree(), 
    #  rotate right 45+45, call drawTree(), rotate left 45, 
    #  move backward to origin
    
    topSize = 4*height/5
    botSize = height/5

    ttl.fd(botSize)
    randAngle = 45 + (generation%2) * (30 * random.random() - 15)
    ttl.left(randAngle)
    drawTree(ttl, topSize, generation-1)
    ttl.right(randAngle)
    
    randAngle = 45 + (generation%2) * (30 * random.random() - 15)
    ttl.right(randAngle)
    drawTree(ttl, topSize, generation-1)
    ttl.left(randAngle)

    ttl.pensize(newWidth)
    ttl.back(botSize)
    
    return
In [17]:
# Reset the screen
turtle.clearscreen()
screen.reset()
screen.screensize(dimX, dimY)

# NOTE: tracer(10) reduces the frames drawn
#  to speed up tracing. It can have unintended
#  consequences when generations are low
turtle.tracer(10)

# Create a turtle
ttl = turtle.Turtle()

ttl.resizemode("user")
ttl.speed(5)
ttl.penup()
ttl.setposition(0, -dimY/2 + 50)
ttl.pendown()

ttl.left(90)
drawTreeV2(ttl, 300, 9)

Tree Drawing Extra Credit

Take some time to play with the tree code. Here are some things you could try (complete at least 3 for extra points)

  • Tinker with the generations to get a nice full tree. One approach is actually to get rid of the generations parameter altogether and use the spiral approach, iterating until the length is smaller than some set limit like 3 or 4 pixels.

  • Changing the base angle from 45 will give you trees with quite different characters. Try some different angles (or even mix them within the same tree) to get a tree you like.

  • Inject randomness into the tree. Perhaps multiple the tree length by a random value or use a random angle.

  • Adjust the width of the turtle’s lines (using turtle.width()) based on the current length so the tree tapers (i.e., make the width a function of the length).

  • Rather than shrinking the length in half every time, experiment with other size reductions like one third, or three quarters.

  • Add color, perhaps even based on the current generation.

  • Make a forest with multiple trees.

  • Do something else not mentioned above.

Make sure the function includes a docstring enumerating the changes you have made.

Here is an example tree with some of these features.

pretty

In [ ]: