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 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 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 Interpreter class to handle all of the actual drawing commands.
To implement various NPR methods, we're going to enable the Interpreter class to execute the 'F' case in different ways. We'll create a field in the Interpreter 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.
- Create a new working folder. Copy your lsystem.py, interpreter.py, and shape.py files from your prior assignment (version 3). This week we are writing version 4 of all three files. Label them as version 4.
- Open your interpreter file. In the Interpreter class __init__ function, add two fields to the object called linestyle and jitterSigma. Give the linestyle field the initial 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.
- In the Interpreter class, create a method def style(self, s) that sets the linestyle field to the argument s. Then create a method def jitter(self, j) that sets the jitterSigma field to the argument j.
In the Interpreter class, create a method def forward(self,
distance) that implements the following algorithm.
def forward(self, distance): # if self.linestyle is 'normal' # have the turtle go foward by distance # else if self.linestyle 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 results 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
- 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.
- Open your shape.py file. In the Shape class, add fields for the linestyle and jitterSigma. Then make mutators called setStyle and setJitter to enable you to set those values. Then edit the draw method so that it calls the style and jitter methods of the Interpreter before calling drawString. Then run the following test file.
Go back to your interpreter.py file. Now we're going to modify the
drawString function to handle parameters on 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.
At the top of the drawString method, initialize three local variables.
# assign to parstring the empty string # assign to parval the value None # assign to pargrab the value False
Then, at the top of the main for loop over the input string, put the following conditional statement, separate from the main 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 False # continue # else if pargrab (is True) # add to parstring the character c # continue
Edit your '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 don't have a case for '!', make one now that follows the logic below.
# if c is '!' # if parval 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 parval as the argument
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.
When you are done, run the following test file.
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, and all instances of a parameterized symbol in a replacement string must be parameterized.
Open your lsystem.py file and delete the contents of your replace function. Then write the following algorithm.
def replace(self, istring): # initialize a variable (tstring) to the empty string, this is our output 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 the input string istring # 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 not None # 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 tstring 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 tstring the result of self.insertpar( replacement, parstring, c ) # else # add to tstring the string '(' + parstring + ')' + c # set parval to None # else (no parameter, so just a standard replacement rule) # if c is in self.rules # add to tstring a randomly chosen replacement from self.rules[c] # else # add to tstring the value c # return tstring
Copy the following two methods, substitute and
insertpar into your lsystem file. Make sure to run Entab on
the file before continuing.
# 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 def substitute(self, sequence, value ): # 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 # given: a sequence, a parameter string, a symbol # # inserts the parameter, with parentheses, before each # instance of the symbol in the sequence def insertpar(self, sequence, parstring, symbol): # 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
- Run the final test function using one of the L-systems given below.
Once you have finished the lab, go ahead and get started on project 10.