Title: Draw Fibonacci squares in Python
My previous post, Use three important techniques to calculate Fibonacci numbers in Python, shows how to calculate Fibonacci numbers. This example uses those numbers to make a sequence of squares with side lengths equal to the Fibonacci numbers. The program then resizes the squares so the fit nicely on the window.
Square Orientation
Positioning the squares isn't hard but it can be a bit confusing because it's easy to lose track of how a square's position relates to the previous squares.
Before you start writing drawing code, it's very helpful to think about exactly how you want to orient each square. I decided to start each square at its lower right corner and then add the points counterclockwise.
The picture on the right shows the program displaying the squares' indices 0, 1, 2, 3, etc. The points that define square number 6 begin at the large dot and then follow the direction of the red arrow. Notice that the text is oriented so it points "up" for each square.
Also notice that each square's width equals to the sums of the widths of the two previous squares. For example, square 6 has the same width as the sum of the widths of squares 4 and 5. Similarly square 7's width equals the sum of the widths of squares 5 and 6. This is the recurrence relation that defines the Fibonacci numbers.
You can use other orientations than this one; it'll just change the following discussion about how the squares are oriented. (In fact, it might make a good exercise to see if you can make the program use a different orientation.)
Positioning Squares
Having picked an orientation, we can position the squares. The key is to decide, given the points that define some squares, how to generate the points that define the next one. Here's the code that this program uses to define the squares.
def get_fibonacci_squares(num_squares):
'''Return a list of Vector2 points forming Fibonacci squares.'''
# Initialize the first 2 squares.
squares = [
[(0, 0), (0, -1), (1, -1), (1, 0)],
[(1, -1), (2, -1), (2, 0), (1, 0)],
]
squares = [[pygame.Vector2(point) for point in square]
for square in squares]
# Generate other squares.
for i in range(2, num_squares):
# Make square number i (with numbers starting at 0).
# It shares an edge with squares[i - 1] and squares[i - 2].
# p0 is at the upper left corner of the previous square.
p0 = squares[i - 1][2]
# Get the direction of the first side.
v01 = squares[i - 1][2] - squares[i - 1][1]
v01.scale_to_length(fibonacci(i + 1))
# Get the second point.
p1 = p0 + v01
# Rotate v01 by 90 degrees to get the next side direction.
v12 = v01.rotate(90)
# Get the third and fourth points.
p2 = p1 + v12
p3 = p2 - v01
# Save the square.
squares.append((p0, p1, p2, p3))
# Flip the Y coordinates.
squares = [[pygame.Vector2(point.x, -point.y) for point in square]
for square in squares]
return squares
This code starts by defining the first two squares. Look again at the previous picture and focus on the first two squares. The little dot at the spiral's beginning shows the origin where square number 0 starts.
The code first defines the squares' vertices as X/Y coordinate pairs and then uses a list comprehension to convert them into pygame.Vector2 objects to make it easier to transform the points.
The method then enters a loop to create the other squares. The first point in square i is the same as the third point in square i - 1. For example, look at the previous picture and consider square 7. Its first point (in its lower right corner) is at the same spot as square 6's 3rd vertex (in its upper left corner). In code that means p0 = squares[i - 1][2].
To get the rest of the square's vertices, the code defines vectors pointing in the proper directions and adds them to point p0.
For square 7, the first edge points to the left in the picture. That's the same direction as the edge in square 6 that points from its second point squares[i - 1][1] to its third point squares[i - 1][2]. The code subtracts those points to get the vector v01 points from square 7's first point to its second point.
This is the main reason why I represent the points as pygame.Vector2 objects. You can subtract two points to get the vector between them. You can also easily add, scale, and rotate vectors. If you need a refresher on vector arithmetic, see my post A brief primer on vector arithmetic in Pygame and Python.
Having found a vector pointing from square 7's first point to its second point, the code scales that vector to the correct length. That length is simply Fibonacci(i + 1). The + 1 is there because the first Fibonacci number is 0 and we're not drawing a square with width 0. There are other ways you could handle this, but this one seems easiest.
The code adds the scaled vector to point p0 to get point p1 in square 7.
Next, the code rotates the vector by 90 degrees and adds the result to point p1 to get point p2.
Finally, the code subtracts vector v01 from point p2 to get point p3. (Subtracting a vector is the same as reversing it and then adding. That's what we need here because square 7's third edge points in the opposite direction from its first edge.)
Having found the new square's vertices, the program adds them as a tuple to the squares list.
After it finishes finding all of the squares, the method uses a list comprehension to negate the points' Y coordinates to flip the whole thing upside down. That's necessary because video monitors use the goofy inverted coordinate system where Y coordinates increase downward. If you don't do this, you still get a sequence of squares, they just curl in the other direction so they don't match the earlier pictures.
Once it has finished generating and then flipping the squares, the method returns the squares list.
Drawing Squares
The following code shows the program's main drawing method.
def draw(self):
'''Draw the Fibonacci squares.'''
# Get the number of squares.
num_squares = self.num_squares_var.get()
# Get the drawing area coordinates.
tk.Tk.update(self.canvas)
canvas_wid = self.canvas.winfo_width()
canvas_hgt = self.canvas.winfo_height()
# Get the squares.
squares = get_fibonacci_squares(num_squares)
# Transform the squares to fit the canvas.
margin = 10
rect = (margin, margin, canvas_wid - margin, canvas_hgt - margin)
squares = transform_point_lists(squares, rect)
# Convert the Vector2 objects to (x, y) coordinate pairs.
squares = [[(point.x, point.y) for point in square]
for square in squares]
# See what text we should display.
text_to_draw = self.text_var.get()
# Draw the squares.
colors = ['pink', 'yellow', 'light green', 'light blue', 'orange']
self.canvas.delete(tk.ALL)
outline = 'blue'
for i, square in enumerate(squares):
fill = colors[i % len(colors)]
self.canvas.create_polygon(square, fill=fill, outline=outline)
# Get the square's centroid.
cx = (square[0][0] + square[1][0] +
square[2][0] + square[3][0]) / 4
cy = (square[0][1] + square[1][1] +
square[2][1] + square[3][1]) / 4
# Set the text.
if text_to_draw == 'Sizes':
# Display each square's size, which is its Fibonacci number.
p = fibonacci(i + 1)
text = f'{p}'
elif text_to_draw == 'Indexes':
# Display the index.
text = f'{i}'
else:
text = ''
# Get the text's angle.
angle = 90 * ((i + 2) % 4)
# Get the square's height.
base = math.dist(square[0], square[1])
hgt = base / math.sqrt(3)
num_digits = len(text)
font_size = int(hgt / (num_digits + 0.5))
# Draw the text.
self.canvas.create_text(cx, cy, text=text, fill='black',
font=('Arial', font_size, 'bold'),
angle=angle)
# Draw the origin.
origin = squares[0][0]
radius = (squares[0][2][0] - squares[0][0][0]) / 10
self.canvas.create_oval(
origin[0] - radius, origin[1] - radius,
origin[0] + radius, origin[1] + radius,
fill='black')
The method first gets the desired number of squares from the program's spinbox. Next, it updates the drawing canvas and gets its dimensions.
The code then calls get_fibonacci_squares to get the squares. It creates a rectangle that defines the area where it should draw the squares and then calls transform_point_lists to transform the squares to fit that rectangle. For information about how that function works, see my post Map points so they fit within a target area in Python.
The square-generation and transformation code works with pygame.Vector2 objects, so the code uses a list comprehension to convert those into the X, Y coordinate pairs that tkinter understands.
At this point we're almost ready to draw, but we need to know what text to display. To learn that, the code gets the selection (Sizes, Indexes, or None) from the text combobox. The program clears the drawing canvas and then enters a loop to draw the squares. It creates a polygon for each square, finds the square's center, and composes the text that we need to display.
Next, the code performs a short calculation to pick a font size that will make the text a reasonable size for the current square. Finally, the program draws the square's text rotated to show the square's orientation.
Finally, after it has drawn the squares, the program draws a dot at the origin to make it easier to figure out where the whole thing starts.
Conclusion
This program draws squares with side lengths equal to the Fibonacci numbers. If you look at the pictures, you can see how each square is lined up with the two previous squares. Download the example to experiment with it and to see additional details.
|