Springs

REMARK: Examples on this page will run faster when just one sketch is running in the browser! All examples can be downloaded from this zip file

The universe can be modeled from springs. Nearly every solid material can be thought of as a series of particles which are attached to each other by elastic forces.

From Isaac Newton (1686) we know his second law of motion, that F = ma.
This states that in general, force is proportional to mass times acceleration.

From Robert Hooke (1678) we know the spring law, F = -Kx.
This states that the force applied by a spring, is proportional (by some constant K) to its distention x (amount of stretch or compression). The minus sign tells us that the spring’s “restorative force” happens in the opposite direction.

To take an example, if you stretch a spring (which is rigidly fixed at one end) one centimeter to the east, it will apply a restoring force towards the west. If you stretch it two centimeters eastward, it will apply a westerly force which is twice as strong.

Because F = ma and F = -kx, we can derive that ma = -kx. Interestingly, this is actually a “second-order differential equation” — which describes a relationship between a particle’s position, x,  and its acceleration (or the rate of change of the rate of change of its position), a. The solution to such equations always take the form of an “exponentially damped cosinusoid”, in what is called damped harmonic motion:

3c_41

Pleasingly, this exactly captures the movement of a spring over time: it wiggles back and forth, gradually losing energy due to friction. Depending on the amount of damping, the spring may be very wiggly, or not. We use terms like overdamped and underdamped to describe this:

Fig-10-33

The constant K is the “spring constant” which is unique for each material. When the value of K is high, the material is stiff, snappy or brittle. When K is low, the material is soft, flexible, stretchy. (High values of K will expose the limits of Euler integration, our current numerical method for solving springs here; if K is too high, the simulation will “explode”. We’ll let you discover that…). You can find the value of K in the code samples below. Good values for our solver are in the range of 0.01 to 0.5; try using 0.1 to get started.

There is also a damping factor in our particles which describes how much energy they retain after each frame of animation. A value of 0.96 is fine to get started (meaning, they lose 4% of their energy each frame). Under no circumstances should you ever set this higher than 1.0! Lower values (e.g. 0.7, 0.5, etc.) will cause overdamping.


Examples

Springs 1 (Simple):

Here, two particles are connected to each other by a spring. Pulling one away from the other, causes the spring to exert an elastic restorative force on each.
springs-simple.js

var myParticles = [];
var mySprings = [];

// The index in the particle array, of the one the user has clicked.
var whichParticleIsGrabbed = -1;

//-------------------------
function setup() {
  createCanvas(600, 400);
  createParticles(); 
  createSpringMeshConnectingParticles(); 
}

//-------------------------
function createParticles(){
  var particle0 = new Particle(); 
  var particle1 = new Particle(); 
  
  particle0.set(250,200);
  particle1.set(350,200);
  
  myParticles.push(particle0);
  myParticles.push(particle1);
}

//-------------------------
function createSpringMeshConnectingParticles(){
  // The spring constant. 
  var K = 0.1; 
  
  // Stitch the particles together by a spring.
  var p = myParticles[0];
  var q = myParticles[1];
  var aSpring = new Spring(); 
  aSpring.set (p, q, K);
  mySprings.push(aSpring);
}


function mousePressed() {
  // If the mouse is pressed, 
  // find the closest particle, and store its index.
  whichParticleIsGrabbed = -1;
  var maxDist = 9999;
  for (var i=0; i<myParticles.length; i++) {
    var dx = mouseX - myParticles[i].px;
    var dy = mouseY - myParticles[i].py;
    var dh = sqrt(dx*dx + dy*dy);
    if (dh < maxDist) {
      maxDist = dh;
      whichParticleIsGrabbed = i;
    }
  }
}
 
 
function draw() {
  background (0);
 
  for (var i=0; i<myParticles.length; i++) {
    myParticles[i].update(); // update all locations
  }
 
  if (mouseIsPressed && (whichParticleIsGrabbed > -1)) {
    // If the user is grabbing a particle, peg it to the mouse.
    myParticles[whichParticleIsGrabbed].px = mouseX;
    myParticles[whichParticleIsGrabbed].py = mouseY;
  }
 
  for (var i=0; i<mySprings.length; i++) {
    mySprings[i].update(); // draw all springs
  }

  for (var i=0; i<mySprings.length; i++) {
    mySprings[i].render(); // draw all springs
  }

  for (var i=0; i<myParticles.length; i++) {
    myParticles[i].render(); // draw all particles
  }
  
  fill(255); 
  noStroke(); 
  text("Grab a point", 5,20); 
}
 

//==========================================================
var Particle = function Particle() {
  this.px = 0;
  this.py = 0;
  this.vx = 0;
  this.vy = 0;
  this.mass = 1.0;
  this.damping = 0.96;
  
  this.bFixed = false;
  this.bLimitVelocities = true;
  this.bPeriodicBoundaries = false;
  this.bHardBoundaries = false;
  
  
  // Initializer for the Particle
  this.set = function(x, y) {
    this.px = x;
    this.py = y;
    this.vx = 0;
    this.vy = 0;
    this.damping = 0.96;
    this.mass = 1.0;
  };

  // Add a force in. One step of Euler integration.
  this.addForce = function(fx, fy) {
    var ax = fx / this.mass;
    var ay = fy / this.mass;
    this.vx += ax;
    this.vy += ay;
  };

  // Update the position. Another step of Euler integration.
  this.update = function() {
    if (this.bFixed == false){
      this.vx *= this.damping;
      this.vy *= this.damping;
  
      this.limitVelocities();
      this.handleBoundaries();
      this.px += this.vx;
      this.py += this.vy;
    }
  };

  this.limitVelocities = function() {
    if (this.bLimitVelocities) {
      var speed = sqrt(this.vx * this.vx + this.vy * this.vy);
      var maxSpeed = 10;
      if (speed > maxSpeed) {
        this.vx *= maxSpeed / speed;
        this.vy *= maxSpeed / speed;
      }
    }
  };

  this.handleBoundaries = function() {
    if (this.bPeriodicBoundaries) {
      if (this.px > width) this.px -= width;
      if (this.px < 0) this.px += width;
      if (this.py > height) this.py -= height;
      if (this.py < 0) this.py += height;
    } else if (this.bHardBoundaries) {
      if (this.px >= width){
        this.vx = abs(this.vx)*-1;
      }
      if (this.px <= 0){
        this.vx = abs(this.vx);
      }
      if (this.py >= height){
        this.vy = abs(this.vy)*-1;
      }
      if (this.py <= 0){
        this.vy = abs(this.vy);
      }
    }
  };

  this.render = function() {
    fill(255);
    ellipse(this.px, this.py, 9, 9);
  };
}


//==========================================================
var Spring = function Spring() {
  var p;
  var q;
  var restLength;
  var springConstant;

  this.set = function(p1, p2, k) {
    p = p1;
    q = p2;
    var dx = p.px - q.px;
    var dy = p.py - q.py;
    restLength = sqrt(dx * dx + dy * dy);
    springConstant = k;
  };

  this.update = function() {
    var dx = p.px - q.px;
    var dy = p.py - q.py;
    var dh = sqrt(dx * dx + dy * dy);

    if (dh > 1) {
      var distention = dh - restLength;
      var restorativeForce = springConstant * distention; // F = -kx
      var fx = (dx / dh) * restorativeForce;
      var fy = (dy / dh) * restorativeForce;
      p.addForce(-fx, -fy);
      q.addForce( fx,  fy);
    }
  };

  this.render = function() {
    stroke(255);
    line(p.px, p.py, q.px, q.py);
  };
}

When particles are connected by rigid elements in a triangle, we have the fundamental element of truss structures. All architects know this. (The following simulation also has a gravity force added in to the particles, so that they are affected both by gravity and by spring forces.)
springs-triangle.js

var myParticles = [];
var mySprings = [];

// The index in the particle array, of the one the user has clicked.
var whichParticleIsGrabbed = -1;

//-------------------------
function setup() {
  createCanvas(600, 400);
  createParticles(); 
  createSpringMeshConnectingParticles(); 
}

//-------------------------
function createParticles(){
  var particle0 = new Particle(); 
  var particle1 = new Particle(); 
  var particle2 = new Particle(); 
  
  particle0.set(250,200);
  particle1.set(350,200);
  particle2.set(300,286);
  
  particle0.bHardBoundaries = true; 
  particle1.bHardBoundaries = true; 
  particle2.bHardBoundaries = true; 
  
  myParticles.push(particle0);
  myParticles.push(particle1);
  myParticles.push(particle2);
}

//-------------------------
function createSpringMeshConnectingParticles(){
  // The spring constant. 
  var K = 0.1; 
  
  // Stitch the particles together by springs.
  var p0 = myParticles[0];
  var p1 = myParticles[1];
  var p2 = myParticles[2];
  
  var aSpring0 = new Spring(); 
  aSpring0.set (p0, p1, K);
  mySprings.push(aSpring0);
  
  var aSpring1 = new Spring(); 
  aSpring1.set (p1, p2, K);
  mySprings.push(aSpring1);
  
  var aSpring2 = new Spring(); 
  aSpring2.set (p0, p2, K);
  mySprings.push(aSpring2);
}


function mousePressed() {
  // If the mouse is pressed, 
  // find the closest particle, and store its index.
  whichParticleIsGrabbed = -1;
  var maxDist = 9999;
  for (var i=0; i<myParticles.length; i++) {
    var dx = mouseX - myParticles[i].px;
    var dy = mouseY - myParticles[i].py;
    var dh = sqrt(dx*dx + dy*dy);
    if (dh < maxDist) {
      maxDist = dh;
      whichParticleIsGrabbed = i;
    }
  }
}
 
 
function draw() {
  background (0);
 
  for (var i=0; i<myParticles.length; i++) {
    myParticles[i].addForce(0, 0.1); // gravity!
    myParticles[i].update(); // update all locations
  }
 
  if (mouseIsPressed && (whichParticleIsGrabbed > -1)) {
    // If the user is grabbing a particle, peg it to the mouse.
    myParticles[whichParticleIsGrabbed].px = mouseX;
    myParticles[whichParticleIsGrabbed].py = mouseY;
  }
  
 
  for (var i=0; i<mySprings.length; i++) {
    mySprings[i].update(); // update all springs
  }

  for (var i=0; i<mySprings.length; i++) {
    mySprings[i].render(); // render all springs
  }

  for (var i=0; i<myParticles.length; i++) {
    myParticles[i].render(); // render all particles
  }
  
  fill(255); 
  noStroke(); 
  text("Grab a point", 5,20); 
}


//==========================================================
var Particle = function Particle() {
  this.px = 0;
  this.py = 0;
  this.vx = 0;
  this.vy = 0;
  this.mass = 1.0;
  this.damping = 0.96;
  
  this.bFixed = false;
  this.bLimitVelocities = true;
  this.bPeriodicBoundaries = false;
  this.bHardBoundaries = false;
  
  
  // Initializer for the Particle
  this.set = function(x, y) {
    this.px = x;
    this.py = y;
    this.vx = 0;
    this.vy = 0;
    this.damping = 0.96;
    this.mass = 1.0;
  };

  // Add a force in. One step of Euler integration.
  this.addForce = function(fx, fy) {
    var ax = fx / this.mass;
    var ay = fy / this.mass;
    this.vx += ax;
    this.vy += ay;
  };

  // Update the position. Another step of Euler integration.
  this.update = function() {
    if (this.bFixed === false){
      this.vx *= this.damping;
      this.vy *= this.damping;
  
      this.limitVelocities();
      this.handleBoundaries();
      this.px += this.vx;
      this.py += this.vy;
    }
  };

  this.limitVelocities = function() {
    if (this.bLimitVelocities) {
      var speed = sqrt(this.vx * this.vx + this.vy * this.vy);
      var maxSpeed = 10;
      if (speed > maxSpeed) {
        this.vx *= maxSpeed / speed;
        this.vy *= maxSpeed / speed;
      }
    }
  };

  this.handleBoundaries = function() {
    if (this.bPeriodicBoundaries) {
      if (this.px > width) this.px -= width;
      if (this.px < 0) this.px += width;
      if (this.py > height) this.py -= height;
      if (this.py < 0) this.py += height;
    } else if (this.bHardBoundaries) {
      if (this.px >= width){
        this.vx = abs(this.vx)*-1;
      }
      if (this.px <= 0){
        this.vx = abs(this.vx);
      }
      if (this.py >= height){
        this.vy = abs(this.vy)*-1;
      }
      if (this.py <= 0){
        this.vy = abs(this.vy);
      }
    }
  };

  this.render = function() {
    fill(255);
    ellipse(this.px, this.py, 9, 9);
  };
}


//==========================================================
var Spring = function Spring() {
  var p;
  var q;
  var restLength;
  var springConstant;

  this.set = function(p1, p2, k) {
    p = p1;
    q = p2;
    var dx = p.px - q.px;
    var dy = p.py - q.py;
    restLength = sqrt(dx * dx + dy * dy);
    springConstant = k;
  };

  this.update = function() {
    var dx = p.px - q.px;
    var dy = p.py - q.py;
    var dh = sqrt(dx * dx + dy * dy);

    if (dh > 1) {
      var distention = dh - restLength;
      var restorativeForce = springConstant * distention; // F = -kx
      var fx = (dx / dh) * restorativeForce;
      var fy = (dy / dh) * restorativeForce;
      p.addForce(-fx, -fy);
      q.addForce( fx,  fy);
    }
  };

  this.render = function() {
    stroke(255);
    line(p.px, p.py, q.px, q.py);
  };
}

It’s possible to “fix” one or more particles to a rigid location. It is as if one particle were attached to something too large to move:
springs-fixed.js

var myParticles = [];
var mySprings = [];

//-------------------------
function setup() {
  createCanvas(600, 400);
  createParticles(); 
  createSpringMeshConnectingParticles(); 
}

//-------------------------
function createParticles(){
  var particle0 = new Particle(); 
  var particle1 = new Particle(); 
  
  particle0.set(300,150);
  particle1.set(300,250);
  particle0.bFixed = true;
  
  myParticles.push(particle0);
  myParticles.push(particle1);
}

//-------------------------
function createSpringMeshConnectingParticles(){
  // The spring constant. 
  var K = 0.1; 
  
  // Stitch the particles together by a spring.
  var p = myParticles[0];
  var q = myParticles[1];
  var aSpring = new Spring(); 
  aSpring.set (p, q, K);
  mySprings.push(aSpring);
}


 
function draw() {
  background (0);
 
  for (var i=0; i<myParticles.length; i++) {
    myParticles[i].addForce(0, 0.5); // gravity!
    myParticles[i].update(); // update all locations
  }
 
  if (mouseIsPressed) {
    // If the user is grabbing particle 0, peg it to the mouse.
    myParticles[1].px = mouseX;
    myParticles[1].py = mouseY;
  }
 
  for (var i=0; i<mySprings.length; i++) {
    mySprings[i].update(); // draw all springs
  }

  for (var i=0; i<mySprings.length; i++) {
    mySprings[i].render(); // draw all springs
  }

  for (var i=0; i<myParticles.length; i++) {
    myParticles[i].render(); // draw all particles
  }
  
  
  fill(255); 
  rect(200,100,200,50); 
  noStroke(); 
  text("Grab a point", 5,20); 
  
}
 

//==========================================================
var Particle = function Particle() {
  this.px = 0;
  this.py = 0;
  this.vx = 0;
  this.vy = 0;
  this.mass = 1.0;
  this.damping = 0.96;
  
  this.bFixed = false;
  this.bLimitVelocities = true;
  this.bPeriodicBoundaries = false;
  this.bHardBoundaries = false;
  
  
  // Initializer for the Particle
  this.set = function(x, y) {
    this.px = x;
    this.py = y;
    this.vx = 0;
    this.vy = 0;
    this.damping = 0.96;
    this.mass = 1.0;
  };

  // Add a force in. One step of Euler integration.
  this.addForce = function(fx, fy) {
    var ax = fx / this.mass;
    var ay = fy / this.mass;
    this.vx += ax;
    this.vy += ay;
  };

  // Update the position. Another step of Euler integration.
  this.update = function() {
    if (this.bFixed == false){
      this.vx *= this.damping;
      this.vy *= this.damping;
  
      this.limitVelocities();
      this.handleBoundaries();
      this.px += this.vx;
      this.py += this.vy;
    }
  };

  this.limitVelocities = function() {
    if (this.bLimitVelocities) {
      var speed = sqrt(this.vx * this.vx + this.vy * this.vy);
      var maxSpeed = 10;
      if (speed > maxSpeed) {
        this.vx *= maxSpeed / speed;
        this.vy *= maxSpeed / speed;
      }
    }
  };

  this.handleBoundaries = function() {
    if (this.bPeriodicBoundaries) {
      if (this.px > width) this.px -= width;
      if (this.px < 0) this.px += width;
      if (this.py > height) this.py -= height;
      if (this.py < 0) this.py += height;
    } else if (this.bHardBoundaries) {
      if (this.px >= width){
        this.vx = abs(this.vx)*-1;
      }
      if (this.px <= 0){
        this.vx = abs(this.vx);
      }
      if (this.py >= height){
        this.vy = abs(this.vy)*-1;
      }
      if (this.py <= 0){
        this.vy = abs(this.vy);
      }
    }
  };

  this.render = function() {
    fill(255);
    ellipse(this.px, this.py, 9, 9);
  };
}


//==========================================================
var Spring = function Spring() {
  var p;
  var q;
  var restLength;
  var springConstant;

  this.set = function(p1, p2, k) {
    p = p1;
    q = p2;
    var dx = p.px - q.px;
    var dy = p.py - q.py;
    restLength = sqrt(dx * dx + dy * dy);
    springConstant = k;
  };

  this.update = function() {
    var dx = p.px - q.px;
    var dy = p.py - q.py;
    var dh = sqrt(dx * dx + dy * dy);

    if (dh > 1) {
      var distention = dh - restLength;
      var restorativeForce = springConstant * distention; // F = -kx
      var fx = (dx / dh) * restorativeForce;
      var fy = (dy / dh) * restorativeForce;
      p.addForce(-fx, -fy);
      q.addForce( fx,  fy);
    }
  };

  this.render = function() {
    stroke(255);
    line(p.px, p.py, q.px, q.py);
  };
}

Springs 2 (Rope, with and without Gravity):

We can connect multiple particles into a rope. A for() loop has been used to connect each particle to the next one. Here we see the basic code structure we will be using: an array of particles, and an array of springs which (selectively) connects certain ones among them:
springs-rope.js

Here is the same example, but with a gravity force added in. Architects will readily recognize that the simulation is able to achieve the well-known catenary curve.
springs-rope-gravity.js

Springs 3 (Blob):

In this example, particles are placed around the perimeter of a circle. Then springs are used to connect each one to its neighbor. All of particles are also connected by a spring to a single central particle, like spokes of a wheel. I have taken the liberty of decorating this set of particles with a closed Bezier curve that stitches through them all:
springs-wheel.js

In this example, particles are placed around the perimeter of a circle. Then springs are used to connect (some of) them. Specifically, particles are connected to their neighbors, and some of their other nearby neighbors as well. The result is a kind of bouncy bag:
springs-blob.js

Springs 4 (Big Square Mesh):

Here is a classic mesh. Just as in real life, for proper rigidity, we have to connect elements diagonally, or else the structure will not have integrity and will collapse. The code for doing so is (slightly) tricky.
springs-mesh1.js

Springs 5 (Auto-Bouncy Mesh):

We can grab one of the particles and move it automatically. Here one particle is being moved by a combination of sinusoidal and Perlin-noise forces. The rest of the mesh accommodates. (Note that this example is not interactive.)
springs-mesh2

Springs 6 (Narrow Truss):

Here is a narrow truss which imitates a bottom-feeding worm:
springs-truss.js