Skip to Content
Nextra 4.0 is released 🎉
ReferenceProject GuidesProject 3: Generative Art

Project 3: Generative Art

In this project you’ll build a program that generates original artwork using code. By the end, your program will place random shapes on a canvas and save the result as an image file.

You’ll build it one piece at a time over five phases — each phase adds something new to the project.


Phase 1 — Define Your Color Palette

What you’ll use: Lists, Dictionaries

Your first task is to define the colors your artwork will use. Colors in Pillow are tuples of (red, green, blue) — each value is between 0 and 255.

You can use Coolors  to generate a palette, but you’ll need to convert the hex codes to RGB tuples (e.g. #FF5733 → (255, 87, 51)). This hex to RGB converter  can help.

Tasks:


Phase 2 — Draw Random Circles

What you’ll use: random, Pillow

Install Pillow, create a blank canvas, and use random to scatter circles across it. Your goal is to get an image file saved to your computer by the end of class.

Tasks:

Hint — drawing random circles

You’ll need a loop that runs once per circle. Inside the loop, generate a random x, y, size, and color — then draw the circle.

repeat 50 times: pick a random x between 0 and canvas width pick a random y between 0 and canvas height pick a random size (e.g. between 10 and 100 pixels) pick a random color from the values in your palette dictionary draw a circle at (x, y) with that size and color

img and draw are objects — variables that bundle data and behavior together. draw.circle() is a method — a function that belongs to the draw object. You’ll learn more about this in Phase 3.


Phase 3 — Shape Classes

What you’ll use: Classes

Right now your drawing code generates circles using loose variables — x, y, size, and color are separate values scattered through your loop. The goal this class is to bundle that data together using classes.

Instead of generating random values and immediately drawing them, you’ll create a Circle object that holds its own position, size, and color, and knows how to draw itself. This sets you up for Phase 4 where a Canvas will manage a whole collection of shapes.

What to migrate:

In Class 2 your loop probably looked something like this:

for i in range(50): x = random.randint(0, width) y = random.randint(0, height) size = random.randint(10, 100) color = random.choice(list(palette.values())) draw.circle((x, y), radius=size, fill=color)

By the end of this class, that same circle will be represented as an object:

circle = Circle(x, y, size, color) circle.draw(draw)

Then you can create 50 Circle objects in a loop and draw each of them.

Tasks:

Stretch goal — Add a Rectangle class:

draw_context is the ImageDraw object you created in Phase 2 with ImageDraw.Draw(img). When you call circle.draw(draw), you’re passing that object in so the shape can use it to draw itself onto the canvas. The shape doesn’t create it — it just receives it and uses it.

Hint — structuring a shape class

class Circle: init takes: x, y, radius, color store each as an attribute (self.x, self.y, etc.) draw takes: draw_context use draw_context to draw a circle using self.x, self.y, self.radius, self.color

Phase 4 — The Canvas Class

What you’ll use: Classes, Lists, Loops, Polymorphism

Now build a Canvas class that stores a collection of shapes and renders them all. Here’s the key idea: Canvas doesn’t need to know whether a shape is a Circle or a Rectangle — it just calls draw() on each one and they handle the rest.

What to migrate:

In Phase 3 your code probably looked something like this — creating the image, draw context, and loop all at the top level:

create a blank image with width, height, and background_color create a draw context from the image repeat 50 times: pick a random x, y, size, and color create a Circle with those values call circle.draw(draw context) save the image to "output.png"

By the end of this phase, all of that setup and teardown moves into Canvas:

create a Canvas with width, height, and background_color repeat 50 times: pick a random x, y, size, and color create a Circle with those values call canvas.add_shape(circle) call canvas.render("output.png")

Canvas now owns the image, the draw context, and saving the file — your top-level code just adds shapes and calls render().

Tasks:

Hint — the render loop

render(filename): create a draw context from the image for each shape in self.shapes: call shape.draw(draw_context) save the image to filename

Notice that render doesn’t check what type of shape it has — it just calls draw(). This works because both Circle and Rectangle have a draw() method.


Phase 5 — Polish and Present

Put the finishing touches on your piece and get ready to show it.

Ideas for polishing:

  • Adjust the ratio of each shape type to change the feel of the piece
  • Vary size based on position (e.g. smaller shapes toward the edges)
  • Add a title to your image using ImageDraw.text()
  • Generate several variations and pick the best one

Tasks:

Stretch goal — Refactor with inheritance:

Notice that Circle and Rectangle both store x, y, and color in their __init__. That’s repeated code. Create a Shape base class that holds those shared attributes, then have Circle and Rectangle inherit from it.

Last updated on