An Introduction to Graphics in Python

Learning objectives

In this code along, we will give a short introduction to graphics in Python, which will help us in future code alongs to visualize the Game of Life and other cellular automata. We will then use this introduction to make a simple but beautiful drawing of a face that showcases our artistic prowess.

Code along summarypy

Setup

To complete this code along as well as other code alongs relying on graphics in this and following chapters, you will use a popular Python graphics library called pygame, which will help us draw objects and generate images.

First, install pygame by opening a command line terminal and executing the command pip3 install pygame (on macOS/Linux) or pip install pygame (on Windows).

In python/src, create a folder drawing, then create main.py inside it with the following contents. Note that we import the pygame package at the top.

import pygame

def main():
    print("Drawing a head.")


if __name__ == "__main__":
    main()

Understanding Pygame: our library for drawing

The pygame library allows us to create a graphical window for our programs. We will cover more about object-oriented programming in a future chapter, but for now, think of a pygame window as a rectangular screen with a specified width and height, similar to the screen you are reading this on. The width and height are measured in pixels, where each pixel is a tiny point on the screen that can be colored individually.

In the RGB color model, every rectangular pixel on a computer screen emits a single color formed as a mixture of differing amounts of the three primary colors of light: red, green, and blue (hence the acronym “RGB”). The intensity of each primary color in a pixel is expressed as an integer between 0 and 255, inclusive, with larger integers corresponding to greater intensities.

A few colors are shown in the figure below along with their RGB equivalents; for example, magenta corresponds to equal parts red and blue. Note that a color like (128, 0, 0) contains only red but appears duskier than (255, 0, 0) because the red in that pixel is less intense.

A collection of colors along with their RGB codes. This table corresponds to mixing colors of light instead of pigment, which causes some non-intuitive effects; for example, yellow is formed by mixing equal parts red and green. The last six colors appear muted because they only receive half of a given color value compared to a color that receives 255 units. If all three colors are mixed in equal proportions, then we obtain a color on the gray scale between white (255, 255, 255) and black (0, 0, 0). Source: Excel at Finance.

The functions from pygame that we will need in this and the next code along are listed below. However, a best practice for using Python libraries is to refer to the online API provided here to understand functions and objects in the library: https://www.pygame.org/docs/. We will say more about these functions as they are needed. (Links are provided for specific documentation of each function below for those who are interested.)

  • pygame.Surface.fill(): Fills the Surface with a solid color. (link)
  • pygame.draw.circle(): draw a circle. (link)
  • pygame.draw.rect(): draw a rectangle. (link)
  • pygame.init(): initialize all imported pygame modules. (link)
  • pygame.Surface(): pygame object for representing images. (link)
  • pygame.image.save(): save an image to file. (link)
  • pygame.quit(): uninitialize all pygame modules. (link)

Creating a blank canvas

In main(), we initialize pygame by calling pygame.init(). Even though we won’t open a window, pygame still needs to set up its internal modules before we can create surfaces or draw anything.

def main():
    print("Drawing a head.")
    
    # Initialize pygame but don't open window
    pygame.init()

Rather than placing all our drawing code inside def main(), we will call a function named draw_face(). This function will not take any inputs or return anything, but it will contain code for drawing our image.

def main():
    print("Drawing a head.")
    
    # Initialize pygame but don't open window
    pygame.init()

    draw_face()

After we have drawn the face, we call pygame.quit() to shut down all Pygame modules and release all resources. This step is important to prevent memory leaks and save memory and properly close Pygame’s internal systems.

def main():
    print("Drawing a head.")
    
    # Initialize pygame but don't open window
    pygame.init()

    draw_face()

    pygame.quit()

We will next implement draw_face(). The first thing that we will do is to create a canvas object, which we will call c.

First, in main.py, we will create a Pygame “surface” by calling the function pygame.Surface(), which takes a tuple (width, height) of two integers as input and returns a Surface object that is width pixels wide and height pixels tall. The Surface object acts like a virtual canvas that we can draw on. “Drawing” on surface will not show anything on the screen as of yet; surface only exists in memory.

def draw_face() -> None:
    """
    Draws a simple cartoon face onto the given Pygame surface.
    The face consists of:
      - A large white head (circle)
      - Two black eyes (circles)
      - A small black nose (circle)
      - A red rectangular mouth
    Args:
        None
    Returns:
        None
    """

    # Create an off-screen surface
    width, height = 1000, 2000
    surface = pygame.Surface((width, height))

    # to fill in...
Note: The function pygame.Surface((width, height)) is a constructor in Python. This creates a new Surface object and stores that in the variable surface. This Surface object surface, has functions “in it” which can be accessed by doing surface.function_name(). We will discuss constructors and object-oriented programming in greater detail in the future.

Next, we will define some colors using RGB values. black = (0, 0, 0) represents the absence of light, white = (255, 255, 255) is full light for all three channels, and red = (255, 0, 0) is just red. Pygame functions will take in these 3-int tuples for color parameters.

def draw_face() -> None:
    """
    Draw a simple cartoon face on a new off-screen Pygame Surface and save it.
    The face consists of:
      - A large white head (circle)
      - Two black eyes (circles)
      - A small black nose (circle)
      - A red rectangular mouth
    Args:
        None
    Returns:
        None
    """

    # Create an off-screen surface
    width, height = 1000, 2000
    surface = pygame.Surface((width, height))

    black = (0, 0, 0)
    white = (255, 255, 255)
    red = (255, 0, 0)

    # fill in

Next, we paint the entire surface with black by calling surface.fill(black).

def draw_face() -> None:
    """
    Draw a simple cartoon face on a new off-screen Pygame Surface and save it.
    The face consists of:
      - A large white head (circle)
      - Two black eyes (circles)
      - A small black nose (circle)
      - A red rectangular mouth
    Args:
        None
    Returns:
        None
    """

    # Create an off-screen surface
    width, height = 1000, 2000
    surface = pygame.Surface((width, height))

    black = (0, 0, 0)
    white = (255, 255, 255)
    red = (255, 0, 0)
    
    # Fill canvas with black
    surface.fill(black)

    # fill in
Note: surface.fill is a method (a function that is part of the Surface object) that fills the entire surface with the input color. More information on how to define these functions for objects will be discussed in a future code along.

We can convert surface to an image by saving it to a PNG so that the image file can be viewed outside of Python. We call the function pygame.image.save() to produce this PNG, which we call "fun.png".

def draw_face() -> None:
    """
    Draw a simple cartoon face on a new off-screen Pygame Surface and save it.
    The face consists of:
      - A large white head (circle)
      - Two black eyes (circles)
      - A small black nose (circle)
      - A red rectangular mouth
    Args:
        None
    Returns:
        None
    """

    # Create an off-screen surface
    width, height = 1000, 2000
    surface = pygame.Surface((width, height))

    black = (0, 0, 0)
    white = (255, 255, 255)
    red = (255, 0, 0)
    
    # Fill canvas with black
    surface.fill(black)

    # save to file
    pygame.image.save(surface, "fun.png")

Even though we have not finished creating our face, let’s run our code so that we can see what is produced.

STOP: In a new terminal window, navigate into our directory using cd python/src/drawing. Then run your code by executing python3 main.py (macOS/Linux) or python main.py (Python).

After running your code, you should see the achievement of modern art shown below appear as fun.png in your python/src/drawing directory.

A thrilling black rectangle.

The graphics coordinate system

Before adding a head to our drawing, we need to understand the pygame coordinate system. In the Cartesian plane that we are accustomed to working with in mathematics, increasing x-coordinates extend to the right, and increasing y-coordinates extend upward (see figure below).

In the classic Cartesian plane, x-coordinates increase to the right, and y-coordinates increase upward. Three example points are shown according to this representation.

However, graphics uses a different standard that dates to the foundation of computing. Early computers drew an image to the screen starting in the top left corner of the screen and extending right and down. As a result, many graphics packages (pygame included) still use the standard of viewing the top left corner of a window as an “origin”, with increasing x-coordinates extending to the right, and increasing y-coordinates extending downward (see figure below).

In graphics, x-coordinates increase to the right, and y-coordinates increase downward. Three example points are shown according to this representation; note that the horizontal positions of these points are the same as the figure on the left, but their vertical position is inverted.

Adding a head

We will now add a head to surface, which will appear as a white circle. To do so, we will call the pygame.draw.circle() function. Remember when your geometry teacher told you that a circle is defined by its center and its radius? We know that you don’t remember it, but it is nevertheless true. We will now use this fact by calling pygame.draw.circle(), which takes four parameters as input:

  • surface (pygame.Surface): the surface you want to draw on.
  • color (tuple or pygame.Color): the color of the circle, e.g. (255, 0, 0) for red.
  • center (tuple of two ints): the (x, y) coordinates of the circle’s center.
  • radius (int): the radius of the circle in pixels.

We want the head to lie in the top part of the rectangle, halfway across from left to right. Because the canvas is 1000 pixels wide, we know that the x-coordinate of the circle’s center should be 500. In order to have the same amount of space on the top, left, and right, let’s make its y-coordinate 500 as well; that is, the circle’s center will be 500 pixels from the top. As for the radius, let’s set it to be 200 pixels.

As a result, we will call pygame.draw.circle(surface, white, (500, 500), 200). This function call should occur after we fill the canvas with black, and before we save to PNG.

def draw_face() -> None:
    """
    Draws a simple cartoon face onto the given Pygame surface.
    The face consists of:
      - A large white head (circle)
      - Two black eyes (circles)
      - A small black nose (circle)
      - A red rectangular mouth
    Args:
        None
    Returns:
        None
    """

    # Create an off-screen surface
    width, height = 1000, 2000
    surface = pygame.Surface((width, height))

    black = (0, 0, 0)
    white = (255, 255, 255)
    red = (255, 0, 0)
    
    # Fill canvas with black
    surface.fill(black)

    # add head (white circle)
    pygame.draw.circle(surface, white, (500, 500), 200)

    # save to file
    pygame.image.save(surface, "fun.png")

When we run our code again, we obtain the white circle below as fun.png.

Adding a big round head to our drawing.

Adding facial features to our head

Let’s next add a nose, which we will draw as a small black circle, whose center will be slightly below the center of the head. The nose’s x-coordinate will therefore be the same as that of the head, but the y-coordinate will be a little bit larger at 550 pixels. As for the radius, let’s make it ten pixels. And the nose’s color should be black.

def draw_face() -> None:
    # previous code here

    # add head (white circle)
    pygame.draw.circle(surface, white, (500, 500), 200)
    
    # add nose (black circle)
    pygame.draw.circle(surface, black, (500, 550), 10)

    # save to file
    pygame.image.save(surface, "fun.png")

When we run our code once more, we see the figure shown below.

Do you think my nose is too small?

Next, we will add two black eyes, which are once again circles. The eyes will fall slightly above the center of the face, and so we will make their y-coordinates equal to 475. Let’s move them to 75 pixels left and right of the center line, so that their x-coordinates will be 425 and 575. We will set their radii equal to 15 to make the eyes a little bigger than the nose.

def draw_face() -> None:
    # previous code here

    # add head (white circle)
    pygame.draw.circle(surface, white, (500, 500), 200)
    
    # add nose (black circle)
    pygame.draw.circle(surface, black, (500, 550), 10)
    
    # add eyes (black circles)
    pygame.draw.circle(surface, black, (425, 475), 15)
    pygame.draw.circle(surface, black, (575, 475), 15)

    # save to file
    pygame.image.save(surface, "fun.png")

Our updated face is shown in the figure below and is starting to take shape as a landmark achievement of Western art.

Starting to take, uh, shape.

Finally, we will make a mouth, which gives us the opportunity to draw a different color, red.

We will also make the mouth rectangular, which will allow us to use the function pygame.draw.rect(). This function takes the following parameters as input:

  • surface (pygame.Surface): the surface you want to draw the rectangle on.
  • color (tuple or pygame.Color): the color of the rectangle, e.g. (255, 0, 0) for red.
  • rect (tuple[int, int, int, int]): A 4-tuple (x, y, width, height) specifying the rectangle’s top-left x- and y-coordinates along with width and height, in pixels.

The function then creates a rectangle at the indicated location with the given dimensions and color. In our case, we will make the rectangle representing the mouth 200 pixels wide and 20 pixels tall, and so the x-coordinates will range from 400 to 600, and the y-coordinates will range from 600 to 620.

def draw_face() -> None:
    # previous code here

    # add head (white circle)
    pygame.draw.circle(surface, white, (500, 500), 200)
    
    # add nose (black circle)
    pygame.draw.circle(surface, black, (500, 550), 10)
    
    # add eyes (black circles)
    pygame.draw.circle(surface, black, (425, 475), 15)
    pygame.draw.circle(surface, black, (575, 475), 15)
    
    # add mouth (red rectangle)
    pygame.draw.rect(surface, red, (400, 600, 200, 20))

    # save to file
    pygame.image.save(surface, "fun.png")

Running our full code produces our final face, as shown below.

I am smiling.
Note: Feel free to continue editing our face. Add a body, some arms, or change colors however you like to get the hang of working with pygame.

Looking ahead

Now that we understand a bit more about drawing, we are ready to apply what we have learned to draw cellular automata. Please join us in the next code along to do so!

Page Contents
Scroll to Top