14. 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". She did it with sinusoids, but we will do it here by reusing our previous approach to springs. Mousing over a circle 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.
// Based on Greenberg Appendix A-13 Spring[] springs = new Spring[30]; function setup() { createCanvas(400,300); smooth(); // Evenly space the springs across the window, // with a bit of margin (25 on each side) var boxR = 0.5*(width-50.0)/springs.length; var x0 = 25 + boxR; for (var i=0; i<springs.length; i++) springs[i] = new Spring(2*boxR*i+x0,height/2, boxR); } function draw() { background(0); // Our usual convention: draw and update each object for (var i=0; i<springs.length; i++) springs[i].draw(); for (var i=0; i<springs.length; i++) springs[i].update(); }
class Spring { var cx, cy; // rest position var x, y; // current position var vx=0, vy=0; // velocity in the two directions var r = 10; // radius var k = 0.05; // spring constant var d = 0.75; // damping // Initialize a Spring at rest position (x0,y0), of size w*h Spring(var x0, var y0, var r0) { x = x0; y = y0; cx = x0; cy = y0; r = r0; // Random spring constant and damping k = random(0.01,0.02); d = random(0.975,1.0); } function draw() { stroke(128); strokeWeight(3); line(cx,cy,x,y); fill(255); stroke(128); strokeWeight(1); ellipse(x,y,2*r,2*r); } function update() { if(dist(mouseX,mouseY,x,y) < r) { // Give it a kick of velocity when (while) mouse is over it vy = 10; } // usual spring stuff vx -= k * (x-cx); vy -= k * (y-cy); vx *= d; vy *= d; x += vx; y += vy; } }
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.
// Based on R&F 50-16 // Two connected springs Spring spring1, spring2; function setup() { createCanvas(400,400); smooth(); spring1 = new Spring(width/2,height/2,10); spring2 = new Spring(width/2,height/2,10); } function 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 { var cx, cy; // rest position var x, y; // current position var vx=0, vy=0; // velocity in the two directions var r = 10; // radius var k = 0.05; // spring constant var d = 0.75; // damping var g = 1; // gravity // Initialize a Spring at rest position (x0,y0) Spring(var x0, var y0, var r0) { x = x0; y = y0; cx = x0; cy = y0; r = r0; } function draw() { stroke(128); strokeWeight(3); line(cx,cy,x,y); fill(255); stroke(128); strokeWeight(1); ellipse(x,y,2*r,2*r); } // Update the spring, with a new center function update(var ncx, var ncy) { // set the new center cx = ncx; cy = ncy; // usual spring stuff vx -= k * (x-cx); vy -= k * (y-cy); vy += g; vx *= d; vy *= d; x += vx; y += vy; } }
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.
// Based on R&F 50-17 Spring[] springs = new Spring[30]; function setup() { createCanvas(400,600); smooth(); // Construct springs at center horizontally, equally spaced vertically for (var i=0; i<springs.length; i++) springs[i] = new Spring(width/2, i*height/springs.length,10); } function draw() { background(0); for (var i=0; i<springs.length; i++) springs[i].draw(); // Recenter first spring at the mouse springs[0].update(mouseX,mouseY); // Recenter each following spring at preceding spring for (var i=1; i<springs.length; i++) springs[i].update(springs[i-1].x, springs[i-1].y); }
class Spring { var cx, cy; // rest position var x, y; // current position var vx=0, vy=0; // velocity in the two directions var r = 10; // radius var k = 0.05; // spring constant var d = 0.75; // damping var g = 1; // gravity // Initialize a Spring at rest position (x0,y0) Spring(var x0, var y0, var r0) { x = x0; y = y0; cx = x0; cy = y0; r = r0; } function draw() { stroke(128); strokeWeight(3); line(cx,cy,x,y); fill(255); stroke(128); strokeWeight(1); ellipse(x,y,2*r,2*r); } // Update the spring, with a new center function update(var ncx, var ncy) { // set the new center cx = ncx; cy = ncy; // usual spring stuff vx -= k * (x-cx); vy -= k * (y-cy); vy += g; vx *= d; vy *= d; x += vx; y += vy; } }
Greenberg (11-16) has a fancier version of this same basic idea, making a worm crawling toward food. Here we present a variation that uses our same Spring class (modified to draw outlines 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 constant step along it.
// Based on Greenberg 11-16 Spring[] segments = new Spring[30]; function setup() { createCanvas(400,400); smooth(); // Construct segments, with size and spring parameters depending on position along worm for (var i=0; i<segments.length; i++) { var r; if (i > segments.length/2) r = segments.length-i; // smaller toward either end else r = i; // These numbers are pretty magical, from Greenberg segments[i] = new Spring(width/2, height/2, r, .0035*(i+1), .95-.02*i); } } function draw() { background(0); for (var i=0; i<segments.length; i++) segments[i].draw(); // Recenter first segment // Rather than targeting mouse position, target a constant-sized (speed) // step toward the mouse position var speed = 25; var dx = mouseX-segments[0].x, dy = mouseY-segments[0].y; var d = sqrt(dx*dx+dy*dy); // dx/d and dy/d give the fractions of the diagonal step that lie in the two directions if (d>0) segments[0].update(segments[0].x+speed*dx/d, segments[0].y+speed*dy/d); // Recenter each following segment at preceding segment for (var i=1; i<segments.length; i++) segments[i].update(segments[i-1].x, segments[i-1].y); }
class Spring { var cx, cy; // rest position var x, y; // current position var vx=0, vy=0; // velocity in the two directions var r; // radius var k; // spring constant var d; // damping // Initialize a Spring at rest position (x0,y0), radius r, // spring constant k, and damping d Spring(var x0, var y0, var r0, var k0, var d0) { x = x0; y = y0; cx = x0; cy = y0; r = r0; k = k0; d = d0; } function draw() { fill(0); stroke(255); strokeWeight(2); ellipse(x,y,r*2,r*2); } // Update the spring, with a new center function update(var ncx, var ncy) { cx = ncx; cy = ncy; // usual spring stuff vx -= k * (x-cx); vy -= k * (y-cy); vx *= d; vy *= d; x += vx; y += vy; } }
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. Dragging makes use of the Processing functions mouseDragged() and mouseReleased(), which are analogous to mousePressed(), called once for each event of the mouse being pressed/dragged/released.
// Based on R&F synthesis "soda processing", // which is in turn based on the Soda Constructor, http://www.sodaplay.com // The arrays of balls and springs var 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) var currBall=0, currSpring=0; // If a ball has been selected, its index (-1 if none) var selBall=-1; // Universal spring constant and drag coefficient // (each Ball and Spring has its own field, so they could be set independently) var k=0.1, drag=0.95; // Whether we are building (adding balls and springs) -- true // or simulating -- false var building=true; function setup() { createCanvas(400,400); smooth(); } function draw() { // Show mode by way of background if (building) background(128); else background(255); // Draw the springs first stroke(0); strokeWeight(2); for (var i=0; i<currSpring; i++) springs[i].draw(); // Now draw the balls noStroke(); for (var 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 (var i=0; i<currSpring; i++) springs[i].apply(); // Then update the ball velocities and positions for (var i=0; i<currBall; i++) balls[i].update(); } } function mousePressed() { // See if any ball is close enough to be selected // I found it useful to give a little slop -- 1.5*radius var sel = -1; for (var 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; } } } function 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; } } function mouseReleased() { if (!building) selBall = -1; } function 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 function setK(var newK) { k = newK; println("k:"+k); for (var i=0; i<currSpring; i++) springs[i].k = k; } // Set the drag coefficient for all balls function setDrag(var newDrag) { drag = newDrag; println("drag:"+drag); for (var i=0; i<currBall; i++) balls[i].drag = drag; }
class Ball { var x, y; // position var vx=0, vy=0; // velocity in the two directions var r=5; // radius var drag; // multiplicative factor for velocity Ball(var x0, var y0, var drag) { x = x0; y = y0; this.drag = drag; } function draw() { ellipse(x,y,r*2,r*2); } function 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; var rest; // rest length var k; // spring constant // Initialize a Spring between balls b1 and b2, with spring constant k Spring(Ball b1, Ball b2, var k) { this.b1 = b1; this.b2 = b2; this.k = k; rest = dist(b1.x,b1.y, b2.x,b2.y); } function draw() { line(b1.x,b1.y, b2.x,b2.y); } // Apply the spring to its masses function apply() { // Restorative force is proportional to difference in length from rest length var len = dist(b1.x,b1.y, b2.x,b2.y); if (len>0) { var f = k*(len-rest); // Apply it to x coordinates, moving each toward the other var fx = f*(b1.x-b2.x)/len; b1.vx -= fx; b2.vx += fx; // Similarly with y var fy = f*(b1.y-b2.y)/len; b1.vy -= fy; b2.vy += fy; } } }