Lesson 3 contains our first foray into animation. We'll also have a teaching/learning discussion which does not involve reading a set of published papers, but instead, involves some reflection about the skills we've learned and how you might teach them if you ever wanted to.
By the end of this lesson, you should be able to write a program that simulates the movement of an object on the screen, write an "if statement" correctly, write a program that includes setup() and draw() blocks, and use comments to clarify your code.
This lesson will take us one week to complete, 2 June - 8 June 2021. You will turn in Exercise 3.2, as detailed later on, at the end of the lesson. We will also have a teaching/learning discussion that will span the entire week devoted to Lesson 3.
If you have any questions, please post them to our Questions? discussion forum (not e-mail) in Canvas. I will check that discussion forum daily to respond. While you are there, feel free to post your own responses if you, too, are able to help out a classmate.
This page serves as a placeholder to remind you to go visit the first Teaching/Learning discussion for this course, which can be found linked from the Lesson 3 Module in Canvas. Please visit that discussion board often during the week devoted to Lesson 3 to offer your opinion and to reply to your classmates regarding how/why/what to teach your own students about the topics and skills that have come up so far in this course.
Let's quit messing around and get to what's fun about Processing. Let's make stuff move on the screen. I'll start with my lightning bolt because why not, we already know what that looks like. Here's a program that draws one lightning bolt to the screen and moves it horizontally.
//move the lightning bolt horizontally //declare some variables first int y = 20 ; int x = 10 ; //setup function void setup () { size ( 490 , 200 ); } //draw function where the action happens void draw () { smooth (); background ( 0 ); noStroke (); fill ( 250 , 230 , 5 ); beginShape (); vertex (x, y); vertex (x, y + 50 ); vertex (x + 10 , y + 40 ); vertex (x, y + 100 ); vertex (x + 10 , y + 90 ); vertex (x, y + 100 ); vertex (x + 40 , y + 65 ); vertex (x + 30 , y + 75 ); vertex (x + 40 , y + 25 ); vertex (x + 30 , y + 35 ); vertex (x + 40 , y); endShape ( CLOSE ); //increment the value of x by 1 each time the program loops through draw() x++; } |
This program looks fundamentally different from previous ones we've seen so far. For one thing, there are discrete paragraphs instead of just a list of commands. I'm going to go through what the new syntax does.
Now is a good time to talk about comments, which I haven't done explicitly yet. Notice in all my samples of programs there are some lines that start with two slashes (//). Those are comments. They are like the notes you make in the margin of a cookbook telling you why the recipe is written the way it is. Processing ignores them but they are super useful because they tell somebody else in plain English what your code is doing. This is also important when you go back to a program a few days, weeks, years later and you can't remember what you were thinking or what that program does. Having those comments there helps a lot. If you want to write a longish paragraph of an explanation, you can either start each line with the two slashes or else you can type /* at the beginning, write several lines, and then type */ at the end. Everything between the /* */ will be commented out.
Another good use of comments is when you have a program that is working fine, then you add some things to it, then all of a sudden it's not working anymore. Rats! In that case, I try to isolate the problem by commenting out the new parts one by one and seeing if the program runs without those parts. Then I can usually narrow down how I've screwed up.
In the program above, I first write a comment explaining what the program does. Then I declare some variables. Since these variables are declared at the top of the program before anything else happens, they are defined that way for the entire rest of the program. Then comes a paragraph that looks like:
void setup () { some commands } |
This is the setup() block. setup() is a function whose job is to do things like set the window size or the background color, or to load data files, media, and fonts that might be used in the program. Processing runs through setup() exactly once. There can only be one setup() block per program. If you declare a variable inside setup(), then that variable is not available outside of setup(). The setup() block starts out with the key word void. That tells Processing that setup() is a function that doesn't return an answer. Contrast this to a mathematical function such as f(x) = x + 1. This is a function whose job is to add one to any value of x you give it, and then tell you the result. If we were writing a function like that, we'd have to specify the variable type as the key word instead of void.
After the setup() block there is a paragraph that looks like
void draw () { some commands } |
This is the draw() block. draw() is a function whose job is to loop over the commands contained inside it continuously until you tell it to stop. You can kind of think of it as an infinite for loop in which the time between subsequent runs through the loop is the refresh rate of your computer screen. You need to have a draw() block if you want continual looping in your program. Almost all the rest of the programs we will write from now on will contain a setup() and a draw() block in them. If you declare a variable inside draw(), then that variable is not available outside draw().
The section of the code that goes:
x++; |
Is the line that takes advantage of the continuous looping of the draw() function. All the commands inside the beginShape()/endShape() pair form the lightning bolt just like in earlier programs, in which x = 10 and y = 20 (remember we set those at the top of the program). Then we are telling Processing to take the value of x and add one to it. That's what "x++" means. Next time the lightning bolt will be plotted with the origin of the lightning bolt at x = 11 and y = 20. Each time the program loops through draw(), it plots the lightning bolt, recalculates a new value for x, and plots the lightning bolt again shifted one pixel to the right of where it was before. It looks like the lightning bolt is moving because your eyes can't refresh as fast as your screen.
In pseudo-English, here's what the code example at the top of the page does
that's it!
In the program on the previous page, the lightning bolt traveled off the screen, never to be seen from again, drawing itself in the ether outside of our window. That's not fun! Let's fix it.
An if statement is a way to change a program's behavior depending on some event. It reminds me a lot of the way natives of Pittsburgh use the word "whenever." (As in, "Whenever you are going to the store will you buy me some eggs?" Only western Pennsylvanians have this usage. I think it is weird.) You write an if statement like this:
if (thing you are testing) { some commands that will execute only if (thing you are testing) is true ; } |
The test is enclosed in parentheses, and the commands to execute if the test turns out to be true are enclosed in curly braces. When Processing sees an if statement, it checks whether the test is true and if it is true, it executes the commands between the braces, then goes on with the rest of the program. If the test turns out not to be true, it ignores all the commands inside the curly braces and moves on to the rest of the program. Okay, back to our lightning bolt! In kind-of English, one way to fix the earlier program is to write something like,
if (the lightning bolt gets to the far right edge of the window) { start over again on the left side } |
We can do that! Here's how:
We add some lines of code to our program that say:
if (x == width ) { x = 0 ; } |
That is the code translation of what we wrote in English up above. Now the lightning bolt will move along horizontally and when it gets to the right edge of the display window it will jump back to the left edge and start over.
Simply: One equals sign is telling. Two equals signs are asking.
The == part is a logical operator that means equality. We are testing whether or not two values are equal to each other, in this case x
and the width of window. So x==width is asking the program whether or not x equals width or not.
We already set the width when we specified it using size(200,200), so Processing knows that the value of width is 200 already. Therefore you don't have to write "200" explicitly. It's almost always better to refer to a variable than to write an actual number because then if you change the size of the window, the program will still work the way you intended. Otherwise you'd have to hunt down all the places you wrote 200 and change them to the new width.
Below the line that says if (x==width){, I wrote x=0;
In this case, I am not testing for equality, I am setting x equal to zero. I am telling the program to make x have the value of zero. So remember, that's the difference between one = sign and two of them. One is telling, two is asking. There are other logical operators, such as > (greater than), < (less than), >= (greater than or equal to), <= (less than or equal to), != (not equal to), && (and).
So basically, the if test we wrote won't kick in (Processing will evaluate it as false) until the lightning bolt gets to the far right of the display window. Until then, Processing will check whether or not the value of x is equal to the width of the display window. As long as it is not, then it will not reset the value of x to zero. It will ignore that command. Once the value of x reaches the value that is the width of the display window, Processing will reset the value of x to zero and go on with the program, incrementing x until it reaches the value of width again. It goes forever until you stop the program.
Here's what the whole program looks like now:
// move the lightning bolt horizontally // when it gets to the right edge, put it back where it started //declare some variables first int y = 20 ; int x = 10 ; //setup function void setup () { size ( 200 , 200 ); } //draw function where the action happens void draw () { background ( 0 ); smooth (); noStroke (); fill ( 250 , 230 , 5 ); beginShape (); vertex (x,y); vertex (x,y+ 50 ); vertex (x+ 10 ,y+ 40 ); vertex (x,y+ 100 ); vertex (x+ 10 ,y+ 90 ); vertex (x,y+ 160 ); vertex (x+ 40 ,y+ 65 ); vertex (x+ 30 ,y+ 75 ); vertex (x+ 40 ,y+ 25 ); vertex (x+ 30 ,y+ 35 ); vertex (x+ 40 ,y); endShape ( CLOSE ); //increment the value of x by 1 each time the program loops through draw() x++; if (x> width ){ x= 0 ; } } |
What if we want to make the lightning bolt move faster? Change the line where we are incrementing the value of x to something like:
x = x + 5;What if we want to make the lightning bolt move slower? Change the line where were are incrementing the value of x to something like:
x = x + 0.5;
If you haven't programmed before, then it may take some extra thinking to grasp the meaning of an expression such as x = x + 5;. If this were math class, then you'd quickly realize that there is no such value of x that will make the equation x = x + 5 work. You'd collect like terms and be left with the expression 0 = 5 which is no good. But this isn't math class.
So the expression x = x + 5; is not there to ask the computer to solve for x. What it is saying is: take the value of x that you have in memory right now, add 5 to it, and then take whatever answer you get and reassign that number to x. (Tell x to be the new value you just computed.)
If x were 1, then after performing the command x = x + 5, the value of x will be 6. Any other command or expression performed from here on out will substitute "6" every time x shows up in the program, until x gets reassigned to something else.
There's no rule that says we have to use whole numbers, except for the fact that when we first declared x in our program, we declared it as an integer. That means we cannot give it a fractional part. Therefore if you write x=x+0.5 like I suggested you try out, you will get an error telling you "cannot convert from float to int." What's that about?
If we want to be able to allow x to have a fractional part, we need x to be a floating point number instead of an integer. These are called "floats" in Processing. A float is a floating-point number, which is a number that is not an integer, such as 1.5 or PI (π). Why bother with the difference between ints and floats? Why not just call them all “numbers” or something? Well, one difference between a float and an int is the amount of memory your computer has to allocate to store it. Also, integers are precise and floats are not.
Let’s say I have a cookie. I can declare int numCookie = 1 to tell Processing that I have exactly one cookie. If I’m going to share it with 6 other friends of mine I will break it into seven pieces. If I am really accurate at cookie-breaking we will all get exactly one seventh of the cookie. We can write this precisely as 1/7 on paper. However, if we wanted to express that in a Processing program, we’d probably declare something like
float cookiePortion0 = 0.14 ; float cookiePortion1 = 0.14 ; float cookiePortion2 = 0.14 ; float cookiePortion3 = 0.14 ; float cookiePortion4 = 0.14 ; float cookiePortion5 = 0.14 ; float myCookiePortion = 0.14 ; |
If we add up all the floats we get 0.98, not 1. We could write out each number to more decimal places. 1/7 is actually 0.142857 . . . but it is a repeating decimal. We can’t write an infinite number of digits past the decimal because we don’t want our entire computer’s memory taken up by deciding how much of a cookie each person is getting. The computer has to truncate the number at some point, and it will probably be good enough, but it is not perfect. That's the difference between an int and a float.
In our lightning bolt program, we'll change the declaration of the variable x up at the top to say
float x = 10 ; |
and then we are all set to increment x by a float later on in the program.
We also don't have to make the lightning bolt move from right to left. It can go wherever we want, limited by imagination only.
To make it go down instead of sideways, increment y instead of x, like so:
y++; if (y == height ) { y = 0 ; } |
If you want the lightning bolt to move diagonally, change both x and y with each loop through draw.
If you want the lightning bolt to move back and forth, you have to find a way to use the if test to change the direction of motion in x or y or both. Try it yourself and then check my screencast below to see how I did it.
Simply put, if you place a call to background() inside draw(), then it is the equivalent of erasing the screen with each run through draw(), which makes it look like the object is moving because the human eye can't refresh as fast as your computer display and Processing makes a run through draw() with each new frame your computer draws to its screen. On the other hand, if you put the call to background() inside setup(), you will draw over your previous iterations, without erasing them, essentially leaving a trail of an object's previous positions. Check out the program below. It draws a pink display window on which an orange circle is moving back and forth while a black line moves up and down.
float y = 0.0 ; float x = 0.0 ; float ell_direction = 1 ; float line_direction = 1 ; void setup () { size ( 200 , 200 ); } void draw () { background ( 234 , 128 , 128 ); frameRate ( 60 ); stroke ( 51 , 34 , 45 ); line ( 0 , y, width , y); y = y + ( 1 * line_direction); fill ( 245 , 166 , 47 ); ellipse (x, height / 2 , 10 , 10 ); x = x + ( 0.5 * ell_direction); if (x > width || x < 0 ) { ell_direction = ell_direction * (- 1 ); } if (y > height || y < 0 ) { line_direction = -line_direction; } } |
What would it look like if you cut and pasted the line that says background(234, 128, 128); out of draw() and into setup()?
//draw a circle. Make it move across the screen following the path f(x)=sin(x) float x= 0 ; float y= 100 ; int direction = 1 ; void setup (){ size ( 360 , 200 ); } void draw (){ rectMode ( CENTER ); background ( 200 ); fill ( 0 ); if (x< 0 || x> width ){ direction = direction*- 1 ; } if (direction == 1 ){ ellipse (x,y+ 20 * sin (direction*x* PI / 90 ), 20 , 20 ); x=x+direction; } else if (direction == - 1 ) { rect (x,y+ 20 * sin (direction*x* PI / 90 ), 20 , 20 ); x=x+direction; } else { println ( "I don't know what to do!" ); } } |
What it looks like running:
Let me explain the thought process behind building the program above. I could go through the program from top to bottom, but I didn't really write it in that order. I kind of wrote it from the middle to the outsides. First came the idea: I want to make a shape move across the window according some functional form other than a straight line, and I want it to stay on the screen so I have to make it turn around and go backwards when it gets to the side.
We haven't really talked about this yet, but Processing can make mathematical calculations such addition, subtraction, multiplication, division, exponentiation, logarithms, and trig functions. For my example I decided to draw a shape that traces out a sine wave across the screen. I decided to make it a circle, but I could have made it any shape I wanted. So the meat of the program is going to be something like this:
ellipse (x, sin (x),diameter,diameter); x++; |
This will make an ellipse of some diameter at the initial (x,y) position given by (x, sin(x)). Then we increment x by 1 with each run through draw() and so the circle will trace out a sine wave. So then I'll write some stuff around these lines that will make a runnable program, to see what it looks like so far. I need to write setup block that has the window dimensions in it, and I'll set the values of x and diameter while I'm at it.
float x= 0 ; float y= 100 ; int diameter= 20 ; void setup (){ size ( 200 , 200 ); } void draw (){ background ( 200 ); fill ( 0 ); ellipse (x,y+ sin (x),diameter,diameter); x++; } |
This works, except the circle doesn't go back and forth, and also it looks jittery. We can fix the back-and-forth problem by using an if test that tests where the circle is and what direction it is going and then changes the direction when we exceed the dimensions of our display window. We know how to do that. We just went over that in the previous couple of pages.
The jittery "problem" (I'm calling it a problem because I think it looks ugly. If you like it, then we're all done here!) arises because trig functions in Processing are in radians, not degrees. So the value of sin(x) is sweeping over its entire range very fast. One cycle of sine takes 360 degrees which is 2pi, a number between 6 and 6 and a half. How about if we make this shape sweep out exactly 2 cycles of sin(x) over the whole window? That would look cool. So we'll do a little math inside of where we wrote sin(x). While we're at it let's increase the amplitude so it looks nicer, too. In order to convert radians to degrees you do radians*PI/180. We want 2 cycles so let's multiply by PI/90 instead, and then let's help ourselves out by making the width of the window 360, so that each pixel is like 2 degrees of arc for our sine wave. I basically just trial-and-errored it until I got something that looked nice to me:
ellipse (x,y+ 20 * sin (x* PI / 90 ),diameter,diameter); |
Now let's make it go back and forth. I'll do it with an if test, like this:
if (x< 0 || x> width ){ direction = direction*- 1 ; } |
And then I'll change the line that says x++; to x = x + direction;. That way when direction is negative, x will decrement, and when direction is positive x will increment.
Great, I did it. But now that I got that to work, I realized that it would look even better if the bounceback on the window edges traced sin(-x) instead, so it looks like the ellipse grazes the side of the display window and turns in an arc instead of retracing its exact path. And while I'm at it, I'll change it so it's a square on the way back. The way I'm going to do this is to write another if test. This one in pseudo-English, will do the following:
if(I'm going from left to right){
draw a circle that goes like sin(x)
}
else if (I'm going from right to left){
draw a square that goes like sin(-x)
}
else {do some error handling in case of unexpected results}
Here's how it looks now:
//draw a circle. Make it move across the screen following the path f(x)=sin(x) float x= 0 ; float y= 100 ; int direction = 1 ; void setup (){ size ( 360 , 200 ); } void draw (){ rectMode ( CENTER ); background ( 200 ); fill ( 0 ); if (x< 0 || x> width ){ direction = direction*- 1 ; } if (direction == 1 ){ ellipse (x,y+ 20 * sin (direction*x* PI / 90 ), 20 , 20 ); x=x+direction; } else if (direction == - 1 ) { rect (x,y+ 20 * sin (direction*x* PI / 90 ), 20 , 20 ); x=x+direction; } else { println ( "I don't know what to do!" ); } } |
When I added the square, I changed rectMode to CENTER because otherwise there would be a jump when the shape changed from circle to square. Instead of doing sin(x) and sin(-x) I took advantage of the fact that direction was going from positive to negative so I could just use that variable to set which kind of sine function I wanted, and then I could write the same line both times. I could have just done an if and an else to do the switching between circle and square since direction can only be 1 or -1 in this program. However, it's not a bad idea to get in the habit of putting an else in there to mop up in case you made a mistake somewhere else in your code and there is actually an option that gets triggered that you didn't count on.
Now you can expertly move a shape you drew around your screen at different speeds! Cool!
3.1 Write a program that moves a custom shape around the screen (your choice about what path it follows: straight line or some other function). It should stay on the screen and change direction if it encounters any of the sides of the display window.
3.2 Modify your program from Exercise 3.1 so that the shape also changes color if it hits any of the sides of the display window
Submit Exercise 3.2 to its appropriate assignment in the Lesson 3 module in Canvas. Name your file lastname3_2.pde
Your program should demonstrate correct usage of an if statement to change the direction and fill and/or stroke color of your shape. Your shape should be a custom shape drawn using beginShape() and endShape(). Extra creativity such as using a for loop to set the vertices, or creating a composition with more than one shape is encouraged!
You have reached the end of Lesson 3! Double-check the to-do list on the Lesson 3 Overview page to make sure you have completed all of the activities listed there before you begin Lesson 4.
You should have been participating in the teaching/learning discussion and you should have submitted Exercise 3.2 to the Lesson 3 dropbox.