Title: Fill a spiral of Theodorus with a color gradient in Python
Sometimes it's useful to map numeric values to and from colors. For example, intensity of color could indicate population density, agricultural yield, rainfall, or other values on a map.
This program draws a spiral of Theodorus with sections shaded in red, yellow, green, aqua, blue, fuchsia, and red again. (You can draw a rainbow using just red, green, blue, and red again but it's not as bright and the intermediate colors yellow, aqua, and fuchsia don't show up well.)
For more information on drawing the spiral of Theodorus, see my post Draw a colored spiral of Theodorus in Python. For information about mapping numbers to and from colors, read on.
This example includes two functions: one that maps colors to numbers and one that maps numbers to colors.
Colors to Numbers
One key idea of this mapping is to ignore the smallest color component value. For example, if the color's red, green, and blue components are 255, 255, and 128, then this color is mostly red and green with less blue, so the code ignores the blue component.
This is necessary because the gradient only contains saturated colors. These colors have at most 2 non-zero color components. Other non-saturated colors are hues of the basic colors. They are basically the saturated colors with extra white or gray added.
The following function maps colors to numbers.
def color_to_rainbow_number(rgb):
'''
Map a color to a rainbow number between 0 and 1 on the
Red-Yellow-Green-Aqua-Blue-Fuchsia-Red rainbow.
'''
# See which color is weakest.
r, g, b = rgb
if (r <= g) and (r <= b):
# Red is weakest. It's mostly blue and green.
g -= r
b -= r
if g + b == 0:
return 0
return (2 / 6 * g + 4 / 6 * b) / (g + b)
elif (g <= r) and (g <= b):
# Green is weakest. It's mostly red and blue.
r -= g
b -= g
if r + b == 0:
return 0
return (1 * r + 4 / 6 * b) / (r + b)
else:
# Blue is weakest. It's mostly red and green.
r -= b
g -= b
if r + g == 0:
return 0
return (0 * r + 2 / 6 * g) / (r + g)
This method starts by deciding which color component to ignore. It then finds the color's location by using a weighted average of the remaining color components' positions in the gradient. For example, consider the color (30, 60, 120). The red component is smallest so the code ignores it. The green and blue components are 60 and 120. Green is located at position 2/6 on the gradient and blue is at 4/6 so the location for this color is given by this weighted average:
If the green component is small and the blue component is large, the result is close to blue. If the two are close to equal, the result is about halfway between green and blue.
Numbers to Colors
The following rainbow_number_to_color function maps a number to a color.
def rainbow_number_to_color(number):
'''
Map a rainbow number between 0 and 1 to a color on the
Red-Yellow-Green-Aqua-Blue-Fuchsia-Red rainbow.
'''
r = g = b = 0
if number < 1 / 6:
# Mostly red with some green.
r = 255
g = int(r * (number - 0) / (2 / 6 - number))
elif number < 2 / 6:
# Mostly green with some red.
g = 255
r = int(g * (2 / 6 - number) / (number - 0))
elif number < 3 / 6:
# Mostly green with some blue.
g = 255
b = int(g * (2 / 6 - number) / (number - 4 / 6))
elif number < 4 / 6:
# Mostly blue with some green.
b = 255
g = int(b * (number - 4 / 6) / (2 / 6 - number))
elif number < 5 / 6:
# Mostly blue with some red.
b = 255
r = int(b * (4 / 6 - number) / (number - 1))
else:
# Mostly red with some blue.
r = 255
b = int(r * (number - 1) / (4 / 6 - number))
return (r, g, b)
This code is fairly complicated but the idea is simple. The code examines the number to see in which sixth of the gradient it lies. It then sets the color component closest to that section of the gradient to 255. For example, if the color is close to green, the code sets the green component to 255.
Next the code uses the weighted average formulas shown previously to find the other color component's value. The new formulas are simply the previous formulas solved for the appropriate color component values. For example, if the color is close to green between green and blue, the code sets the red component to 0, green to 255, and solves the weighted average equation to find the corresponding blue component.
Filling the Spiral
Here's the code the example program uses to draw the spiral of Theodorus. The code that colors the spiral is highlighted in blue.
def draw_theodorus_spiral(self, edge_points):
'''Draw the spiral of Theodorus.'''
# Clear the canvas.
self.canvas.delete(tk.ALL)
# Transform the points so they fit nicely on the canvas.
margin = 10
rect = [margin, margin,
self.canvas.winfo_width() - margin - 1,
self.canvas.winfo_height() - margin - 1]
edge_points, dx, dy = transform_points(edge_points, rect)
# Define color parameters.
num = 1
d_num = 1 / (len(edge_points) - 2)
# Draw the spiral.
for i in range(len(edge_points)-1, 1, -1):
triangle = [
(dx, dy),
(edge_points[i][0], edge_points[i][1]),
(edge_points[i-1][0], edge_points[i-1][1]),
]
color = rgb_to_hex(rainbow_number_to_color(num))
num -= d_num
self.canvas.create_polygon(triangle, fill=color, outline='black')
The code draws the spiral much as before (see the post Draw a colored spiral of Theodorus in Python) but it adds the new colors. Before it starts drawing the spiral's triangles, the code sets num to 1 and d_num equal to 1 divided by the number of triangles minus 1.
Inside the loop, it calls rainbow_number_to_color to convert num into a color and then subtracts d_num from num. That makes num start at 1 and end at 0 (or a value close to 0).
The code passes the rainbow color to the rgb_to_hex function to convert the RGB tuple into a color string that tkinter can understand and uses the result to fill the spiral's triangle.
Conclusion
The key in this example is the rainbow_number_to_color function that maps a number between 0 and 1 onto the rainbow. The program then uses some simple math to color the spiral's triangles with different colors in the rainbow. This example uses a Red-Yellow-Green-Aqua-Blue-Fuchsia-Red rainbow, but you could modify it to use other color gradients.
Download the example to experiment with it and to see additional details.
|