Feb 7: Spring systems
Arrays of unconnected springs
Just as we had arrays of moving objects, we can have arrays of springs. The appendix of Greenberg's book has a cool example of a bunch of unconnected springs (sketch 13), which he calls "box springs". He did it with sinusoids; I've rewritten it here to use our previous approach to springs. Mousing over a box gives it an initial velocity, then the spring goes to work. The different springs have different spring constants and damping factors; try it with uniform ones.
int num = 50; // how many springs BoxSpring[] springs = new BoxSpring[num]; void setup() { size(400,300); smooth(); noStroke(); background(0); rectMode(CENTER); // Evenly space the springs across the window, // with a bit of margin (25 on each side) float boxW = (width-50.0)/num, boxH=50; float x0 = 25 + boxW/2.0; for (int i=0; i<num; i++) springs[i] = new BoxSpring(boxW*i+x0,height/2, boxW,boxH); } void draw() { background(0); // Our usual convention: draw and update each object for (int i=0; i<num; i++) springs[i].draw(); for (int i=0; i<num; i++) springs[i].update(); }
class BoxSpring { float cx, cy; // rest position float x, y; // current position float vy=0; // velocity (only spring up and down) float w, h; // width and height of the box float k; // spring constant float d; // damping // Initialize a Spring at rest position (x0,y0), of size w*h BoxSpring(float x0, float y0, float w, float h) { x = x0; y = y0; cx = x0; cy = y0; this.w = w; this.h = h; // Random spring constant and damping k = random(0.01,0.02); d = random(0.95,1.0); } void draw() { // Color according to how far away from rest position float r = abs(y-cy); fill(255,255-r,2*r); rect(x,y,w,h); } void update() { if (mouseX > x-w/2 && mouseX < x+w/2 && mouseY > y-h/2 && mouseY < y+h/2) { // Give it a kick of velocity when (while) mouse is over it vy = 10; } else { // Usual spring stuff vy -= k * (y-cy); vy *= d; y += vy; } } }
[applet]
Spring snakes
We can get interesting behaviors if we connect springs together, rather than having them move independently of each other. Let's start with just a pair of springs (following based on Reas and Fry, 50-16), and make one spring's center (rest position) follow the mouse and the other spring's center follow the current position of the first spring. Thus each frame we will draw the springs (here with an ellipse for the current position and a line from that to the center), adjust their centers, and do the usual spring stuff. Some gravity orients them vertically.
// Two connected springs Spring spring1, spring2; void setup() { size(100,100); smooth(); background(0); strokeWeight(3); spring1 = new Spring(width/2,height/2); spring2 = new Spring(width/2,height/2); } void draw() { background(0); spring1.draw(); spring2.draw(); // Recenter spring1 at the mouse spring1.update(mouseX,mouseY); // Recenter spring2 at spring1 spring2.update(spring1.x,spring1.y); }
class Spring { float cx, cy; // rest position float x, y; // current position float vx=0, vy=0; // velocity in the two directions float r = 10; // radius float k = 0.05; // spring constant float d = 0.75; // damping float g = 1.0; // gravity // Initialize a Spring at rest position (x0,y0) Spring(float x0, float y0) { x = x0; y = y0; cx = x0; cy = y0; } void draw() { // A "mass" at the current position, and a line to the center noStroke(); ellipse(x,y,r*2,r*2); stroke(128); line(cx,cy, x,y); } // Update the spring, with a new center void update(float ncx, float ncy) { cx = ncx; cy = ncy; // Spring equations for both x and y; gravity for y vx -= k * (x-cx); vx *= d; x += vx; vy -= k * (y-cy); vy += g; vy *= d; y += vy; } }
[applet]
Now we can do the same thing with an array of springs (based on Reas and Fry 50-17). When we create the springs, we stack them up vertically. Then each frame we update the zeroth spring to center on the mouse, the first to center on the zeroth's current position, the second to center on the first's, etc.
int num=30; Spring[] springs = new Spring[30]; void setup() { size(200,600); smooth(); background(0); strokeWeight(3); // Construct springs at center horizontally, equally spaced vertically for (int i=0; i<num; i++) springs[i] = new Spring(width/2, i*height/num); } void draw() { background(0); for (int i=0; i<num; i++) springs[i].draw(); // Recenter first spring at the mouse springs[0].update(mouseX,mouseY); // Recenter each following spring at preceding spring for (int i=1; i<num; i++) springs[i].update(springs[i-1].x, springs[i-1].y); }
[applet]
Greenberg (11-16) has a fancier version of this same basic idea, making a worm crawling toward food. I've rewritten it to use our same Spring class (modified to draw ellipses outlined rather than filled), and to move toward the mouse. There are a few important aspects to this sketch. First note that when we create the springs (in setup()), they have different radii (for the worm look) as well as different spring constants and damping coefficients. The springs toward the back are stiffer than those toward the front, and are damped more; thus they don't spring as much. Second note that the worm moves at a constant speed. I did this because when I first had the first segment re-center on the mouse (as in our previous sketch), the worm really bounced all over the place, rather than slithered (give it a try). The approach to moving at a constant speed is kind of like easing -- look at the difference between the current position and the mouse position. But rather than moving a fraction of the way along that direction, we move a contant step along it.
int num=30; Spring[] segments = new Spring[num]; int speed=10; // how big a step to take each time void setup() { size(400,400); smooth(); background(0); // Construct segments, with size and spring parameters depending on position along worm for (int i=0; i<num; i++) { int r = i; if (i > num/2) r = num-i; // These numbers are pretty magical, from Greenberg segments[i] = new Spring(width/2, height/2, r, .0035*(i+1), .95-.02*i); } } void draw() { background(0); for (int i=0; i<num; i++) segments[i].draw(); // Recenter first segment // Rather than targeting mouse position, target a constant-sized // step toward the mouse position float dx = mouseX-segments[0].x, dy = mouseY-segments[0].y; float l = sqrt(dx*dx+dy*dy); // dx/l and dy/l give the fractions of the diagonal step that lie in the two directions if (l>0) segments[0].update(segments[0].x+speed*dx/l, segments[0].y+speed*dy/l); // Recenter each following segment at preceding segment for (int i=1; i<num; i++) segments[i].update(segments[i-1].x, segments[i-1].y); }
[applet]
Mass-spring system
For some real fun with springs, we can set up an interactive system in which masses are hooked together with springs and then given a push. Soda Constructor is a full-fledged system for doing that (and more); a simplified Processing version is in the Synthesis section of Reas and Fry. Here is an even more simplified version that gets at the heart of the mass-spring system.
To represent the positions and velocities of the masses, we use the Ball class from before (stripped down to the relevant parts). To represent springs, we modify the Spring class so that rather than being defined in terms of a rest position, a spring is defined in terms of a rest length -- the initial distance between the balls that it is connecting. A spring accelerates the two balls to try to restore their distance to that rest length.
The interaction has two modes, building/simulating, selected by keypresses ('b'/'s') and indicated by background (gray/white). When building, a mass is added by a button press away from any existing mass, and a spring is added by clicking on one mass and then another (clicking on nothing or on the same mass twice aborts a half-created spring). When simulating, a mass is picked up by clicking on it; it can then be dragged around and released, giving it a new position and velocity. The pseudo-physics then kicks in.
// The arrays of balls and springs int maxBalls=1000, maxSprings=1000; Ball[] balls = new Ball[maxBalls]; Spring[] springs = new Spring[maxSprings]; // How far we are into the arrays (how many have been created) int currBall=0, currSpring=0; // If a ball has been selected, its index (-1 if none) int selBall=-1; // Universal spring constant and drag coefficient // (each Ball and Spring has its own field, so they could be set independently) float k=0.1, drag=0.95; // Whether we are building (adding balls and springs) -- true // or simulating -- false boolean building=true; void setup() { size(400,400); smooth(); } void draw() { // Show mode by way of background if (building) background(128); else background(255); // Draw the springs first stroke(0); strokeWeight(2); for (int i=0; i<currSpring; i++) springs[i].draw(); // Now draw the balls noStroke(); for (int i=0; i<currBall; i++) { if (i == selBall) fill(0,255,0); // highlight the selected one else fill(0); balls[i].draw(); } if (!building) { // Simulate // First apply the spring forces for (int i=0; i<currSpring; i++) springs[i].apply(); // Then update the ball velocities and positions for (int i=0; i<currBall; i++) balls[i].update(); } } void mousePressed() { // See if any ball is close enough to be selected // I found it useful to give a little slop -- 1.5*radius int sel = -1; for (int i=0; i<currBall; i++) { if (dist(mouseX,mouseY, balls[i].x,balls[i].y) < 1.5*balls[i].r) sel = i; } if (!building) { // When simulating, just remember that the ball was selected selBall = sel; } else if (sel < 0) { // Nobody's close if (selBall >= 0) { // Had previously selected -- cancel that selBall = -1; } else if (currBall<maxBalls-1) { // Add a new one balls[currBall] = new Ball(mouseX,mouseY,drag); currBall++; } } else { // Selected a ball -- must select two to add a spring if (selBall < 0) { // This is the first selected selBall = sel; } else if (selBall == sel) { // Same one twice -- cancel selBall = -1; } else if (currSpring<maxSprings-1) { // Second one -- make spring springs[currSpring] = new Spring(balls[selBall],balls[sel],k); currSpring++; selBall = -1; } } } void mouseDragged() { if (!building && selBall >= 0) { // When simulating, move ball and set its velocity balls[selBall].x = mouseX; balls[selBall].y = mouseY; balls[selBall].vx = mouseX-pmouseX; balls[selBall].vy = mouseY-pmouseY; } } void mouseReleased() { if (!building) selBall = -1; } void keyPressed() { if (key=='b') building=true; else if (key=='s') building=false; else if (key=='k') setK(k*0.5); else if (key=='K') setK(min(k*2,0.999)); else if (key=='d') setDrag(1-(1-drag)*0.5); else if (key=='D') setDrag(max(1-(1-drag)*2,0.001)); } // Set the spring constant for all springs void setK(float newK) { k = newK; println("k:"+k); for (int i=0; i<currSpring; i++) springs[i].k = k; } // Set the drag coefficient for all balls void setDrag(float newDrag) { drag = newDrag; println("drag:"+drag); for (int i=0; i<currBall; i++) balls[i].drag = drag; }
class Ball { float x, y; // position float vx=0, vy=0; // velocity in the two directions float r=5; // radius float drag; // multiplicative factor for velocity Ball(float x0, float y0, float drag) { x = x0; y = y0; this.drag = drag; } void draw() { ellipse(x,y,r*2,r*2); } void update() { // Damp according to drag vx *= drag; vy *= drag; // Move in the appropriate direction by the step size x += vx; y += vy; // Bounce, with frictionless walls if (x > width-r) { x=width-r; vx=-vx; } else if (x < r) { x=r; vx=-vx; } if (y > height-r) { y=height-r; vy=-vy; } else if (y < r) { y=r; vy = -vy; } } }
class Spring { Ball b1, b2; float rest; // rest length float k; // spring constant // Initialize a Spring between balls b1 and b2, with spring constant k Spring(Ball b1, Ball b2, float k) { this.b1 = b1; this.b2 = b2; this.k = k; rest = dist(b1.x,b1.y, b2.x,b2.y); } void draw() { line(b1.x,b1.y, b2.x,b2.y); } // Apply the spring to its masses void apply() { // Restorative force is proportional to difference in length from rest length float len = dist(b1.x,b1.y, b2.x,b2.y); if (len>0) { float f = k*(len-rest); // Apply it to x coordinates, moving each toward the other float fx = f*(b1.x-b2.x)/len; b1.vx -= fx; b2.vx += fx; // Similarly with y float fy = f*(b1.y-b2.y)/len; b1.vy -= fy; b2.vy += fy; } } }
[applet]
Practice problems
- Make a pattern to the springiness of the box springs (e.g., springier in the middle than on the edges). [hints]
- Have a bunch of worms seeking different food sources. [hints]
- Incorporate gravity into the mass-spring system. [hints]
- User interface problems: in the mass-spring system, allow masses and springs to be deleted; allow individual drag and spring constant values to be set (so some balls slow down faster, and some springs are springier).