Mondrian Art Generator

Tags: large, artistic, bext

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 algorithm works by creating a data structure (the canvas dictionary) with randomly spaced vertical and horizontal lines.

Next, it removes some of the line segments to create larger rectangles.

Finally, the algorithm randomly fills some rectangles with yellow, red, blue, or black.

You can find another version of this Mondrian art generator at https://github.com/asweigart/mondrian_art_generator/ along with several sample images.

mondrian_art_generator.py
  1"""Mondrian Art Generator, by Al Sweigart al@inventwithpython.com
  2Randomly generates art in the style of Piet Mondrian.
  3More info at: https://en.wikipedia.org/wiki/Piet_Mondrian
  4This code is available at https://nostarch.com/big-book-small-python-programming
  5Tags: large, artistic, bext"""
  6
  7import sys, random
  8
  9try:
 10    import bext
 11except 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:
 18MIN_X_INCREASE = 6
 19MAX_X_INCREASE = 16
 20MIN_Y_INCREASE = 3
 21MAX_Y_INCREASE = 6
 22WHITE = 'white'
 23BLACK = 'black'
 24RED = 'red'
 25YELLOW = 'yellow'
 26BLUE = 'blue'
 27
 28# Setup the screen:
 29width, 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:
 32width -= 1
 33
 34height -= 3
 35
 36while 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:

  • Create programs with different color palettes.

  • Use the 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/.

https://inventwithpython.com/bigbookpython/project46.html