Unit 5: Working with Graphics
5.0. Overview
We can certainly use the Python language in the context of a text-based console, but it's also possible to leverage the individual pixels on our computer screens. Python has some very powerful graphics capabilities built right into the language. In this unit, however, we'll be using the Processing platform to create some powerful graphics experiences, both static and animated.
Let's get started.
5.1.0. Graphics and Python
The Turtle module comes built into Python, and allows you to immediately create fascinating graphics on your computer screen. You can learn about the Turtle module by searching for information online, or ask the instructor for further information.
There are a number of other platforms available for working with graphics in Python. You may see references to Tkinter, a powerful (and complex) set of Python utilities. PyGame is another platform that is a little tricky to get installed at first, but provides excellent support for graphics, sound, and "listeners" that allow for complex gameplay interactions.
For our exploration in here, we're going to be using the Processing platform. Although this platform is based on Java, it has a Python-mode that allows us to Python-style syntax to sketch out amazing graphical demonstrations in a simple but powerful development environment.
We'll write some games, too!
5.1.1. Windows and Pixels
Your computer screen, and windows displayed on our computer screen, are made up of thousands of picture elements, or pixels. These pixels are used to display light of varying color and intensity, and when working with computer graphics (as opposed to a text-based display), you have the ability to control these pixels individually.
To control these pixels we need to have some way of addressing them individually. Each pixel on the window is identified by its column, row, where the upper-left corner of the screen is 0, 0. For a window that is 640 pixels wide and 480 pixels in height, the lower-right pixel would have the location 639, 479.
The window
The screen or window is a rectangular grid of pixels, each addressed by an x-y pair of coordinates.
X runs across the top of the screen, and is indexed from 0 to the width - 1.
Y runs down the left side of the screen, and is indexed from 0 to the height - 1.
For a 640 x 480 window:
5.2.0. The Processing Environment
Processing is an open-source, Java-based programming language that specializes in allowing one to experiment with creative programs producing graphical results. You can learn more about the language from its website at processing.org and from its Wikipedia article.
Follow the steps below to download and install processing, and begin experimenting with Processor's Integrated Development Environment, the Processor Development Environment (PDE).
5.2.1. Download Processing and Python Mode
- Go to the Processing download page. Download the software for your computer and follow instructions on your computer to install it.
- Start the Processing program. Processing ordinarily is based on a Java syntax, but we're going to install a Python mode to make things easier for us. Click on the dropdown menu in the upper right that says "Java" and select "Add Mode..."
- From the "Modes" tab select "Python Mode for Processing 3..." and then click the "Install" button.
- Once the Python mode has finished installing, select Python mode from the dropdown menu in the upper right corner of the screen. Now we can use Python in Processing.
After all your hard work getting Processing installed, let's take a break by watching the first 5 minutes or so of the introductory video available at hello.processing.org. It's a good introduction to some of the incredible creative power of coding with Processing.
5.2.2. A Processing program
Once it has been installed on your computer, start up the Processing application. Once you click past the startup window, you should see the main development window on your screen.
It's in this window that you'll be able to enter Processing commands, run programs, view Console output in the pane below, and view graphical content in a window for that purpose off to the side.
5.2.2.1. Hello, World!
It's a classic program, so we might as well get it out of the way. You don't need to write a main program, however, and you don't need to write a class, and you don't even have to compile the program you're writing. Simply enter this line of code and click on the right-triangle "Run" button.
Click the Run button and sure enough, the desired announcement appears in the Console pane below.
Most Processing programs don't use the console at all—the emphasis in this platform is on creating graphical effects—but the console can be very useful in debugging a program. If you're not sure about what's happening in a program, use some print() statements to reveal what your variables are doing. You can use that information to help you figure out what's going on.
5.2.2.2. Saving your Processing program
Processing likes to save your programs in the directory ~/Processing/, although your work doesn't have to be stored there. You can put your Processing projects wherever you want.
You do have to keep your main processing program in a folder that has the same name as the program. So, if you save the program we just wrote as hello_world, there will be a file created, hello_world.pyde, that is save in a folder hello_world. If you decide to change the name of the file, you'll also need to change the name of the folder it contains.
(The file extension pyde is what identifies this file as being a Python-based Processing file.)
5.2.3. Commands, shapes
There are a number of graphical shapes that you can draw and experiment with. Typically, you'll want to use one of the instructions for defining the width of a line or the fill color that will be used:
Then you can use one of the many geometric shapes provided by Processing. Here are a few to begin experimenting with:
You can enter these commands into the "sketch" code window in Processing, then hit the Run icon to see what they produce.
5.2.4. Color and Transparency
Color (of light) is different from pigments, which are what most artists work with.
The primary light colors are red, green, and blue, and by combining these colors in varying degrees, all the colors of the rainbow can be produced.
The diagram to the right indicates that red light and green light, when combined, will produce a yellow color. (If you were to combine red and green pigments, on the other hand, you'd have a brown/gray mess.)
To specify different colors for use in your artwork you can indicate how much of each primary color—red, green, and blue—you wish to use, with a value from 0-255.
So, if you want "white" to be the background of your window, you'd use the instruction
To specify the color of your ellipse as magenta, just before creating the ellipse you'd use the instruction
Take a few moments to try creating different ellipses on different colored backgrounds.
5.2.4.1. The Alpha channel
In addition to specifying to a very precise degree the amount of red, green, and blue light you wish in a color, you can also indicate the transparency of the color. A color with an "alpha channel" (transparency) of 0 will be completely transparent, while a color with an alpha of 255 will be completely opaque.
The alpha channel is specified as an optional fourth parameter with any given color. So,
would indicate a color "cyan" (aqua), that is 50% transparent. Objects drawn beneath this ellipse will be visible, but obscured to some degree by the cyan color on top.
Copy/enter this code into processing, and use the mouse to change the transparency value of the rightmost circle to see what kinds of effects this can produce.
5.2.5. Save your work
You can always screenshot your work, but it's easier just to have Processing export the file for you.
If you want to save the sketch that you've made, include a
command at the bottom. A graphic image of your window will be saved in the home directory of your sketch, probably inside the directory ~/Processing on your computer.
5.2.6. Listeners
The real strength of Processing comes with it's ability to easily allow for interacting with the code during runtime.
A listener is a bit of code that monitors input devices for activity. A mouse listener tracks the mouse's x- and y- coordinates on the screen, and whether or not any of the mouse's button's have been pressed or released. (We used a mouse listener in the example above.) A keyboard listener constantly checks to see if any keys on the keyboard have been pressed, and if so, which one.
We can write our own Listeners in Java, but the Processing platform includes code that supports listening automatically.
A typical Processing program would consist of two standard methods: the setup() method which establishes some of the initial parameters of the program: window dimensions, background color, etc. (Think of it as a constructor.) The subsequent draw() method, which will (normally) loop repeatedly, executes whatever instructions are given inside that method.
Enter the follow methods and then run the program to see what happens.
You can make this program a little more interactive by checking to see what the state of the mousebutton is when the program is running:
5.2.7. Incorporating random values
The random(n) method produces a random float value between 0 and n. You can convert that to an int value by casting as needed.
Here's a simple program that demonstrates that idea:
5.2.8. Working with objects in Processing
Processing uses the Java syntax, but there are some differences between Processing and Java. Processing only uses int and float values, in an effort to minimize computational churn and maximize the speed that a program can run at.
You've already seen that we println() information—there is no System.out.println() in Processing.
You can use objects, of course, and the more complex your project, the more you'll want to write your code as objects.
5.2.8.1. GraphicalRandomWalker
GraphicalRandomWalker
Using the Processing platform, write a sketch GraphicalRandomWalker that uses a Walker() class.
The Walker class
The Walker class tracks the x- and y-coordinates of a walker. It includes
- a constructor with int parameters x and y to establish the walker's initial position
- accessor methods .getX() and .getY() that return the values of those variables
- mutator methods .setX(int newX) and .setY(int newY) that reset the values of those variables
- the .move() method which causes the walker's position to change, usually by one step in the x or y direction
The GraphicalWalker class
In Processing, the main program (in this case GraphicalRandomWalker) has to be the first, leftmost tab in the project. Click on the down-arrow in the tab to create a new tab for additional classes (in this case Walker) that should be included in the project.
The main program in the sketch will include the setup() method to establish a graphical window and a draw() loop that displays the walker and its position as long as the walker hasn't returned to the middle of the screen. Consider using separate colors to identify the origin, the walker, and the walker's path as it moves.
Also, print in the console the coordinates of the walker and the number of steps it has taken.
Note that you don't need to set up an explicit loop for this project: the draw() method itself repeats.
5.3.0. Working with photos
In this section we'll use nested loops to process a real-world object: a digital image.
5.3.1. Nested Loops and Graphics Files
We've talked about the classic nested loop construction, which is especially good for looking at data that's organized in a table format, or by column-row. One classic context for this is photo data, or graphics files in a .jpg or .png format.
5.3.2. Pixels and Images
Digital images are composed of "picture elements", or pixels, where each pixel in the photo (displayed on screen or printed on paper) is represented by a colored dot.
We'd like to be able to go through a picture pixel-by-pixel, across all the rows and columns of the photo, and get information about the color of that pixel, and (if we're editing the photo) set the color of the pixel.
The Processing platform provides support for inspecting and creating graphical images. Take a look at this example program to see a demonstration of some of the basic possibilities.
If you want to experiment with this file, create a new project in Processing, save it as ImageProcessingBasics, and copy-paste this code into the file editor. Note that running this program requires an image file called image.jpg, which has to be located in the same project folder as the program.
It's quite easy to obscure details in an image by using a "blur" strategy.
Is it also possible to go the other direction, to take a blurry photograph and "enhance" it so that formerly hidden details can be seen?
These movies and TV shows certainly seem to think so!
5.4.0. Processing: Animation
The final step in our introduction to computer graphics is more interesting because it's dynamic. We need to introduce some motion to our graphics.
The basic principle of creating the illusion of motion is based on classic animation techniques: the human eye takes a small amount of time to perceive what it sees. If we can change what the eye is seeing (a "frame") more frequently than the rate at which the eye takes in those frames, we'll perceive those distinct frames as motion.
If the frame rate is slower than what we can perceive—a strobe light firing in a dark room while a person dances—we see the frames as separate. If the frame rate is faster than what we can perceive—a video game playing on a computer with a powerful graphics card—we see the frames as representing fluid, continuous motion.
We've already used the Processing platform to draw some simple graphics. We're going to do that again, but this time we'll have them move.
5.4.1. Creating a Ball class
- In a new project in Processing, click the "down triangle" to create create a new tab that will hold describe the Ball class. This class is an abstraction that is constructed with a ball's radius, an initial x and y position, and an initial velocity in both the x and y directions. Write appropriate getters and setters for these, as well as a .move(float t) method that mutates x and y based on the ball's change in position during a time period t. (Physics reminder: for a non-accelerating object, xfinal = xinitial + vxt)
- As part of the Ball class, also write a .toString() method that returns a string containing information about the ball's current state, ie. the state of all its variables. This will be useful in debugging issues later on.
5.4.2. Creating the main sketch
If you've already written the Ball class, it's time to write the runner program that will use it.
- The first tab of your Processing "sketch" is where the "main" program should go. In Processing this includes the global variables we'll use, the setup() function, and the draw() function which repeats over and over.
Let's set up some initial values:
""" This Processing sketch creates a bouncing ball """ import ball def setup(): global time, x, y, vX, vY, radius, b size(800, 600) # creates the window background(255) # background white smooth() # attempts to remove glitches noStroke() # no outline on the ball radius = 30 x = width / 2 y = height / 2 vX = 4 # initial values of x- and... vY = 0 # ... y-velocities time = 0.8 # time interval b = ball.Ball(x, y, vX, vY, radius) def draw(): global time, x, y, vX, vY, radius, b background(255) # clear the screen fill(255, 0, 0) # set fill to red for the ball circle(b.getX(), b.getY(), b.getRadius() * 2) print(b) # displays ball info in console (debugging) - Now we need to add the Ball class that we wrote so that Processing can use it. Click the down-arrow button next to the name of the sketch in the Processing window, and choose New Tab. In the new window that's created, copy-paste the Ball class definition.
#!/usr/bin/env python3 """ ball.py This program defines a Ball class, and includes a tester to that indicates how it works. """ __author__ = "Richard White" __version__ = "2020-12-12" class Ball(object): """The Ball class tracks the position, velocity, and acceleration of a ball in two-dimensional (x,y) space. """ def __init__(self, x, y, vX, vY, radius): self.x = x self.y = y self.vX = vX self.vY = vY self.radius = radius def getX(self): return self.x def getY(self): return self.y def setX(self, newX): self.x = newX def setY(self, newY): self.y = newY def getvX(self): return self.vX def getvY(self): return self.vY def setvX(self, newvX): self.vX = newvX def setvY(self, newvY): self.vY = newvY def getRadius(self): return self.radius def move(self, deltaT): self.x = self.x + self.vX * deltaT self.y = self.y + self.vY * deltaT return self.x, self.y def __str__(self): return "Ball[x=" + str(self.x) + ",y=" + str(self.y) + \ ",vX=" + str(self.vX) + ",vY=" + str(self.vY) + \ ",radius=" + str(self.radius) + "]"
- Run the sketch. It should show the ball in the middle of the screen. You should also be able to read the ball's status in the console window.
- Is the ball moving? Not yet! We have to call the ball's .move(time) method in the draw() loop. Add that line and try running the program now.
5.4.3. Collision detection
At this point our ball is animating nicely, but it disappears pretty quickly from the screen. Let's see if we can get it to bounce off the edge of the screen so that it stays within the frame.
- In that draw() method, right after we've called the move() method, let's check to see if the ball has moved off the screen. If it has, we'll have it change its direction:
def draw(): global time, x, y, vX, vY, radius, b background(255) # clear the screen fill(255, 0, 0) # set fill to red for the ball circle(b.getX(), b.getY(), b.getRadius() * 2) print(b) # displays ball info in console (debugging) b.move(time) if (b.getX() > width): b.setvX(-1 * b.getvX())Does that fix the problem?
- It does, partially, but there are two more issues that need to be dealt with now: one is that the ball now flies off the left side of the screen now. We'll need to modify our if condition to address that issue. Go ahead and do that now.
- The other is a bit of a detail, but an important one: when the ball is bouncing, say, from the right side of the window, it isn't bouncing when it reaches the right edge—it's bouncing when its center reaches the right side of the screen, and that looks odd. It should bounce not when the center of the ball hits the edge, but when the right side of the ball hits the edge. Where is that right side located, relative to the center? It's b.getX() + b.getRadius(). Modify the if statement appropriately.
- Finally, make modifications to the sketch so that the ball, with an initial y-velocity, will also bounce off the top and bottom edges of the window.
- Once you've got that working, consider some variations. What if the ball loses energy every time it hits an edge, by having its velocity diminish by 10%? What does that look like? Are there any issues that develop in the process of implementing this variation? )
5.4.4. x, v, and... a?
Modify your program so that the ball is subject to a vertical acceleration downwards, just as balls do in the real world. You'll need to modify your ball class so that it is constructed with an acceleration value, and so that its x, y, vX, and vY values are all modified accordingly.