Piet Mondrian was a 20th-century Dutch painter and one of the founders of neoplasticism, an abstract art movement. His most iconic paintings relied on blocks of primary colors (blue, yellow, red), black, and white. Using a minimalist approach, he separated these colors with horizontal and vertical elements.
This program generates random paintings that follow Mondrian’s style. You can find out more about Piet Mondrian at https://en.wikipedia.org/wiki/Piet_Mondrian.
The bext
module allows our Python program to display bright primary colors in the text output, even though this book only shows black-and-white images. Figure 47-1 shows what the output will look like when you run mondrian.py.
The algorithm works by creating a data structure (the canvas
dictionary) with randomly spaced vertical and horizontal lines, as in Figure 47-2.
Next, it removes some of the line segments to create larger rectangles, as shown in Figure 47-3.
Finally, the algorithm randomly fills some rectangles with yellow, red, blue, or black, as in Figure 47-4.
You can find another version of this Mondrian art generator at https://github.com/asweigart/mondrian_art_generator/ along with several sample images.
1. """Mondrian Art Generator, by Al Sweigart [email protected]
2. Randomly generates art in the style of Piet Mondrian.
3. More info at: https://en.wikipedia.org/wiki/Piet_Mondrian
4. This code is available at https://nostarch.com/big-book-small-python-programming
5. Tags: large, artistic, bext"""
6.
7. import sys, random
8.
9. try:
10. import bext
11. except ImportError:
12. print('This program requires the bext module, which you')
13. print('can install by following the instructions at')
14. print('https://pypi.org/project/Bext/')
15. sys.exit()
16.
17. # Set up the constants:
18. MIN_X_INCREASE = 6
19. MAX_X_INCREASE = 16
20. MIN_Y_INCREASE = 3
21. MAX_Y_INCREASE = 6
22. WHITE = 'white'
23. BLACK = 'black'
24. RED = 'red'
25. YELLOW = 'yellow'
26. BLUE = 'blue'
27.
28. # Setup the screen:
29. width, height = bext.size()
30. # We can't print to the last column on Windows without it adding a
31. # newline automatically, so reduce the width by one:
32. width -= 1
33.
34. height -= 3
35.
36. while True: # Main application loop.
37. # Pre-populate the canvas with blank spaces:
38. canvas = {}
39. for x in range(width):
40. for y in range(height):
41. canvas[(x, y)] = WHITE
42.
43. # Generate vertical lines:
44. numberOfSegmentsToDelete = 0
45. x = random.randint(MIN_X_INCREASE, MAX_X_INCREASE)
46. while x < width - MIN_X_INCREASE:
47. numberOfSegmentsToDelete += 1
48. for y in range(height):
49. canvas[(x, y)] = BLACK
50. x += random.randint(MIN_X_INCREASE, MAX_X_INCREASE)
51.
52. # Generate horizontal lines:
53. y = random.randint(MIN_Y_INCREASE, MAX_Y_INCREASE)
54. while y < height - MIN_Y_INCREASE:
55. numberOfSegmentsToDelete += 1
56. for x in range(width):
57. canvas[(x, y)] = BLACK
58. y += random.randint(MIN_Y_INCREASE, MAX_Y_INCREASE)
59.
60. numberOfRectanglesToPaint = numberOfSegmentsToDelete - 3
61. numberOfSegmentsToDelete = int(numberOfSegmentsToDelete * 1.5)
62.
63. # Randomly select points and try to remove them.
64. for i in range(numberOfSegmentsToDelete):
65. while True: # Keep selecting segments to try to delete.
66. # Get a random start point on an existing segment:
67. startx = random.randint(1, width - 2)
68. starty = random.randint(1, height - 2)
69. if canvas[(startx, starty)] == WHITE:
70. continue
71.
72. # Find out if we're on a vertical or horizontal segment:
73. if (canvas[(startx - 1, starty)] == WHITE and
74. canvas[(startx + 1, starty)] == WHITE):
75. orientation = 'vertical'
76. elif (canvas[(startx, starty - 1)] == WHITE and
77. canvas[(startx, starty + 1)] == WHITE):
78. orientation = 'horizontal'
79. else:
80. # The start point is on an intersection,
81. # so get a new random start point:
82. continue
83.
84. pointsToDelete = [(startx, starty)]
85.
86. canDeleteSegment = True
87. if orientation == 'vertical':
88. # Go up one path from the start point, and
89. # see if we can remove this segment:
90. for changey in (-1, 1):
91. y = starty
92. while 0 < y < height - 1:
93. y += changey
94. if (canvas[(startx - 1, y)] == BLACK and
95. canvas[(startx + 1, y)] == BLACK):
96. # We've found a four-way intersection.
97. break
98. elif ((canvas[(startx - 1, y)] == WHITE and
99. canvas[(startx + 1, y)] == BLACK) or
100. (canvas[(startx - 1, y)] == BLACK and
101. canvas[(startx + 1, y)] == WHITE)):
102. # We've found a T-intersection; we can't
103. # delete this segment:
104. canDeleteSegment = False
105. break
106. else:
107. pointsToDelete.append((startx, y))
108.
109. elif orientation == 'horizontal':
110. # Go up one path from the start point, and
111. # see if we can remove this segment:
112. for changex in (-1, 1):
113. x = startx
114. while 0 < x < width - 1:
115. x += changex
116. if (canvas[(x, starty - 1)] == BLACK and
117. canvas[(x, starty + 1)] == BLACK):
118. # We've found a four-way intersection.
119. break
120. elif ((canvas[(x, starty - 1)] == WHITE and
121. canvas[(x, starty + 1)] == BLACK) or
122. (canvas[(x, starty - 1)] == BLACK and
123. canvas[(x, starty + 1)] == WHITE)):
124. # We've found a T-intersection; we can't
125. # delete this segment:
126. canDeleteSegment = False
127. break
128. else:
129. pointsToDelete.append((x, starty))
130. if not canDeleteSegment:
131. continue # Get a new random start point.
132. break # Move on to delete the segment.
133.
134. # If we can delete this segment, set all the points to white:
135. for x, y in pointsToDelete:
136. canvas[(x, y)] = WHITE
137.
138. # Add the border lines:
139. for x in range(width):
140. canvas[(x, 0)] = BLACK # Top border.
141. canvas[(x, height - 1)] = BLACK # Bottom border.
142. for y in range(height):
143. canvas[(0, y)] = BLACK # Left border.
144. canvas[(width - 1, y)] = BLACK # Right border.
145.
146. # Paint the rectangles:
147. for i in range(numberOfRectanglesToPaint):
148. while True:
149. startx = random.randint(1, width - 2)
150. starty = random.randint(1, height - 2)
151.
152. if canvas[(startx, starty)] != WHITE:
153. continue # Get a new random start point.
154. else:
155. break
156.
157. # Flood fill algorithm:
158. colorToPaint = random.choice([RED, YELLOW, BLUE, BLACK])
159. pointsToPaint = set([(startx, starty)])
160. while len(pointsToPaint) > 0:
161. x, y = pointsToPaint.pop()
162. canvas[(x, y)] = colorToPaint
163. if canvas[(x - 1, y)] == WHITE:
164. pointsToPaint.add((x - 1, y))
165. if canvas[(x + 1, y)] == WHITE:
166. pointsToPaint.add((x + 1, y))
167. if canvas[(x, y - 1)] == WHITE:
168. pointsToPaint.add((x, y - 1))
169. if canvas[(x, y + 1)] == WHITE:
170. pointsToPaint.add((x, y + 1))
171.
172. # Draw the canvas data structure:
173. for y in range(height):
174. for x in range(width):
175. bext.bg(canvas[(x, y)])
176. print(' ', end='')
177.
178. print()
179.
180. # Prompt user to create a new one:
181. try:
182. input('Press Enter for another work of art, or Ctrl-C to quit.')
183. except KeyboardInterrupt:
184. sys.exit()
After entering the source code and running it a few times, try making experimental changes to it. On your own, you can also try to figure out how to do the following:
Pillow
module to produce image files of Mondrian art. You can learn about this module from Chapter 19 of Automate the Boring Stuff with Python at https://automatetheboringstuff.com/2e/chapter19/.Try to find the answers to the following questions. Experiment with some modifications to the code and rerun the program to see what effect the changes have.
canvas[(x, y)] = WHITE
on line 41 to canvas[(x, y)] = RED
?print(' ', end='')
on line 176 to print('A', end='')
?