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);
[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); }
[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)); } }
[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+");"); } }
[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]+");"); } }
[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);
[applet]
Programming notes
- Function definition
- A function definition is of the form
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.
type name(type1 paramName1, type2 paramName2, ...) { statements } - 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
- 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.
- Create funky shapes with curved sides and bezier sides, and similarly compare your by-hand versions with the results from the sketch tools.
- 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]