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.
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(); }
[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 }
[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).
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; }
[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).
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(); }
[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(); }
[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(); }
[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); } }
[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; } }
[applet]
Practice problems
- Modify your funky shapes from a couple lectures ago so that they can be drawn anywhere at any size. [hints]
- Draw a snowperson (or whatever) leaning down a hill. [hints]
- Make the second hand of an analog clock. [hints]
- Create a sketch that draws lines at random positions in the window pointing toward wherever the mouse is. [hints]