Unit 6: Working with Graphics
Topics covered in this unit
After completing this unit you will:
- be able to describe the terms screen, window, and pixel
- be able to use x-y coordinates to address individual pixels in a window
- be able to use the Processing environment to create simple geometric shapes in a window
- know the hexadecimal-based red-green-blue system for specifying colors
- use
setup()
anddraw()
methods to create programs with Listener loops - be able to use nested row-column loops to process digital photos
- be able to write an object with a
.move()
method to create simple graphical animation
6.0. Overview
We can certainly use the Java language in the context of a text-based console, but it's also possible to leverage the individual pixels on our computer screens. Java has some very powerful graphics capabilities, and this unit introduces some of them.
Let's get started.
6.1.0. Graphics and Java
Java has a powerful set of libraries that are used for graphical displays. One set of libraries, Swing, focuses on Graphical User Interfaces (GUI), with tools for easily displaying buttons and fields of information. If you were to write a calculator application, this is the approach you would use.
There are additional libraries that give you even more control over the pixels on your screen. With this increased power comes increased complexity, and writing Java programs to produce graphical displays can be enormously challenging, especially at this stage in your CS career.
Fortunately for us, much of the complexity of these classes has been nicely encapsulated in various toolkits. For our purposes, for now, we're going to be using the Processing language, which is based on Java and allows us to sketch out some powerful graphical demonstrations in a simple but powerful development environment.
6.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:
6.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).
6.2.1. Download and install Processing
Download and install Processing by following the instructions at https://processing.org . Processing is open source and cross-platform, so you only need to select the version that is appropriate for your computer.
6.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.
6.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.
println("Hello, World!");
Click the Run button and sure enough, the desired announcement appears in the Console pane below.
6.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:
background(0-255); // sets the grayscale color of the background screen background(0-255, 0-255, 0-255); // sets the red,green,blue (RGB) color of the background screen stroke(0-255); // defines the grayscale color of a line stroke(r, g, b); // describes an RGB line color strokeWeight(thickness); // describes the thickness of the line noStroke(); // if you don't want a border on your shapes fill(0-255); // describes a grayscale fill color fill(r, g, b); // describes an RGB fill color
Then you can use one of the many geometric shapes provided by Processing. Here are a few to begin experimenting with:
ellipse(centerX, centerY, width, height); point(X, Y); // highlights a point on the window triangle(x1, y1, x2, y2, x3, y3); // creates a triangle with these vertices quad(...); // creates a quadrilateral rect(x, y, width, height); // creates a rectangle line(x1, y1, x2, y2); // creates a line
You can enter these commands into the "sketch" code window in Processing, then hit the Run icon to see what they produce.
6.2.4. Color and Transparency
Color (of light) is different from colors of pigments. The color "red" is electromagnetic radiation of a specific wavelength, like the red light coming from your computer screen, or a stoplight, or the sun at sunset. The pigment red refers to an object with that appearance: an apple has a red pigment, Santa's coat has a red pigment, the red ink on a homework assignment is a red pigment.
As we are discussing light emitted from a display screen in this unit, we'll be referring to these various hues as "colors."
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
background(255, 255, 255) # red, green, and blue values
To specify the color of your ellipse as magenta, just before creating the ellipse you'd use the instruction
fill(255, 0, 255) # all red, no green, all blue = magenta
Take a few moments to try creating different ellipses on different colored backgrounds.
6.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,
fill(0, 255, 255, 128)
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.
void setup() { size(640, 480); // screen dimensions background(255, 255, 255); // RGB background } void draw() // repeats over & over { float transp = map(mouseY, 0, height, 0, 255); // magic! print(transp); // print transp in console fill(255, 0, 255); // first color is RB = magenta ellipse(300, 240, 100, 100); // draw ellipse using magenta fill(255, 255, 0, transp); // change paintbrush to yellow, with transparency ellipse(350, 240, 100, 100); // draw yellow ellipse on top of magenta ellipse }
6.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
save("drawing.png");
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.
6.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.
void setup() { size(640, 480); // establishes the size of the graphics window background(255, 255, 255); // Red, Green, Blue values to produce a white background smooth(); // Smoothes the rendering of the graphics } void draw() // The "draw" method is called repeatedly { fill(255, 0, 0); // R, G, B color for red ellipse(mouseX, mouseY, 50, 50); // Draws an ellipse with the upper-left corner at // the mouse's location, 50 pixels wide and 50 high }
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:
void draw() { if (mousePressed) { fill(0); } else { fill(255); } ellipse(mouseX, mouseY, 80, 80); }
6.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:
void setup() { size(640, 480); background(0); } void draw() { float randRed = random(256); float randGreen = random(256); float randBlue = random(256); float randX = random(width); float randY = random(height); fill(randRed, randGreen, randBlue); ellipse(randX, randY,100,100); }
6.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.
6.2.8.1. PixelWalker
PixelWalker
Using the Processing platform, write a sketch PixelWalker
that uses a RandomWalker()
class.
The RandomWalker
class
The RandomWalker
class tracks the x
- and y
-coordinates of a walker. It includes
- a constructor with
int
parametersx
andy
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 thex
ory
direction
The PixelWalker
class
In Processing, the main program (in this case PixelWalker
) 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 RandomWalker
) that should be included in the project.
The main program in the sketch will include the void setup()
method to establish a graphical window and a void 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.
6.3. Working with photos
In this section we'll use nested loops to process a real-world object: a digital image.
6.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.
6.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.
6.3.3. Colors
Each dot has a color represented by a series of red, green, and blue values, from 0-255.
Some color combinations:
Color(255,0,0)
represents the color redColor(255,255,0)
is the color yellowColor(255,255,255)
is whiteColor(127,127,127)
is greyColor(0,0,0)
is black
Let's take a look at a program that will show us a picture as well as the RGB values of all the pixels in that photo.
Demo - PictureEditor
This class that I've written here, PictureEditor
, uses another class called Picture
that was created by some computer science people at Princeton. That class has been bundled with some files that you can use for our work over the next couple of days.To use the Picture
class, just make sure a copy of it is included in each project directory that you want to use it in. That way your project will have access to it.
Can you identify what this program will do to the image file?
/** * Write a description of class PictureEditor here. * * An image file (.jpg, .png, .gif) is a good place to practice using nested loops. * Each "picture element" (pixel) can be referred to by its column and row address. * * Each pixel itself is of a Color that includes red, green, and blue values of 0-255. * * This program uses Sedgewick and Wayne's Picture.java class to load images, view * them, access and edit their individual pixels, and save the edited image file. * * @author Richard White * @version 2015-11-02 */ import java.awt.Color; public class PictureEditor { public static void main(String[] args) { Picture myStudents = new Picture("apcs_photo.jpg"); myStudents.show(); // display the photo on screen int h = myStudents.height(); int w = myStudents.width(); for (int row = 0; row < h; row++) { for (int col = 0; col < w; col++) { System.out.println(myStudents.get(col,row)); Color thePixel = myStudents.get(col,row); int red = thePixel.getRed(); int green = thePixel.getGreen(); int blue = thePixel.getBlue(); Color theNewColor = new Color(0, green, blue); myStudents.set(col,row,theNewColor); } } myStudents.show(); // display the new photo on screen // myStudents.save("altered.jpg"); } }
You can get the project files from a link on the Materials page. Take a moment to download that zip file, open it up, and place the directory in your code folder.
Once you've got the files, see if you can piece together the infrastructure to write a program that produces only the blue "channel" of the photo.
Once you've got that going, try this.
BlackAndWhite.java
The assignment is to convert a photo to "grayscale". There are a number of ways to do this, but one really simple way is to:
- get the RGB values for each pixel (a value from 0-255)
- find the average of those three RGB values for that pixel
- set the pixel to color R = average, G = average, and B = average.
Using what we've discussed so far, create a program called BlackAndWhite.java
that can be used to convert an image to grayscale.
6.3.4. Colorize.java
Now create a program called Colorize.java
that "colorizes" photos with an "extreme" palette. You can do this by:
- getting the RGB values for each pixel
- increasing the brightness of those values by some factor, but not higher than the maximum allowed (255).
- set the pixel to these new color values
Note that to make sure you don't exceed a maximum value, you need to use the min
function. If you want to make sure that the value of your red pixel doesn't exceed 255, you'll need to use a statement like:
int newRed = Math.min(oldRed * 3, 255)
This uses whichever value is smaller: oldRed * 3
or 255
.
6.3.5. Mirror.java
Create a program called Mirror.java
that takes the left half of the photo and copies it over to the right half so that the right side of the photo is replaced by a mirror image of the left side of the photo.
6.3.6. Reverse.java
Similar to the Mirror
program above, create a program called Reverse.java
that reverses the photo so that the left side is on the right and the right side is on the left.
Can you do this without having to create an entirely new photo? or do you need to create a blank photo that you'll put new pixels of color into?
6.3.7. Blur.java
For this activity, we need to go through the pixels in the image one by one, and change the value of a pixel based on the value of the pixels around it. We'll need to bring the value of this pixel closer to the ones around it.
We're going to need to create new pixels but not actually change them on our current photo. (Why? Changing pixel i,j, will cause a change in the value of i+1, j, which causes a change in i + 2, j... ) We need a blur effect based on just the local values, not a bleed effect, where one pixel in the upper left corner ends up influencing every other pixel on the screen.
Here's some pseudocode for the Blur.java program.
/** * The Blur class takes an image and applies a blur to it. */ import java.awt.Color; public class Blur { public static void main(String[] args) { // Create a Picture object from a .jpg or .png image on your computer // Store the height and width of the image // set up blank image that will store blurred version Picture myBlurryPicture = new Picture(w,h); // Display original version of picture // for each row in the original picture // for each column in the original picture // We're at location col, row // Now go around and get average of surrounding values /* ------------------------------------- * | | * i-1, j-1 | i, j-1 | i+1, j-1 * | | * ------------------------------------- * | | * i-1, j | i, j | i+1, j * | | * ------------------------------------- * | | * i-1, j+1 | i, j+1 | i+1, j+1 * | | * ------------------------------------- */ int redSum = 0; int greenSum = 0; int blueSum = 0; int pixelsCounted = 0; for (int j = one less than current row, to one greater than current row) { for (int i = one less than current column, to one greater than current column) { if ( we're still within the bounds of the picture ) { // add to pixel count Color thePixel = myPicture.get(i,j); // get one the local pixels redSum += thePixel.getRed(); // add it's red value to current total // add the green value to the current green total // add the blue value to the current blue total } } } // Create a new average color based on the average red, green, and blue values // Set the pixel at col, row in the blurry picture to this average value } } myBlurryPicture.show(); } }
How many levels down do we go in our nested loops in here? 2? 3?
What happens if we don't have the if
statement to check if we're within the bounds of our height and width?
6.3.8. Getting rid of Magic Numbers
If you've used Photoshop, or GIMP (the Free, Open Source Software alternative), you know that you can vary the level of blur applied to a photo.
What numbers in our current program affect the degree of blur in our new photo?
Replace those numbers with a variable called `BLUR_FACTOR` that is established as a constant at the beginning of the program:
final static int BLUR_FACTOR = 2
6.4. 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.
6.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'sradius
, an initialx
andy
position, and an initialvelocity
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 periodt
. (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.
6.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 main method in the first tab of your Processing "sketch" should set up some initial values and a window that we can use to view our ball, and watch it moving around. This would be a good jumping off point:
/** * This program uses kinematics equations to watch a * ball bounce back and forth horizontally. */ // instance variables Ball b; float time; // establishing the window void setup() { size(800, 600); // creates the window background(255); // background white smooth(); // attempts to remove glitches noStroke(); // no outline on the ball int radius = 30; int vX = 4; // initial values of x- and... int vY = 0; // ... y-velocities time = 0.8; // time interval b = new Ball(radius, width/2, height/2, vX, vY); } // the main draw loop void draw() { background(255); // clear the screen fill(255, 0, 0); // set fill to red for the ball // draw the ball ellipse(b.getX(), b.getY(), b.getRadius() * 2, b.getRadius() * 2); println(b); // displays ball info in console (debugging) }
- 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 thedraw()
loop. Add that line and try running the program now.
6.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 themove()
method, let's check to see if the ball has moved off the screen. If it has, we'll have it change its direction:. . . println(b); 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 theif
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? ;)
6.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.