Ok, so now we can generate a whole bunch of L-System strings. Now what do we do with them?
What we'll do is make objects that know how to deal with each individual character in the iteration. First, though, let's think a little bit about our interface. It would be nice to be able to interact with our objects like this:
1 2 | dragon = LSystem('the same stuff we gave lindenmayer') dragon.draw(10) |
and have the 10th iteration of the dragon fractal drawn. Generators are a little difficult to deal with like that, so we'll throw together a quick class that allows us to access different indices on a generator:
1 2 3 4 5 6 7 8 9 10 | class GeneratorList(object): def __init__(self, generator): self.__generator = generator self.__list = [] def __getitem__(self, index): for _ in range(index - len(self.__list) + 1): self.__list.append(self.__generator.next()) return self.__list[index] |
That's pretty simple, it's just a generator and a list used like a cache.
Now we have to make some objects that can deal with the lindenmayer iterations. We'll make a class called LSystem. It will be a GeneratorList, and the generator will be made from lindenmayer.
To draw an iteration we'll iterate over each character in the iteration string and look up what to do. We find out what to do by looking up the character in an actions dictionary.
The code's pretty simple. Remember that self[n] is the nth iteration of the lindenmayer generator, because LSystem inherits GeneratorList.
1 2 3 4 5 6 7 8 | class LSystem(GeneratorList): def __init__(self, axiom, rules): super(LSystem, self).__init__(lindenmayer(axiom, rules)) def draw(self, index): for char in self[index]: if char in self.actions: self.actions[char]() |
Now we just need to make the actions dictionary. Common L-Systems use only a few different operations, three of which we've already seen. The most common actions are:
- F: move forward
- +: turn right
- -: turn left
- G: go forward, without drawing anything
And then there's two other ones that aren't as common, but which are very useful:
- [: save the current drawing state
- ]: return to the most recent saved state
For these two guys, we'll need to keep a states stack, where we'll push and pop the drawing states.
Finally, we should also pass the L-System two more pieces of data: angle, the amount to turn on + or -, and heading, the initial heading in degrees.
Now our LSystem looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class LSystem(GeneratorList): def __init__(self, axiom, rules, angle, heading=0): self.states = [] self.angle = angle self.heading = heading self.actions = { 'F': self.forward, '+': self.right, '-': self.left, 'G': self.go, '[': self.save, ']': self.restore, } super(LSystem, self).__init__(lindenmayer(axiom, rules)) def draw(self, index): for char in self[index]: if char in self.actions: self.actions[char]() |
To get it working, all we have to do is implement forward, left, right, go, save, and restore. Which means that we need to tie things to a graphics system. Which means we need to choose the graphics system.
At this point, we could wrap everything into a module, open another module, choose a graphics system, and extend LSystem in a manner specific to that graphics system. Then, if we ever want to switch graphics systems, we'd just extend LSystem in a different way, and we'd never tie the L-System to the renderer.
There's a word for that. That word is "over-engineering". So instead, we'll just go with python turtle graphics, because this is a quick exercise, not a robust application.
Despite the title of this entry, that's all of the "actually doing something" that we'll do right now. Over in the next entry, though, we'll start generating some images.
-
Python Fractals
- Step 1: Choosing a language
- Dragons Ahead
- Lindenmayer
- Lindenmayer Generator
- Actually Doing Something
- Making some images
- Color Time