CS 2, Winter 2008
Programming for Interactive Digital Arts

Jan 16: State


Our own variables

We've been able to do some pretty fun interactive works just using the provided Processing variables (mouse position, etc.). To do more sophisticated things, though, we'll have to keep track of additional parts of the "state of the world" on our own. We maintain state information by creating, using, and updating our own variables. Let's start with a simple example, where a ball moves around the screen, but under random motion rather than driven by the mouse.

// The current coordinates of the ball, starting at the center
float x=50, y=50;

void setup()
{
  smooth();
  noStroke();
  background(0);
}

void draw()
{
  fill(0,3);
  rect(0,0,width,height);
  fill(255);
  ellipse(x,y,5,5);
  // Move the position by random steps in x and y
  x = x+random(-2,2);
  y = y+random(-2,2);
}
screenshot[applet]

The two things that are different about this sketch are the variable declaration at the start, and the assignment at the end of the draw function. The Programming notes section has the details, but the basic idea is that the declaration tells Processing that we'll be using our own variables named "x" and "y", and that their initial values are both 50. The assignment then updates the values of the variables, setting x to its old value plus some random amount, and similarly with y. Note that we can use these variables in statements just like Processing variables (or like the values they hold).

Let's get a bit fancier, and introduce another variable holding the current shade of the ball, along with one telling us whether we should increase or decrease the shade. Once the shade gets too low, we'll increase until it gets too high, and then we'll decrease. We'll do the same type of thing with the size, but take smaller steps.

// The current coordinates of the ball, starting at the center
float x=50, y=50;
// The current color of the ball, and whether it's increasing or decreasing
int gray=255, dgray=-1;
// The current size of the ball, and how much it's increasing or decreasing
float sz=5, dsz=0.1;

void setup()
{
  smooth();
  noStroke();
  background(0);
}

void draw()
{
  fill(0,3);
  rect(0,0,width,height);
  fill(gray);
  ellipse(x,y,sz,sz);
  // Move the position by random steps in x and y
  x += random(-2,2);
  y += random(-2,2);
  // Increase/decrease the color, changing direction at the extremes
  gray += dgray;
  if (gray == 128) {
    // Decreased too far; start increasing
    dgray = 1;
  }
  else if (gray == 255) {
    // Increased too far; start decreasing
    dgray = -1;
  }
  // Increase/decrease the size, changing direction at the extremes
  sz += dsz;
  if (sz <= 2 || sz >= 15) dsz = -dsz;
}
screenshot[applet]

A couple of pieces of syntax here -- the shorthand x += random(-2,2) is equivalent to x = x+random(-2,2), while x++ is equivalent to x = x+1. The two ways of changing direction here (explicitly set the variable vs. negate it) are equivalent; note that when we're using floating point numbers, it's best not to test for exact equality but for the appropriate inequality.

We can also use state to interact with the mouse. Here we have a "follower" that is at its own position, and takes a step towards the current mouse position. It updates its x position by some fraction of the difference between its current x position and the mouse. If the fraction is 1, it goes right to the mouse; if it's smaller, it goes only part of the way (try it with different settings). This is called "easing". The following sketch is based on the Processing example Basics | Input | Easing, but I gave the follower a little personality.

// The current coordinates of the follower
float x=0, y=0;
// State of the mouth: 0 = closed, 1,3 = half-open, 2 = open
int mouth=0;
// A parameter controlling how quickly the follower catches up
float easing=0.05;

void setup() 
{
  size(400, 400); 
  smooth();
  noStroke();  
}

void draw() 
{
  background(0);
  // Move towards the mouse
  x += (mouseX - x)*easing;
  y += (mouseY - y)*easing;
  // Switch mouth
  if (frameCount % 10 == 0)
    mouth = (mouth+1)%4;
  
  // The bait
  fill(255,0,0);
  ellipse(mouseX, mouseY, 10, 10);
  // The follower
  fill(255,255,0,200);
  if (mouth==0)
    ellipse(x, y, 25, 25);
  else if (mouth==2)
    arc(x, y, 25, 25, PI/4, TWO_PI-PI/4);
  else
    arc(x, y, 25, 25, PI/8, TWO_PI-PI/8);
}
screenshot[applet]

I think this is the first we've seen the arc() function. It's just part of an ellipse, between a starting angle and an ending angle (in radians). We'll see later how to make the follower point toward the bait.

You should now be able to go back over the example from the first class, and understand it!

Events

One problem with the way we handled mouse and key presses before is that we checked for them every frame. If the frame rate is fast, we might respond multiple times to a single click (because the mouse is still held down); if it's slow, we might miss a click (because it wasn't held down during the lifespan of the draw call). The following skecth illustrates.

void draw()
{
  if (mousePressed) {
    background(random(255),random(255),random(255));
  }
}
screenshot[applet]

While testing the press in the draw function is appropriate in some cases, there are times when we want to respond to a mouse press exactly once. To support that, Processing lets us define our own functions to handle mouse and key events. Here's the above example, but with press handled by the mousePressed() function (yes, it has the same name as the variable).

void draw()
{
  // Empty
}

void mousePressed() 
{
  background(random(255),random(255),random(255));
}
screenshot[applet]

There are similar function that get called while the mouse is being dragged (moved while pressed) and when it is released. Surprisingly, these are called mouseDragged() and mouseReleased(). These let us build up GUI like elements.

float x=0;              // location of the handle
boolean grabbed=false;  // has the mouse been clicked in the handle?

void setup()
{
  strokeWeight(5);
  smooth();
}

void draw()
{
  // Black to green according to x position of handle
  background(0,255*x/width,0);
  if (grabbed) { // highlight the handle
    stroke(255,255,0);
    fill(255,255,0);
  }
  else {
    stroke(255);
    fill(255);
  }
  line(0,height/2,x,height/2);
  ellipse(x,height/2,20,20);
}

void mousePressed()
{
  if (dist(mouseX,mouseY,x,height/2) < 10) {  // within handle
    grabbed = true;
    x = mouseX;
  }
}

void mouseDragged()
{
  if (grabbed) {
    x = constrain(mouseX,0,width);
  }
}

void mouseReleased()
{
  grabbed = false;
}
screenshot[applet]

One other noteworthy function in this example is constrain(), which doesn't let a value (first parameter) exceed a minimum (second) or maximum (third). It returns the value if it's okay, or else the minimum or maximum. You could do this yourself with an if statement (think it through), but this is short and to the point.

The keyPressed() function works similarly. E.g., I use it to capture the screenshot image for each sketch:

void keyPressed()
{
  save("sketch.png");
}

The save() function can output different formats, depending on the file extension (here, png).

By combining keys with state, we can let whoever is playing with our sketch control different aspects of it.

float r=255,g=255,b=255;  // drawing color
int dc=5;                 // amount of color increment/decrement
float sz=20;              // ellipse size
boolean fade=true;        // transparent rectangle each time?

void setup()
{
  size(400,400);
  smooth();
  noStroke();
  frameRate(30);
  background(0);
  println("1:white; 0:black; c:random color");
  println("r/R, g/G, b/B: less/more red, green, blue");
  println("z/Z: smaller/bigger");
  println("f/F: no fade / fade");
}

void draw()
{
  if (fade) {
    fill(0,5);
    rect(0,0,width,height);
  }
  if (mousePressed) {
    fill(r,g,b);
    ellipse(mouseX,mouseY,sz,sz);
  }
}

void keyPressed()
{
  if (key=='1') {      // white
    r = 255; g = 255; b = 255;
    println("white");
  } 
  else if (key=='0') { // black
    r = 0; g = 0; b = 0;
    println("black");
  }
  else if (key=='c') { // random color
    r = random(255);
    g = random(255);
    b = random(255);
    println("color:"+r+","+g+","+b);
  }
  else if (key=='r') { // less red
    if (r > dc) r-=dc;
    println("red:"+r);
  }
  else if (key=='R') { // more red
    if (r < 255-dc) r+=dc;
    println("red:"+r);
  }
  else if (key=='g') {
    if (g > dc) g-=dc;
    println("green:"+g);
  }
  else if (key=='G') {
    if (g < 255-dc) g+=dc;
    println("green:"+g);
  }
  else if (key=='b') {
    if (b > dc) b-=dc;
    println("blue:"+b);
  }
  else if (key=='B') {
    if (b < 255-dc) b+=dc;
    println("blue:"+b);
  }
  else if (key=='z') { // smaller
    if (sz > 1) sz--;
    println("size:"+sz);
  }
  else if (key=='Z') { // bigger
    if (sz < 100) sz++;
    println("size:"+sz);
  }
  else if (key=='f') { // no fade
    fade = false;
    println("no fade");
  }
  else if (key=='F') { // fade
    fade = true;
    println("fade");
  }
}
screenshot[applet]

Or we can create a simple game. Here players 1 and 2 hit their keys to grab chips from the mouth; at some random point the mouth will close, and the player who tried to grab then will be penalized.

int num;    // how many presses
boolean mouthOpen=true;
int chips1=0, chips2=0;    // score of the game

// Parameters for the game: how many times before it snaps for sure,
// how many chips are given or taken away
int maxPresses=10, bonus=1, penalty=2;

void setup()
{
  background(0);
  smooth();
  textFont(loadFont("LucidaConsole-24.vlw"));
  num = round(random(maxPresses));
}

void draw()
{
  background(0);
  // The mouth
  noStroke(); fill(255,255,0);
  if (mouthOpen) {
    arc(50, 50, 50, 50, PI/4, TWO_PI-PI/4);
  }
  else {
    ellipse(50, 50, 50, 50);
    stroke(0);
    line(50,50, 75,50);
  }
  // The score
  fill(255,0,0);
  text(chips1,10,25);
  fill(0,0,255);
  text(chips2,70,25);
}

void keyPressed()
{
  if (key=='0') {  // reset
    mouthOpen = true;
    num = round(random(maxPresses));
  }
  else if ((key=='1' || key=='2') && mouthOpen) {  
    num--;
    if (num==0) {   // snap
      mouthOpen=false;
      // Penalize
      if (key=='1') chips1 -= penalty;
      else chips2 -= penalty;
    }
    else {   // safe
      if (key=='1') chips1 += bonus;
      else chips2 += bonus;
    }
  }
}
screenshot[applet]

Programming notes

Declaration
To create our own variable, we declare it by listing its type and its name, e.g., "float x;". In many cases, it is apropriate to give it an initial value, too, e.g., "float x=50;". Multiple variables of the same type can be declared together, separated by commas, e.g., "float x=50, y=50;".
Local vs. global
Where and how a variable can be used depends on where it is declared. A global variable is declared before the setup function. It can be used anywhere, and its value carries over from one function call to the next (e.g., from frame to frame). A local variable is declared inside a function definition (e.g., inside the draw function). It can only be used in that function, and its value is reset each time the function is called. So far we're only using global variables, but local variables are quite useful to hold temporary calculations, as we'll see in the next few lectures.
Type
A variable can only hold a certain type of value, which is specified as part of the declaration. So far we have daalt with three types: int, float, and boolean. As discussed before, booleans can only take on the values true and false. Int and float are two different types of numbers -- for those that have no decimal part (int) and those that do (float). For example, ints are useful for counting, while floats are useful for coordinates. One key difference between the two is division -- division of ints throws away the fractional part; thus 1/2 == 0, whereas 1.0/2.0 == 0.5. Integers also support the "modulus" (remainder) operator, written with a percent sign; e.g., 4%2==0 while 5%2==1. There are some subtle issues in dealing with floats that hopefully we won't have to deal with here, but due to the finite precision in a computer, math sometimes does funny things. In both cases, numbers can't be arbitrarily large.
Type conversion
Processing "upgrades" ints to floats when combining the two in math operations. We can also force the conversion by casting, e.g., "((float)i)" converts i to a float. To convert a float to an int, we need to figure out what to do with the fractional part; the round() function rounds up or down (whichever is closer), while the ceil() always rounds up and the floor() function always rounds down.
Identifier
Choose meaningful names for variables, so that when you read the code, you can tell what they stand for. There are restrictions on the names, though -- stick with alphanumeric (beginning with a letter), and avoid names already used by Processing (e.g., ellipse). They are case sensitive; unless you want your code to look like it's from the 70s, use mostly lower-case. A common practice for multi-word variables is to capitalize the first letter of the second (third, fourth, ...) word, e.g., myName.
Assignment
Assignment updates the value of a variable. It is written "variable = expression", where before the equals sign is the name of the variable, and after it is an expression of the appropriate type. We can combine math operators with assignment (e.g., +=, -=, *=), e.g., "variable += expression" is shorthand for "variable = variable + expression". Even shorter-hand expressions are available for incrementing by one, "x++", and decrementing by one, "x--".

Practice problems

  1. Modify the wanderer (first sketch) so that it doesn't leave the window. Or modify it so that if it leaves one side of the window, it reenters on the other side. [hints]
  2. Modify the "wanderer" to vary in red, green, and blue, instead of gray. [hints]
  3. Modify the "follower" to have an additional state, with the mouth even wider open. Then try having it cycle through the states differently depending on how close it is to the bait (e.g., opening to the wide-mouth state only when close enough). [hints]
  4. Modify the "wanderer" to change color whenever the mouse is pressed over it. [hints]
  5. Modify the rectangle mouse-over example from last time so that the state of the button can be toggled. That is, when pressed once, it turns to a new color, and when pressed again, it turns back to the original color. [hints]
  6. Control the speed of the "wanderer" with a slider. [hints]
  7. Control the easing of the "follower" with key commands. [hints]