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 modifying your current system to make NPR easy to implement. If you are 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 will continue to use material from the ABOP book.

Tasks

Crayon NPR

In lab today we will be editing the Shape class and the Interpreter 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 the classes you have developed so far will work with minimal to no modification. The design choice in our existing system that makes it possible to do this is the use of the Interpreter class to handle all of the actual drawing commands.

To implement various NPR methods, we will enable the Interpreter class to execute the "F" case in different ways, depending on a style field. We will also add a style field to the Shape class so different objects can draw themselves using different styles.

  1. Create a new project 10 folder and copy your lsystem.py, interpreter.py, and shape.py files from your prior assignment into this folder. This week we are writing version 4 of all three files; note this in the comments section at the top of each file.
  2. Open your interpreter file. In the Interpreter.__init__() function, before the test of the Interpreter.initialized class field, add two fields named lineStyle initialized to "normal" and jitterSigma initialized to 2.

    Create methods named setStyle(self, style) and setJitter(self, jitter) that modify the style and jitter fields.

  3. Create a method named Interpreter.forward, that implements the following algorithm.
     
    def forward(self, distance):
        # if self.lineStyle is "jitter"
            # create four random values to use to offset the end points of a line
            # use a Gaussian distribution, for example
            # dx0 = random.gauss(0, self.jitterSigma)
        
            # get the turtle's current position (x0, y0)
            # pick up the turtle
            # have the turtle go forward by distance
            # get the turtle's current position (x1, y1)
            
            # get the turtle's current pen width
            # set the turtle width to (currentWidth + random.randint(0, 3))
            
            # send the turtle to (x0 + dx0, y0 + dy0)
            # put the turtle down
            # send the turtle to (x1 + dx1, y1 + dy1)
            # pick up the turtle
            # send the turtle to (x1, y1)
            # put the turtle down
            # reset the turtle's pen width
        # otherwise
            # move the turtle forward by distance
    
  4. Edit your "F" case in Interpreter.drawString so that it calls self.forward(distance) instead of turtle.forward(distance). Then download and run this test program.
  5. In the Shape class, add lineStyle, jitterSigma, and width fields. Make mutator methods named setStyle, setJitter, and setWidth that enable setting those values.

    Edit the draw method so that it calls the setStyle, setJitter, and setWidth methods of the Interpreter object before calling drawString. Download and run this test program.

L-System Parameters

We will now modify the Interpreter.drawString function to handle parameters on symbols. We will represent parameters as a number inside parentheses in front of the symbol it modifies. For example, the string "FF(120)+F(60)+F(60)+F(120)+" draws a trapezoid by using parameters to modify the left turns.

  1. The drawString method needs to be modified to recognize parentheses (120), interpret the value within parentheses as a numerical value, and then use that value while interpreting the next character in the string.

    At the top of the Interpreter.drawString method, initialize three local variables:

    # assign to parstring the empty string
    # assign to parval the value None
    # assign to pargrab the value False
    

    At the top of the for loop over characters in the input string, put the following conditional statement, above and separate from the one already there.

    # if c is '('
        # assign to parstring the empty string
        # assign to pargrab the value True
        # continue
    # else if c is ')'
        # assign to parval the result of casting parstring to a float
        # assign to pargrab the value False
        # continue
    # else if pargrab (is True)
        # add to parstring the character c
        # continue
    
  2. Edit the 'F' case so it looks like the following.
    # if parval is None
        # call self.forward with the argument distance
    # else
        # call self.forward with the argument distance * parval
    

    Edit your '+', '-', and '!' cases so they all do their normal action if parval is None, but they use parval as the argument to turtle.left, turtle.right, or turtle.width, respectively, if it is not None.

    If you do not have a case for '!', make one now. If parval is not set, this case should decrease the pen width by 1 if the current width is greater than 1.

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

  3. Download and run the following test program.
  4. The goal of these final changes is to enable L-systems parameters that contain a simple variable. For example:
    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 name we will allow is x, and all occurrences of this variable name in a replacement string must be parameterized (in parentheses).

    Rewrite the body of the Lsystem.replace() method with the following algorithm.

    def replace(self, inString):
        # initialize a variable (outString) to the empty string
        # initialize a variable (parstring) to hold the parameter string to ''
        # initialize a variable (parval) to hold the parameter value to None
        # initialize a state variable (pargrab) to False (not grabbing a parameter)
    
        # for each character c in inString
            # if c is '(' then we're starting a parameter
                # assign to parstring the empty string 
                # assign to pargrab value True (now it's grabbing a parameter expr)
                # continue
            # else if c is ')' then we're ending a parameter
                # assign to parval the parstring cast to a float
                # assign to pargrab the value False
                # continue
            # else if pargrab is True
                # add to parstring the character c
                # continue
    
            # if parval is None (no parameter, standard replacement)
                # if c is in self.rules
                    # add to outString a randomly chosen replacement from self.rules[c]
                # else
                    # add to outString the value c
            
            # else (parameter replacement)
                # assign to a local variable (key) the expression '(x)'+c
                # if key is in the dictionary self.rules
                    # assign to a variable (replacement) a random choice from self.rules[key]
                    # add to outString the result of self.substitute( replacement, parval )
                # else
                    # if c is in the dictionary self.rules
                        # assign to a variable (replacement) a random choice from self.rules[c]
                        # add to outString the result of self.insertpar( replacement, parstring, c )
                    # else
                        # add to outString the string '(' + parstring + ')' + c
                # set parval to None
    
        # return outString
    
  5. Copy the following two methods, substitute and insertpar, into your Lsystem class. Make sure to run Entab on the file before continuing.
    def substitute(self, sequence, value ):
        '''
        Evaluate an expression that contains the symbol x, e.g. (4*x)F, after
        replacing x with the given value.
        
        Parameters:
        sequence - a string containing expressions in x
        value - the value to replace for x in the expression
        
        Returns:
        a string where expressions in x are replaced with values
        ''' 
        # an expression string and state variable
        expr = ''
        exprgrab = False
    
        # the output sequence
        outsequence = ''
    
        # for each character in the sequence
        for c in sequence:
    
            # if a parameter expression starts
            if c == '(':
                # set the state variable to True (grabbing the expression)
                exprgrab = True
                expr = ''
                continue
    
            # else if a parameter expression ends
            elif c == ')':
                # set the state variable to False (expression completed)
                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 ) ) + ')'
    
                # add the new numeric parameter to the output sequence
                outsequence += newpar
    
            # else if the state variable is True (grabbing an expression)
            elif exprgrab:
                # add the character to the expression
                expr += c
    
            # else not grabbing an expression and not a parenthesis
            else:
                # add the character to the out sequence
                outsequence += c 
    
        # return the output sequence
        return outsequence
    
    def insertpar(self, sequence, parstring, symbol):
        '''
        Inserts a parameter enclosed in parentheses before each occurrence
        of a symbol within a sequence.
        
        Parameters:
        sequence - a string that contains a symbol
        parstring - a string parameter
        symbol - a symbol to search for
        
        Returns:
        the string result with the parameter inserted
        '''    
        # initialize a return string
        tstring = ''
    
        # for each character in the input string
        for c in sequence:
            # if the character is the symbol
            if c == symbol:
                # add the parameter string in parentheses
                tstring += '(' + parstring + ')'
            # add the character
            tstring += c
        # return the output string
        return tstring
    
  6. Copy your tree.py file from last week into your working directory. Run the final test program using one of the L-systems given below.

When you are done with the lab exercises, get started on the project.