CS 2, Winter 2008
Programming for Interactive Digital Arts

Jan 23: More complex shapes


Shapes

Rectangles and ellipses are great, and by stacking them just right we can create things like snowpeople, but sometimes we want even fancier shapes. Processing provides a way to define a shape by a set of vertices defining points on its boundary

strokeWeight(5);
fill(150,150,255);
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);
screenshot[applet]

The vertices of a shape are specified via the vertex() function, which takes x and y coordinates. Before calling that function, we must tell Processing that we're starting a new shape, by calling the beginShape() function; when we've listed all the vertices, we must call the endShape() function. Parameters may be given to beginShape() to produce different forms -- individual points or lines, sets of individual triangles or quads, or tesselations. I'll let you play with those variations on your own. There is an optional parameter for endShape(); if it is given the value CLOSE, the last vertex is connected back to the first, while otherwise the shape is left "open".

We can specify colors for the shape by calling stroke() and fill() before beginShape().

Our own functions

Once we've gone to all the work to figure out the vertices for a shape, it would be nice to be able to reuse the shape, e.g., putting it in different places (a monogrammer?). That is, we want to be able to package up our shape, and use it just like ellipse(). We can do just that by defining our own function.

int sz = 25;    // how big to draw

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

void draw()
{
  // Nothing to do here (but need the blank function anyway)
}

void mousePressed()
{
  fill(random(255),random(255),random(255));
  drawC(mouseX, mouseY, sz);
}

void keyPressed()
{
  if (key=='Z') {      // Bigger
    if (sz<50) sz+=5;
  }
  else if (key=='z') {
    if (sz > 5) sz-=5;
  }
}

// Draw a letter "C" of size s, with the upper left at (x,y).
void drawC(float x, float y, int s)
{
  beginShape();
  vertex(x,       y);
  vertex(x+s,     y);
  vertex(x+s,     y+0.2*s);
  vertex(x+0.2*s, y+0.2*s);
  vertex(x+0.2*s, y+0.8*s);
  vertex(x+s,     y+0.8*s);
  vertex(x+s,     y+s);
  vertex(x,       y+s);
  endShape(CLOSE);
}
screenshot[applet]

Defining our own function works much the same way as filling in setup() and draw(), but we choose our own name, and our own parameters (names and types). We can use the parameters within the body of the function, just like local variables. We then call it the same way we call built-in Processing functions. Details on function definition are in the Programming notes section.

In our drawC function, to draw the shape at a specified position, we simply use the variables x and y instead of 0 and 0 in the original sketch. To allow the shape to be scaled, we treat the coordinates in the original sketch as percentages, so that 100 is 1.0 of the size, 80 is 0.8 of the size, and so forth.

The following sketch uses functions to separate out distinct tasks -- constructing a shape (placing and moving vertices) vs. stamping a shape. The names and structure lead to code that's easier to read.

// Vertices of a shape
int maxVertices = 100;
float[] x = new float[maxVertices], y = new float[maxVertices];
// Which vertex is next to add
int v = 0;
// Which vertex is being dragged (-1 if none)
int dragging = -1;
// Current color to stamp
color c = color(random(128,255),random(128,255),random(128,255));

void setup()
{
  size(800,400);
  background(0);
  stroke(255);
  smooth();
  textFont(loadFont("LucidaSans-24.vlw"));
  line(width/2+1,0,width/2+1,height);
  strokeWeight(3);
}

void draw()
{
  fill(0); 
  noStroke();
  rect(0,0,width/2,height);
  fill(c); 
  stroke(c);
  // Create a shape from the current vertices
  beginShape();
  for (int i=0; i<v; i++) vertex(x[i],y[i]);
  endShape(CLOSE);
  // Label the vertices
  for (int i=0; i<v; i++) {
    if (i==dragging) fill(255,255,0);
    else fill(255);
    text(i+1,x[i],y[i]);
  }
}

void mousePressed()
{
  // Left half: constructing shape; right half: stamping it
  if (mouseX < width/2) construct();
  else stamp();
}

// Add a new vertex or begin dragging an existing one
void construct()
{ 
  // See if any vertex is close enough to be dragged
  dragging = -1;
  for (int i=0; i<v; i++) {
    if (dist(mouseX,mouseY,x[i],y[i]) < 10) dragging = i;
  }
  if (dragging < 0) {  // Nobody's close; add a new vertex
    x[v] = mouseX; 
    y[v] = mouseY;
    if (v<maxVertices) v++;
  }
}

// Draw a copy of the shape
void stamp() 
{ 
  if (v>0) {  // Nothing to do if no vertices yet
    // Offset the coordinates of the original vertices
    // according to the current mouse location
    float dx = mouseX-x[0];
    float dy = mouseY-y[0];
    fill(c); 
    stroke(c);
    beginShape();
    for (int i=0; i<v; i++) vertex(x[i]+dx,y[i]+dy);
    endShape(CLOSE);
  }
}

void mouseDragged()
{
  if (dragging >= 0) {
    x[dragging] = mouseX;
    y[dragging] = mouseY;
  }
}

void mouseReleased()
{
  dragging = -1;
}

void keyPressed()
{
  if (key=='p') {  // Print vertices
    for (int i=0; i<v; i++)
      println("vertex("+x[i]+","+y[i]+");");
  }
  else if (key=='n') {   // Start drawing a new shape
    v = 0;
    c = color(random(128,255),random(128,255),random(128,255));
  }
}
screenshot[applet]

Curves

The segments of a shape can be curved rather than straight, by using curveVertex() instead of vertex(). Processing figures out how to draw good curves between vertices, using what are called Catmull-Rom splines. The first and last vertices aren't drawn as part of the curve, but are control points used to help set the curvature at the neighboring vertices. The overall amount of curvature can be set with the curveTightness() function, with increasing positive numbers bulging more and more, and increasing negative number bowing more and more.

// Vertices of a shape
int maxVertices = 100;
float[] x = new float[maxVertices], y = new float[maxVertices];
// Which vertex is next to add
int v = 0;
// Which vertex is being dragged (-1 if none)
int dragging = -1;
// Curve tightness
int tight=0;

void setup()
{
  size(400,400);
  background(0);
  smooth();
  textFont(loadFont("LucidaSans-24.vlw"));
  curveTightness(tight);
}

void draw()
{
  background(0);
  noFill(); stroke(255);
  // Create a shape from the current vertices
  beginShape();
  for (int i=0; i<v; i++) curveVertex(x[i],y[i]);
  endShape();
  // Label the vertices
  if (v>0) {
    // Interior points (on curve)
    fill(0,255,0); noStroke();
    for (int i=1; i<v-1; i++) {
      ellipse(x[i],y[i],10,10);
      text(i,x[i],y[i]);
    }
    // Start and end points (not on curve)
    fill(255,0,0);
    ellipse(x[0],y[0],10,10);
    text("S",x[0],y[0]);
    ellipse(x[v-1],y[v-1],10,10);
    text("E",x[v-1],y[v-1]);
  }
}

void mousePressed()
{
  // See if any vertex is close enough to be dragged
  dragging = -1;
  for (int i=0; i<v; i++) {
    if (dist(mouseX,mouseY,x[i],y[i]) < 10) dragging = i;
  }
  if (dragging < 0) {  // Nobody's close; add a new vertex
    x[v] = mouseX;
    y[v] = mouseY;
    if (v<maxVertices) v++;
  }
}

void mouseDragged()
{
  if (dragging >= 0) {
    x[dragging] = mouseX;
    y[dragging] = mouseY;
  }
}

void mouseReleased()
{
  dragging = -1;
}

void keyPressed()
{
  if (key=='p') {  // Print vertices
    for (int i=0; i<v; i++)
      println("curveVertex("+x[i]+","+y[i]+");");
  }
  else if (key=='T') { // Increase curve tightness
    tight++;
    curveTightness(tight);
    println("curveTightness("+tight+");");
  }
  else if (key=='t') {
    tight--;
    curveTightness(tight);
    println("curveTightness("+tight+");");
  }
}
screenshot[applet]

Bezier curves give more control over the curvature (you might have used them in a drawing program). In addition to the points on the curve (anchor points), Bezier curves also have associated control points that set the slope at the anchors.

// Vertices of a shape
int maxVertices = 100;
float[] x = new float[maxVertices], y = new float[maxVertices];
// Control points (2 per segment)
float [] cx = new float[2*maxVertices], cy = new float[2*maxVertices];
// Which vertex is next to add
int v = 0;
// Which vertex or control point is being dragged (-1 if none)
int dragging = -1, cdragging = -1;

void setup()
{
  size(400,400);
  background(0);
  smooth();
  rectMode(CENTER);
  textFont(loadFont("LucidaSans-24.vlw"));
}

void draw()
{
  if (v>0) {
    background(0);
    noFill(); 
    stroke(255);
    // Create a shape from the current vertices
    beginShape();
    vertex(x[0],y[0]);
    for (int i=1; i<v; i++)
      bezierVertex(cx[2*i-1],cy[2*i-1],cx[2*i],cy[2*i],x[i],y[i]);
    endShape();
    // Label the vertices
    fill(0,255,0); 
    noStroke();
    for (int i=1; i<v; i++) {
      rect(x[i],y[i],10,10);
      text(i,x[i],y[i]);
    }
    fill(0,255,0); 
    rect(x[0],y[0],10,10);
    text("S",x[0],y[0]);
    // Label the control points
    for (int i=1; i<v; i++) {
      fill(255,0,0); 
      stroke(255,0,0);
      ellipse(cx[2*i-1],cy[2*i-1],10,10);
      line(cx[2*i-1],cy[2*i-1],x[i-1],y[i-1]);
      fill(0,0,255); 
      stroke(0,0,255);
      ellipse(cx[2*i],cy[2*i],10,10);
      line(cx[2*i],cy[2*i],x[i],y[i]);
    }
  }
}

void mousePressed()
{
  // See if any vertex is close enough to be dragged
  dragging = -1; 
  cdragging = -1;
  for (int i=0; i<v; i++) {
    if (dist(mouseX,mouseY,x[i],y[i]) < 10) dragging = i;
  }
  if (dragging < 0) {
    // See if any control point is close enough to be dragged
    for (int i=1; i<2*v; i++) {
      if (dist(mouseX,mouseY,cx[i],cy[i]) < 10) cdragging = i;
    }
    if (cdragging < 0) {  // Nobody's close; add a new vertex
      x[v] = mouseX; 
      y[v] = mouseY;
      if (v>0) {
        // Add control points -- one to previous vertex and one to this one
        cx[2*v-1] = x[v-1]; 
        cy[2*v-1] = y[v-1]+20;
        cx[2*v] = mouseX; 
        cy[2*v] = mouseY-20;
      }
      if (v<maxVertices) v++;
    }
  }
}

void mouseDragged()
{
  if (dragging >= 0) {        // drag a vertex
    x[dragging] = mouseX;
    y[dragging] = mouseY;
  }
  else if (cdragging >= 0) {  // drag a control point
    cx[cdragging] = mouseX;
    cy[cdragging] = mouseY;
  }
}

void mouseReleased()
{
  dragging = -1;
}

void keyPressed()
{
  if (key=='p') {
    println("vertex("+x[0]+","+y[0]+");");
    for (int i=1; i<v; i++)
      println("bezierVertex("+cx[2*i-1]+","+cy[2*i-1]+","+cx[2*i]+","+cy[2*i]+","+x[i]+","+y[i]+");");
  }
}
screenshot[applet]

To specify a Bezier curve, we give the first vertex position with the regular vertex() function. Then we call the bezierVertex() function with the positions of two control points and an anchor point. The first control point (first two parameters) is for the preceding vertex and the second is for this one.

A shape that has both straight sides and curved sides can be created by combining vertex types.

smooth(); noStroke(); fill(50,0,200);
beginShape();
curveVertex(30, 50);
curveVertex(20, 40);
curveVertex(35, 15);
curveVertex(65, 15);
curveVertex(80, 40);
curveVertex(70, 50);
vertex(80,70);
vertex(72,85);
vertex(65,70);
vertex(58,85);
vertex(50,70);
vertex(42,85);
vertex(35,70);
vertex(28,85);
vertex(20,70);
endShape(CLOSE);
screenshot[applet]

Programming notes

Function definition
A function definition is of the form
type name(type1 paramName1, type2 paramName2, ...)
{
  statements
}
The name should follow the same rules and style as for variables (alphanumeric, beginning with a letter, internally capitalized, avoiding Processing words). There need not be any parameters. The first type (the return type) can be "void", meaning that the function just executes statements and doesn't return any value.
Return
We didn't see any examples of this here, but for completeness, it's worth noting that a function can return a value (e.g., like min()). The statement "return value;" stops the function right there, and returns the value to whoever called the function. A function that doesn't return a value can just call "return;" to stop right there.
Scope
We've seen several different variable scopes. Recall that global variables, declared before setup, can be used anywhere and hold their values across function calls. Local variables are declared within a function, are only available within the function, and are reinitialized each time. The same holds for function parameters. Note that we can reuse the same variable (or parameter) name in different functions -- they're totally separate. Variables declared in a loop initialization are even more local -- only within the loop (and can likewise be reused elsewhere without conflict). Variables can likewise be declared within the body of a loop or a conditional, and are local to that block of code. One way to think about it is as if the variables "live" only within the curly braces where they are declared.

Practice problems

  1. Create a funky shape with straight lines. First think it through yourself, and then draw it with the "stamping" sketch and hit the "p" button to see the commands.
  2. Create funky shapes with curved sides and bezier sides, and similarly compare your by-hand versions with the results from the sketch tools.
  3. Package up the snowperson sketch into a function to draw a snowperson at some location. Maybe don't draw the snowperson in all its glory -- just the main body. [hints]