CS 2, Winter 2008
Programming for Interactive Digital Arts

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.

sketch1
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();
}
BoxSpring
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;
    }
  }
}
screenshot[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.

sketch2
// 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);
}
Spring
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;
  }
}
screenshot[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.

sketch3
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);
}
screenshot[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.

sketch4
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);
}
screenshot[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.

sketch5
// 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;
}
Ball
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; }
  }
}
Spring
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;
    }
 }
}
screenshot[applet]

Practice problems

  1. Make a pattern to the springiness of the box springs (e.g., springier in the middle than on the edges). [hints]
  2. Have a bunch of worms seeking different food sources. [hints]
  3. Incorporate gravity into the mass-spring system. [hints]
  4. 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).