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.
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 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
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 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 char is '('
# assign to modstring the empty string
# assign to modgrab the value True
# continue
# else if char 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 char
# 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 char is '!'
# if modval is None
# assign to width the result of calling turtle.width()
# if width is greater than 1
# call turtle.width with width-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.
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.
We will be supplying the code for these changes.
Open your lsystem.py file and delete the contents of your applyRules function, replacing it with the following code.
def applyRules(self, inputString):
""" Replace all characters in the istring with strings from the
right-hand side of the appropriate rule. This version handles
parameterized rules.
"""
resultString = ''
parstring = ''
parval = None
pargrab = False
for char in inputString:
if char == '(':
# put us into number-parsing-mode
pargrab = True
parstring = ''
continue
# elif the character is )
elif char == ')':
# put us out of number-parsing-mode
pargrab = False
parval = float(parstring)
continue
# elif we are in number-parsing-mode
elif pargrab:
# add this character to the number string
parstring += char
continue
if parval != None:
key = '(x)' + char
if key in self.rules:
replacement = random.choice(self.rules[key])
resultString += self.substitute( replacement, parval )
else:
if char in self.rules:
replacement = random.choice(self.rules[char])
resultString += self.insertmod( replacement, parstring, char )
else:
resultString += '(' + parstring + ")" + char
parval = None
else:
if char in self.rules:
resultString += random.choice(self.rules[char])
else:
resultString += char
return resultString
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 = ''
continue
# 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
else:
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
So, as a mini-step, add support to drawString for the f symbol.
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.