Time to add some colors. If you've downloaded by code in the last step, you'll notice that I left some hooks for color in the code. For one thing, the save and restore functions save and restore the turtle's color. For another thing, there's a stub for the update function which gets called on every draw. So let's add some colors.
The goal is to have colors rotate through the color wheel. The way we'll do that is as follows:
We'll take the color as a 3-tuple of numbers between 0 and 255. Whenever we want to find the next color, we will either be increasing one channel or decreasing the other. For example, imagine that we start out on red. Here's how we want our color wheel to work:
- Start on red
- Increase the green channel until you get yellow
- Decrease the red channel until you get green
- Increase the blue channel until you get teal
- Decrease the green channel until you get blue
- Increase the red channel until you get purple
- Decrease the blue channel until you're back to red
As you can see, we have to alternate between increasing and decreasing, and which channel we're modifying. If we just finished increasing a channel, then we hop back a channel and start decreasing that. If we just finished decreasing a channel, then we hop forward two channels and start increasing that. Like this:
1 2 3 4 5 6 7 8 9 10 11 | def rotate(color, channel, amount): current = color[channel] if current is 255 and amount > 0: # We just finished increasing direction *= -1 channel = (channel - 1) % 3 elif current is 0 and amount < 0: # We just finished decreasing direction *= -1 color[channel] = min(255, max(0, current + amount)) return color, channel, amount |
That will do the rotation for us. It takes (color, channel, amount) as parameters and returns the next (color, channel, amount) to use. So we could have our fractals calling this thing to rotate the color wheel and keeping track of (color, channel, amount) all on their own, but that's a little much to ask. It could get dirty and repetitive to have every differently colored fractal thread its own colors. So let's make a ColorWheel class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | class ColorWheel: RED, YELLOW, GREEN, TEAL, BLUE, PURPLE = range(6) # These modes are (color, channel, direction) tuples # used to initialize the internal state from a given color # For example, at 'red', the color is (255, 0, 0) and we are # going towards yellow, so green (channel 1) is increasing (+1) # then at yellow we are going towards green, so red (channel 0) # is decreasing (-1), and so on. __modes = { 'red': ((255, 0, 0), 1, 1), 'yellow': ((255, 255, 0), 0, -1), 'green': ((0, 255, 0), 2, 1), 'teal': ((0, 255, 255), 1, -1), 'blue': ((0, 0, 255), 0, 1), 'purple': ((255, 0, 255), 2, -1), } def __init__(self, color=RED, brightness=1, mode=255): self.max = mode if mode in (1.0, 255) else 255 self.goto(color, brightness) def clamp(self, n): """ Return n or the closest value that is a valid color given our color mode """ return min(self.max, max(0, n)) def goto(self, color=None, brightness=None): """ You may set color as a string from __modes (not case sensitive) or by passing in a new color tuple. You may change brightness by passing in a new brightness between 0 and 1 """ if isinstance(color, str) and color.lower() in self.__modes: color, self.channel, self.direction = self.__modes[color.lower()] self.__color = list(color) elif isinstance(color, tuple): self.__color = list(map(self.clamp, color[:3])) if brightness is not None: self.brightness = min(1, max(0, brightness)) return self.color @property def color(self): return tuple(map(lambda c: c * self.brightness, self.__color)) def rotate(self, amount): current = self.__color[self.channel] if current == self.max and self.direction > 0: self.direction *= -1 self.channel = (self.channel - 1) % 3 elif current == 0 and self.direction < 0: self.direction *= -1 self.channel = (self.channel + 2) % 3 delta = self.direction * amount self.__color[self.channel] = self.clamp(current + delta) return self.color |
All that just to get a color wheel? Yes, but now check out how easy it is to make the dragon fractal be colored:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class Dragon(LSystem): def __init__(self, turtle): super(Dragon, self).__init__(turtle, 'FX', {'X': 'X+YF', 'Y': 'FX-Y'}, 90) self.colors = ColorWheel('purple') self.__cap, self.__current = 1, 0 def update(self): if self.__cap == self.__current: self.__cap *= 2 self.turtle.pencolor(self.colors.rotate(40)) self.__current += 1 def draw(self, *args, **kwargs): self.turtle.pencolor(self.colors.color) super(Dragon, self).draw(*args, **kwargs) |
We extend LSystem to Dragon because we assume that not all fractals are colored equally. The dragon fractal keeps a color wheel, which it rotates with halving frequency. In other words, it rotates on the first step, the second step, the fourth step, the eight step, and so on. This means that every time the dragon doubles, the color changes. Which creates a pretty cool effect:
And now our fractals have colors.