CS 151: Lab 10

Title image Project 10
Fall 2019

Lab Exercise 10: Not Quite Straight Lines

The purpose of this lab is to introduce you to the concept of non-photorealistic rendering [NPR] and give you some practice with designing modifications to your current system to make NPR easy to implement.

If you're interested in seeing more examples of NPR, check out the NPR resources page.

The other piece we'll be implementing this week is how to handle parameterized L-systems. These will give us much more flexibility in defining shapes and complex L-system objects. We'll continue to use material from the ABOP book.


In lab today we'll be editing the Shape class and the TurtleInterpreter class to implement one version of NPR that does a reasonable job of simulating a crayon sketch. The goal is to make the change in such a way that all of the classes you've developed so far will work without modification. The design choice in our existing system that made it possible to do this was the use of the TurtleInterpreter class to handle all of the actual drawing commands.

To implement various NPR methods, we're going to enable the TurtleInterpreter class to execute the 'F' case in different ways. We'll create a field in the TurtleInterpreter class that holds the current style and then draw the line corresponding to the 'F' symbol differently, depending on the style field.

To give the capability to select styles to the Shape objects, we'll also add a style field to the Shape class so different objects can draw themselves using different styles.

  1. Setup

    Create a new project10 folder. Copy your lsystem.py, turtle_interpreter.py, and shapes.py files from your prior assignment (version 3). This week we are writing version 4 of all three files. Label them with a comment as as version 4.

  2. Add TurtleInterpreter fields for style and jitterSigma

    In the TurtleInterpreter class __init__ function, add two fields to the object called style and jitterSigma. Give the style field the value 'normal' and the jitterSigma field the value 2. Be sure to put these assignments before the test of the initialized field of the class.

    Design Note: it is tempting to add additional parameters to __init__ for jitterSigma or style. At some point, having so many parameters to a constructor becomes ridiculous. Since, the mutator methods allow a programmer to change the values of any parameter to something other than the defaults, having everything as a parameter to __init__ is not necessary, though it can be convenient. You are welcome to add them as additional default parameters, but understand that it increases the complexity of the __init__ method and any documentation you might provide.

  3. Create get and set methods for style and jitterSigma

    In the TurtleInterpreter class, create a mutator method def setStyle(self, s) that assigns the style field the value of the parameter s. Then create a mutator method def setJitter(self, j) that assigns the jitterSigma field the value of the parameter j. Write get methods for both parameters as well.

  4. Define a forward method to handle all drawing

    In the TurtleInterpreter class, create a method def forward(self, distance) that implements the following algorithm.

        def forward(self, distance):
            # if self.style is 'normal'
                # have the turtle go foward by distance
            # else if self.style is 'jitter'
                # assign to x0 and y0 the result of turtle.position()
                # pick up the turtle
                # have the turtle go forward by distance
                # assign to xf and yf the result of turtle.position()
                # assign to curwidth the result of turtle.width()
                # assign to jx the result of random.gauss(0, self.jitterSigma)
                # assign to jy the result of random.gauss(0, self.jitterSigma)
                # assign to kx the result of random.gauss(0, self.jitterSigma)
                # assign to ky the result of random.gauss(0, self.jitterSigma)
                # set the turtle width to (curwidth + random.randint(0, 2))
                # have the turtle go to (x0 + jx, y0 + jy)
                # put the turtle down
                # have the turtle go to (xf + kx, yf + ky)
                # pick up the turtle
                # have the turtle go to (xf, yf)
                # set the turtle width to curwidth
                # put the turtle down
  5. Have the 'F' case call the new forward function

    Once you have completed the above function, edit your 'F' case in drawString so that it calls self.forward(distance) instead of turtle.forward(distance). Then download the following test function and try it out. Does it look like the top two shapes are drawn differently?

  6. Update the Shape class to include style and jitterSigma

    In the Shape class, update the __init__ method to add fields for the style, jitterSigma, and line width. Make set and get methods: getStyle, setStyle, getJitter, setJitter, getWidth, and setWidth.

    Edit the draw method so that it calls the turtle interpreter's setStyle, setJitter, and setWidth methods before calling drawString, just like it currently does with color. Then run the following test function.

  7. Enable drawString to handle parameterized strings

    Modify the drawString function to handle parameters for symbols. We're going to represent parameters as a number inside parentheses in front of the symbol it modifies. The string FF(120)+F(60)+F(60)+F(120)+, for example, should draw a trapezoid by modifying the left turns (+ symbols).

    There are notes that talk about the strategy for making this function work. Please read them.

    At the top of the drawString method, initialize three local variables along with the stack and colorstack.

        # assign to modstring the empty string
        # assign to modval the value None
        # assign to modgrab the value False

    At the beginning of the main for loop over the input string, put the following conditional statement, separate from the main one already there that executes drawing commands. This section handles modifiers separately from symbols.

        # if c is equal to '('
            # assign to modstring the empty string
            # assign to modgrab the value True
            # continue
        # else if c is equal to ')'
            # assign to modval the result of casting modstring to a float
            # assign to modgrab False
            # continue
        # else if modgrab (is True)
            # add to modstring the character c
            # continue

    Edit the 'F' case so it looks like the following.

        # if modval is None
            # call self.forward with the argument distance
        # else
            # call self.forward with the argument distance * modval

    Edit the '+', '-', and '!' cases so they all execute their normal action if modval is None, but they use modval as the argument to turtle.left(), turtle.right(), or turtle.width(), respectively, if modval is not None. If you don't have a case for '!', make one now that follows the logic below.

             # if c is '!'
                 # if modval is None
                      # assign to w the result of calling turtle.width()
                      # if w is greater than 1
                          # call turtle.width with w-1 as the argument
                 # else
                      # call turtle.width with modval as the argument

    Finally, assign to modval the value None at the end of the for loop over the input string. This should be inside the for loop, but outside of the big if-else structure. It is important that this is indented properly.

    When you are done, run the following test file.

  8. Enable parameterized L-systems

    The goal of this last change is to enable L-system files of the form:

    base (100)F
    rule (x)F (x)F[!+(x*0.67)F][!-(x*0.67)F]

    The above should replace a trunk with a trunk and two branches, where the branches are shorter than the trunk. The only variable we're going to allow is x.

    Because the replacement process is complex, the follow code implements these changes.

    Open the lsystem.py file and delete the contents of the replace function. Then replace it with the following code.

    + (Code)

        def replace(self, istring):
            """ Replace all characters in the istring with strings from the
                right-hand side of the appropriate rule. This version handles
                parameterized rules.
            tstring = ''
            parstring = ''
            parval = None
            pargrab = False
            for c in istring:
                if c == '(':
                    # put us into number-parsing-mode
                    pargrab = True
                    parstring = ''
                # elif the character is )
                elif c == ')':
                    # put us out of number-parsing-mode
                    pargrab = False
                    parval = float(parstring)
                # elif we are in number-parsing-mode
                elif pargrab:
                    # add this character to the number string
                    parstring += c
                if parval != None:
                    key = '(x)' + c
                    if key in self.rules:
                        replacement = random.choice(self.rules[key])
                        tstring += self.substitute( replacement, parval )
                        if c in self.rules:
                            replacement = random.choice(self.rules[c])
                            tstring += self.insertmod( replacement, parstring, c )
                            tstring += '(' + parstring + ')' + c
                    parval = None
                    if c in self.rules:
                        tstring += random.choice(self.rules[c])
                        tstring += c
            return tstring
  9. Add the two support methods substitute and insertmod

    Copy the following two methods, substitute and insertmod into your Lsystem class. Make sure to run Entab on the file before continuing.

    + (Code)

        def substitute(self, sequence, value ):
            """ given: a sequence of parameterized symbols using expressions
                of the variable x and a value for x
                substitute the value for x and evaluate the expressions
            expr = ''
            exprgrab = False
            outsequence = ''
            for c in sequence:
                # parameter expression starts
                if c == '(':
                    # set the state variable to True (grabbing the expression)
                    exprgrab = True
                    expr = ''
                # parameter expression ends
                elif c == ')':
                    exprgrab = False
                    # create a function out of the expression
                    lambdafunc = eval( 'lambda x: ' + expr )
                    # execute the function and put the result in a (string)
                    newpar = '(' + str( lambdafunc( value ) ) + ')'
                    outsequence += newpar
                # grabbing an expression
                elif exprgrab:
                    expr += c
                # not grabbing an expression and not a parenthesis
                    outsequence += c 
            return outsequence
        def insertmod(self, sequence, modstring, symbol):
            """ given: a sequence, a parameter string, a symbol 
                inserts the parameter, with parentheses, 
                before each
                instance of the symbol in the sequence
            tstring = ''
            for c in sequence:
                if c == symbol:
                    # add the parameter string in parentheses
                    tstring += '(' + modstring + ')'
                tstring += c
            return tstring
  10. Test a parameterized L-system

    The next step is to test the new code with one of the L-systems given below. These L-systems have many interesting features. To understand them better, see these notes. In particular they tells us that the f symbol should make the turtle move forward, just like the F symbol does.

    So, as a mini-step, add support to drawString for the f symbol. Have it call self.forward.

    Run the final test function using one of the L-systems given above. E.g, you can run it with sysTree.txt, 3 iterations, a distance of 3, and an angle of 22.5.

  11. Final testing

    As a final test to show what is possible, try out this test function. It requires your turtle_interpreter.py, shapes.py, and tree.py to generate the scene. Run it with sysTree2.txt or sysTree3.txt as the command-line argument.

When you are done with the lab exercises, you may begin the project.