CS 2, Winter 2008
Programming for Interactive Digital Arts

Jan 25: Coordinates revisited


Translation and scaling

So far, we've always treated the origin, (0,0), as the upper-left corner, with the x and y axes extending to the right and down, stepping one pixel at a time. In the "C" monogrammer from the shapes lecture, we had to do some addition and multiplication for each vertex, to put it at the right place and at the right size. It would be more convenient to just tell Processing to move the origin and change the overall scale, so that we don't have to keep doing that ourselves.

translation scale

To do just that, Processing provides the translate() (given x and y offsets), scale() (given x and y scaling factors). Give each of these operations a quick whirl, just plotting an ellipse before/after the operation.

We can use this technique to make the "drawC" function look exactly like our original static "C" sketch, without having to do the arithmetic for each vertex.

// Draw a letter "C" of size s, with the upper left at (x,y).
void drawC(float x, float y, int s)
{
  pushMatrix();
  translate(x,y);
  scale(s/100.0,s/100.0);
  beginShape();
  vertex(0,0);
  vertex(100,0);
  vertex(100,20);
  vertex(20,20);
  vertex(20,80);
  vertex(100,80);
  vertex(100,100);
  vertex(0,100);
  endShape(CLOSE);
  popMatrix();
}
screenshot[applet]

Note that the effects of these functions are cumulative, e.g., translate(10,5); translate(7,3); is the same as translate(17,8);, and likewise we can compose the three various operations. Thus if we aren't careful, we can lose our bearings. To help us out, Processing provides the pushMatrix() function to "remember" the current coordinate system, and popMatrix() function, to return back to it. We can push multiple coordinate systems, and they are treated in "last in first out" order, just like stacking plates on top of each other.

The "Nematode" sketch from Greenberg (10-7 to 9) provides a nice example. 10-7 is the heart of the sketch; I've simplified it here. Each segment of the body is an ellipse; the ellipses grow and shrink, and their centers move sinusoidally.

size(500, 200);
smooth(); noFill(); background(255);
stroke(65, 10, 5); strokeWeight(.2);

int dx=2;
float da=radians(2.55);  // converting x to angle for sine
float radius=0, dr=0.35; // worm segment size and step
translate(0,height/2);   // move axis down to middle
for (int x=0; x<width; x+=dx) {
  ellipse(-radius/2, -radius/2, radius*.75, radius);
  translate(dx, 0.5*sin(x*da));  // move axis over and up/down
  radius += dr;
  if (x==width/2) dr*=-1;   // start shrinking segments
}
screenshot[applet]

Orientation

Going one step further, we can even reorient the coordinate system, so that the axes point diagonally across the window, for example. The function rotate() does that, given the angle to rotate (in radians).

rotate

Give it a try, drawing a horizontal or vertical line before/after rotation. To rotate around some point other than the upper-left corner, we translate to that point first.

float a=0, da=0.05;    // drawing angle and increment

void setup()
{
  fill(0,255,0); noStroke();
  smooth();
  textFont(loadFont("LucidaSans-18.vlw"));
}

void draw()
{
  background(0);
  translate(width/2,height/2);
  rotate(a);
  text("hello", 0,0);
  a+=da;
}
screenshot[applet]

The "Tail" sketch (I see it as waving grass) from Reas and Fry (32-07) provides a nice example using both translation and rotation. The tail starts at the bottom and moves up. After drawing a segment of a tail, the origin is translated to the end of the segment, and rotated a bit. The translations and rotations accumulate, so that there is more rotation towards the tip of the tail. The coordinate system is re-set after completing a tail. Note that the rotation angle is a sum of two sinusoids, so that the tails don't wave back and forth the same amount every time -- sometimes the sines reinforce each other yielding a larger wave, and sometimes they negate each other yielding a smaller wave.

A rotation transformation can be particularly useful when we want to have a sketch react not only to the position of the mouse, but also to the angle. That's what will let us do the freaky eyeballs (below). We can use Processing's atan2() function to calculate the angle between a point and the origin. We provide as parameters the y and x coordinates, in that reversed order (recall from trig that the tangent is the opposite over the adjacent).

atan2

To compute an orientation with respect to some point other than the origin, we subtract out the coordinates of that point before calling atan2(), and then translate() to that point before calling rotate() with the computed angle.

void setup()
{
  stroke(255);
  strokeWeight(10);
}

void draw()
{
  background(0);
  float dx = mouseX - width/2;
  float dy = mouseY - height/2;
  float angle = atan2(dy, dx);  

  pushMatrix();
  translate(width/2,height/2);
  rotate(angle);
  line(0,0,25,0);
  popMatrix();
}
screenshot[applet]

Now we can make our follower from the State lecture look at the bait.

// 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);

  // The bait
  fill(255,0,0);
  ellipse(mouseX, mouseY, 10, 10);

  // Move towards the mouse
  x += (mouseX - x)*easing;
  y += (mouseY - y)*easing;
  // Switch mouth
  if (frameCount % 10 == 0)
    mouth = (mouth+1)%4;

  // Look at the bait
  float dx = mouseX - x;
  float dy = mouseY - y;
  float angle = atan2(dy, dx);  

  // The follower
  pushMatrix();
  translate(x,y);
  rotate(angle);
  fill(255,255,0,200);
  if (mouth==0)
    ellipse(0, 0, 25, 25);
  else if (mouth==2)
    arc(0, 0, 25, 25, PI/4, TWO_PI-PI/4);
  else
    arc(0, 0, 25, 25, PI/8, TWO_PI-PI/8);
  popMatrix();
}
screenshot[applet]

Now for the freaky eyeballs, from the Processing example Basics | Math | Arctangent. It works exactly the same way, moving the center of the eyeball out along the x axis in the rotated coordinate system.

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

void draw()
{
  background(50);

  drawEye( 50,  16,  80);
  drawEye( 64,  85,  40);  
  drawEye( 90, 200, 120);
  drawEye(150,  44,  40); 
  drawEye(175, 120,  80);
}

// Draw an eye of size s at position (x,y)
void drawEye(float x, float y, float s)
{
  // Make the eye look at the mouse
  float angle = atan2(mouseY-y, mouseX-x);
  pushMatrix();
  translate(x, y);
  fill(255);
  ellipse(0, 0, s, s);
  rotate(angle);
  fill(153);
  ellipse(s/4, 0, s/2, s/2);
  popMatrix();
}
screenshot[applet]

A less freaky sketch gives kind of a kaleidoscope feel, by duplicating a rectangle "chip" at various rotations around the center of the window. We take the full circle (2π) and divide it by the degree of symmetry (say, 3-fold, for 3 copies). We put a chip at each such rotation, translated out the reoriented x axis by the same distance the original chip was from the original origin.

int symm=3;      // degree of symmetry
int sz=20;       // size of chips
int rand=75;     // control of random chip generation

void setup()
{
  size(400,400);
  noStroke();
  background(0);
  rectMode(CENTER);
}

void draw()
{
  // The probability of a random chip is 1/rand
  if (random(rand) < 1) 
    addChip(random(width),random(height));
}

void mousePressed()
{
  addChip(mouseX, mouseY);
}

// Place a chip (randomly colored, of the current size)
// at (x,y) and its symmetry-mates
void addChip(float x, float y)
{
  // Angle from center to (x,y)
  float dx = x - width/2;
  float dy = y - height/2;
  float angle = atan2(dy, dx); 
 
  // Distance from center to (x,y)
  // This will be the new x coordinate upon rotation
  // (The new y coordinate will be 0)
  float d = dist(x, y, width/2, height/2);
  
  fill(random(100,255),random(100,255),random(100,255),100);
  pushMatrix();
  translate(width/2,height/2);
  rotate(angle);
  for (int i=0; i<symm; i++) {
    rect(d,0,sz,sz);
    rotate(TWO_PI/symm);
  }
  popMatrix();
}

void keyPressed()
{
  if (key=='Z') {        // Bigger
    sz+=5;
  }
  else if (key=='z') {
    if (sz>5) sz-=5;
  }
  else if (key=='S') {   // Higher symmetry
    symm++;
    println(symm+"-fold");
  }
  else if (key=='s') {
    if (symm>3) symm--;
    println(symm+"-fold");
  }
  else if (key=='R') {    // More random chips
    if (rand>5) rand-=5;
  }
  else if (key=='r') {
    rand+=5;
  }
  else if (key=='c') {    // Clear
    background(0);
  }
}
screenshot[applet]

A variation on that sketch keeps track of where the chips have been placed (in terms of their angle and radius from the center), and continually updates an overall angle of rotation. Notice that to draw, we have a loop that iterates over the chip indices; the body of that loop has a loop that iterates over the symmetry copies.

int symm=3;      // degree of symmetry
int sz=20;       // size of chips
int rand=75;     // control of random chip generation
float a,da=0.01; // global rotation and increment

// The chips: max number, current number, coordinates, size, and colors
int num = 100, chip = 0;
float[] ca = new float[num], cr = new float[num];
int[] csz = new int[num];
color[] col = new color[num];

void setup()
{
  size(400,400);
  noStroke();
  background(0);
  rectMode(CENTER);
}

void draw()
{
  background(0);
  translate(width/2, height/2);
  
  for (int i=0; i<num; i++) {
    fill(col[i]);
    pushMatrix();
    rotate(a+ca[i]);
    for (int d=0; d<symm; d++) {
      rect(cr[i],0,csz[i],csz[i]);
      rotate(TWO_PI/symm);
    }
    popMatrix();
  }

  // The probability of a random chip is 1/rand
  if (random(rand) < 1) 
    addChip(random(width),random(height));
  
  a += da;
}

void mousePressed()
{
  addChip(mouseX, mouseY);
}

// Place a chip (randomly colored, of the current size)
// at (x,y) and its symmetry-mates
void addChip(float x, float y)
{
  col[chip] = color(random(100,255),random(100,255),random(100,255),100);
  csz[chip] = sz;
  
  // Angle from center to (x,y)
  float dx = x - width/2;
  float dy = y - height/2;
  ca[chip] = atan2(dy, dx) - a; // subtract out the current angle,
                                // since it'll be added right back in

  // Distance from center to (x,y)
  // This will be the new x coordinate upon rotation
  // (The new y coordinate will be 0)
  cr[chip] = dist(x, y, width/2, height/2);

  chip = (chip+1)%num;
}

void keyPressed()
{
  if (key=='Z') {        // Bigger
    sz+=5;
  }
  else if (key=='z') {
    if (sz>5) sz-=5;
  }
  else if (key=='S') {   // Higher symmetry
    symm++;
    println(symm+"-fold");
  }
  else if (key=='s') {
    if (symm>3) symm--;
    println(symm+"-fold");
  }
  else if (key=='R') {    // More random chips
    if (rand>5) rand-=5;
  }
  else if (key=='r') {
    rand+=5;
  }
  else if (key=='c') {    // Clear
    // Make all chips have size 0, so drawing them really do anything
    for (int i=0; i<num; i++) csz[i]=0;
  }
}
screenshot[applet]

Practice problems

  1. Modify your funky shapes from a couple lectures ago so that they can be drawn anywhere at any size. [hints]
  2. Draw a snowperson (or whatever) leaning down a hill. [hints]
  3. Make the second hand of an analog clock. [hints]
  4. Create a sketch that draws lines at random positions in the window pointing toward wherever the mouse is. [hints]