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 project10 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 with a comment as 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 mutator method def style(self, s) that assigns the linestyle field the value of the parameter s. Then create a mutator method def jitter(self, j) that assigns the jitterSigma field the value of the parameter 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, update your __init__ method to add fields for the linestyle, jitterSigma, and line width. Then make mutators called setStyle, setJitter, and setWidth to enable a programmer to set those values. Then edit the draw method so that it calls the interpreter's style, jitter, and width methods of the before calling drawString, just like it currently does with color. Then run the following test function.
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 (+ symbols).
At the top of the drawString method, initialize three local variables along with your 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. This section handles modifiers separately from symbols.
# if c is '(' # assign to modstring the empty string # assign to modgrab the value True # continue # else if c is ')' # 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 your '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 your '+', '-', and '!' cases so they all do their normal action if modval is None, but they use modval 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 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.
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.
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 (modstring) to hold the parameter string to '' # initialize a variable (modval) to hold the parameter value to None # initialize a state variable (modgrab) 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 modstring the empty string # assign to modgrab value True (now it's grabbing a parameter expr) # continue # else if c is ')' then we're ending a parameter # assign to modval the modstring cast to a float # assign to modgrab the value False # continue # else if modgrab is True # add to modstring the character c # continue # if modval 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, modval ) # 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.insertmod( replacement, modstring, c ) # else # add to tstring the string '(' + modstring + ')' + c # set modval 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
insertmod 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 insertmod(self, sequence, modstring, 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 += '(' + modstring + ')' # 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.