Rag Dolls and Tapestries

In this experiment, we will explore Object-Oriented Programming by examining Rag Dolls and Tapestries.

In computer simulations, a rag doll is a stick figure. However, the stick figure is constructed in such a way that the joints can move independently, but the body parts remain connected. In fact, the same idea can be used for any item that we wish to simulate that has joints connecting points of mass. We'll see that rag dolls and tapestries can both be constructed from the same elements.

To allow body parts to both move independently, and also remain connected, and to make curtains, we have to use some mathematics. To make it fast, we use a technique called verlet integration.

This code is based on http://gamedevelopment.tutsplus.com/tutorials/simulate-tearable-cloth-and-ragdolls-with-simple-verlet-integration--gamedev-519

We need to basic classes:

  1. PointMass - a point in 2D space, with some mass
  2. Link - a rigid connection between two PointMasses

Out of these two classes, we can build all kinds of things, including:

  1. Circle
  2. Body (uses Circle for head)
  3. Tapestry - a curtain-like matrix composed of a mesh of PointMasses and Links

This uses the same ideas from the bouncing ball examples, with some minor differences:

  • velocity will be computed based on last_position - current_position
  • we will take mass into account
  • objects can't move freely... they may have constraints due to their links to other PointMasses
In [1]:
// PointMass
class PointMass {
  float lastX, lastY; // for calculating position change (velocity)
  float x,y;
  float accX, accY;
  
  float mass = 1;
  float damping = 20;

  // An ArrayList for links, so we can have as many links as we want to this PointMass
  ArrayList links = new ArrayList();
  
  boolean pinned = false;
  float pinX, pinY;
  
  // PointMass constructor
  PointMass(float xPos, float yPos) {
    x = xPos;
    y = yPos;
    
    lastX = x;
    lastY = y;
    
    accX = 0;
    accY = 0;
  }
  
  // The update function is used to update the physics of the PointMass.
  // motion is applied, and links are drawn here
  void updatePhysics(float timeStep) { // timeStep should be in elapsed seconds (deltaTime)
    this.applyForce(0, mass * gravity);
    
    float velX = x - lastX;
    float velY = y - lastY;
    
    // dampen velocity
    velX *= 0.99;
    velY *= 0.99;

    float timeStepSq = timeStep * timeStep;

    // calculate the next position using Verlet Integration
    float nextX = x + velX + 0.5 * accX * timeStepSq;
    float nextY = y + velY + 0.5 * accY * timeStepSq;
    
    // reset variables
    lastX = x;
    lastY = y;
    
    x = nextX;
    y = nextY;
    
    accX = 0;
    accY = 0;
  }
    
  void updateInteractions() {
    // this is where our interaction comes in.
    if (mousePressed) {
      float distanceSquared = distPointToSegmentSquared(pmouseX,pmouseY,mouseX,mouseY,x,y);
      if (mouseButton == LEFT) {
        if (distanceSquared < mouseInfluenceSize) { // remember mouseInfluenceSize was squared in setup()
          // To change the velocity of our PointMass, we subtract that change from the lastPosition.
          // When the physics gets integrated (see updatePhysics()), the change is calculated
          // Here, the velocity is set equal to the cursor's velocity
          lastX = x - (mouseX-pmouseX)*mouseInfluenceScalar;
          lastY = y - (mouseY-pmouseY)*mouseInfluenceScalar;
        }
      }
      else { // if the right mouse button is clicking, we tear the cloth by removing links
        if (distanceSquared < mouseTearSize) 
          links.clear();
      }
    }
  }

  void draw() {
    // draw the links and points
    stroke(0);
    if (links.size() > 0) {
      for (int i = 0; i < links.size(); i++) {
        Link currentLink = (Link) links.get(i);
        currentLink.draw();
      }
    }
    else
      point(x, y);
  }
    
  /* Constraints */
  void solveConstraints() {
    /* Link Constraints */
    // Links make sure PointMasss connected to this one is at a set distance away
    for (int i = 0; i < links.size(); i++) {
      Link currentLink = (Link) links.get(i);
      currentLink.solve();
    }
    
    /* Boundary Constraints */
    // These if statements keep the PointMasss within the screen
    if (y < 1)
      y = 2 * (1) - y;
    if (y > height-1)
      y = 2 * (height - 1) - y;
      
    if (x > width-1)
      x = 2 * (width - 1) - x;
    if (x < 1)
      x = 2 * (1) - x;
    
    /* Other Constraints */
    // make sure the PointMass stays in its place if it's pinned
    if (pinned) {
      x = pinX;
      y = pinY; 
    }
  }
  
  // attachTo can be used to create links between this PointMass and other PointMasses
  void attachTo(PointMass P, float restingDist, float stiff) {
    attachTo(P, restingDist, stiff, 30, true);
  }
  void attachTo(PointMass P, float restingDist, float stiff, boolean drawLink) {
    attachTo(P, restingDist, stiff, 30, drawLink);
  }
  void attachTo(PointMass P, float restingDist, float stiff, float tearSensitivity) {
    attachTo(P, restingDist, stiff, tearSensitivity, true);
  }
  void attachTo(PointMass P, float restingDist, float stiff, float tearSensitivity, boolean drawLink) {
    Link lnk = new Link(this, P, restingDist, stiff, tearSensitivity, drawLink);
    links.add(lnk);
  }
  void removeLink (Link lnk) {
    links.remove(lnk);
  }  
 
  void applyForce(float fX, float fY) {
    // acceleration = (1/mass) * force
    // or
    // acceleration = force / mass
    accX += fX/mass;
    accY += fY/mass;
  }
  
  void pinTo (float pX, float pY) {
    pinned = true;
    pinX = pX;
    pinY = pY;
  }
} 

// The Link class is used for handling distance constraints between PointMasses.
class Link {
  float restingDistance;
  float stiffness;
  float tearSensitivity;
  
  PointMass p1;
  PointMass p2;
  
  // if you want this link to be invisible, set this to false
  boolean drawThis = true;
  
  Link(PointMass which1, PointMass which2, float restingDist, float stiff, float tearSensitivity, boolean drawMe) {
    p1 = which1; // when you set one object to another, it's pretty much a reference. 
    p2 = which2; // Anything that'll happen to p1 or p2 in here will happen to the paticles in our ArrayList
    
    restingDistance = restingDist;
    stiffness = stiff;
    drawThis = drawMe;
    
    this.tearSensitivity = tearSensitivity;
  }
  
  // Solve the link constraint
  void solve() {
    // calculate the distance between the two PointMasss
    float diffX = p1.x - p2.x;
    float diffY = p1.y - p2.y;
    float d = sqrt(diffX * diffX + diffY * diffY);
    
    // find the difference, or the ratio of how far along the restingDistance the actual distance is.
    float difference = (restingDistance - d) / d;
    
    // if the distance is more than curtainTearSensitivity, the cloth tears
    if (d > tearSensitivity) 
      p1.removeLink(this);
    
    // Inverse the mass quantities
    float im1 = 1 / p1.mass;
    float im2 = 1 / p2.mass;
    float scalarP1 = (im1 / (im1 + im2)) * stiffness;
    float scalarP2 = stiffness - scalarP1;
    
    // Push/pull based on mass
    // heavier objects will be pushed/pulled less than attached light objects
    p1.x += diffX * scalarP1 * difference;
    p1.y += diffY * scalarP1 * difference;
    
    p2.x -= diffX * scalarP2 * difference;
    p2.y -= diffY * scalarP2 * difference;
  }

  // Draw if it's visible
  void draw() {
    if (drawThis)
      line(p1.x, p1.y, p2.x, p2.y);
  }
}

// Used as a head for ragdolls
class Circle {
  float radius;
  PointMass attachedPointMass;
  
  Circle (float r) {
    radius = r;
  }
  
  // Constraints
  void solveConstraints () {
    float x = attachedPointMass.x;
    float y = attachedPointMass.y;
    
    // only do a boundary constraint
    if (y < radius)
      y = 2*(radius) - y;
    if (y > height-radius)
      y = 2 * (height - radius) - y;
    if (x > width-radius)
      x = 2 * (width - radius) - x;
    if (x < radius)
      x = 2*radius - x;
      
    attachedPointMass.x = x;
    attachedPointMass.y = y;
  }
  
  void draw () {
    ellipse(attachedPointMass.x, attachedPointMass.y, radius*2, radius*2);
  }
  
  void attachToPointMass (PointMass p) {
    attachedPointMass = p;
  }
}

// Body
// Here we construct and store a ragdoll
class Body {
  /*
     O
    /|\
   / | \
    / \
   |   |
  */
  PointMass head;
  PointMass shoulder;
  PointMass elbowLeft;
  PointMass elbowRight;
  PointMass handLeft;
  PointMass handRight;
  PointMass pelvis;
  PointMass kneeLeft;
  PointMass kneeRight;
  PointMass footLeft;
  PointMass footRight;
  Circle headCircle;
  
  float headWidth;
  float headLength;
  Body (float x, float y, float bodyHeight) {
    headLength = bodyHeight / 7.5;
    headWidth = headLength * 3/4;

    head = new PointMass(x + random(-5,5),y + random(-5,5));
    head.mass = 4;
    shoulder = new PointMass(x + random(-5,5),y + random(-5,5));
    shoulder.mass = 26; // shoulder to torso
    head.attachTo(shoulder, 5/4 * headLength, 1, bodyHeight*2, true);
    
    elbowLeft = new PointMass(x + random(-5,5),y + random(-5,5));
    elbowRight = new PointMass(x + random(-5,5),y + random(-5,5));
    elbowLeft.mass = 2; // upper arm mass
    elbowRight.mass = 2; 
    elbowLeft.attachTo(shoulder, headLength*3/2, 1, bodyHeight*2, true);
    elbowRight.attachTo(shoulder, headLength*3/2, 1, bodyHeight*2, true);
    
    handLeft = new PointMass(x + random(-5,5),y + random(-5,5));
    handRight = new PointMass(x + random(-5,5),y + random(-5,5));
    handLeft.mass = 2;
    handRight.mass = 2;
    handLeft.attachTo(elbowLeft, headLength*2, 1, bodyHeight*2, true);
    handRight.attachTo(elbowRight, headLength*2, 1, bodyHeight*2, true);
    
    pelvis = new PointMass(x + random(-5,5),y + random(-5,5));
    pelvis.mass = 15; // pelvis to lower torso
    pelvis.attachTo(shoulder,headLength*3.5,0.8,bodyHeight*2, true);
    // this restraint keeps the head from tilting in extremely uncomfortable positions
    pelvis.attachTo(head, headLength*4.75, 0.02, bodyHeight*2, false);
    
    kneeLeft = new PointMass(x + random(-5,5),y + random(-5,5));
    kneeRight = new PointMass(x + random(-5,5),y + random(-5,5));
    kneeLeft.mass = 10;
    kneeRight.mass = 10;
    kneeLeft.attachTo(pelvis, headLength*2, 1, bodyHeight*2, true);
    kneeRight.attachTo(pelvis, headLength*2, 1, bodyHeight*2, true);
    
    footLeft = new PointMass(x + random(-5,5),y + random(-5,5));
    footRight = new PointMass(x + random(-5,5),y + random(-5,5));
    footLeft.mass = 5; // calf + foot
    footRight.mass = 5;
    footLeft.attachTo(kneeLeft, headLength*2, 1, bodyHeight*2, true);
    footRight.attachTo(kneeRight, headLength*2, 1, bodyHeight*2, true);
    
    // these constraints resist flexing the legs too far up towards the body
    footLeft.attachTo(shoulder, headLength*7.5, 0.001, bodyHeight*2, false);
    footRight.attachTo(shoulder, headLength*7.5, 0.001, bodyHeight*2, false);
    
    headCircle = new Circle(headLength*0.75);
    headCircle.attachToPointMass(head);
    
    world.addCircle(headCircle);
    addPointMass(head);
    addPointMass(shoulder);
    addPointMass(pelvis);
    addPointMass(elbowLeft);
    addPointMass(elbowRight);
    addPointMass(handLeft);
    addPointMass(handRight);
    addPointMass(kneeLeft);
    addPointMass(kneeRight);
    addPointMass(footLeft);
    addPointMass(footRight);
  }
    
  void removeFromWorld () {
    world.removeCircle(headCircle);
    removePointMass(head);
    removePointMass(shoulder);
    removePointMass(pelvis);
    removePointMass(elbowLeft);
    removePointMass(elbowRight);
    removePointMass(handLeft);
    removePointMass(handRight);
    removePointMass(kneeLeft);
    removePointMass(kneeRight);
    removePointMass(footLeft);
    removePointMass(footRight);
  }
}

// Timesteps are managed here
class World {
  // list of circle constraints
  ArrayList<Circle> circles = new ArrayList<Circle>();
  
  long previousTime;
  long currentTime;
  
  int fixedDeltaTime;
  float fixedDeltaTimeSeconds;
  
  int leftOverDeltaTime;
  
  int constraintAccuracy;
  
  World() {
    fixedDeltaTime = 16;
    fixedDeltaTimeSeconds = (float)fixedDeltaTime / 1000.0;
    leftOverDeltaTime = 0;
    constraintAccuracy = 3;
  }
  
  // Update physics
  void update() {
    // calculate elapsed time
    currentTime = millis();
    long deltaTimeMS = currentTime - previousTime;
    
    previousTime = currentTime; // reset previous time
    
    // break up the elapsed time into manageable chunks
    int timeStepAmt = (int)((float)(deltaTimeMS + leftOverDeltaTime) / (float)fixedDeltaTime);
    
    // limit the timeStepAmt to prevent potential freezing
    timeStepAmt = min(timeStepAmt, 5);
    
    // store however much time is leftover for the next frame
    leftOverDeltaTime = (int)deltaTimeMS - (timeStepAmt * fixedDeltaTime);
  
    // How much to push PointMasses when the user is interacting
    mouseInfluenceScalar = 1.0 / timeStepAmt;
    
    // update physics
    for (int iteration = 1; iteration <= timeStepAmt; iteration++) {
      // solve the constraints multiple times
      // the more it's solved, the more accurate.
      for (int x = 0; x < constraintAccuracy; x++) {
        for (int i = 0; i < pointmasses.size(); i++) {
          PointMass pointmass = (PointMass) pointmasses.get(i);
          pointmass.solveConstraints();
        }
        for (int i = 0; i < circles.size(); i++) {
          Circle c = (Circle) circles.get(i);
          c.solveConstraints();  
        }
      }
      
      // update each PointMass's position
      for (int i = 0; i < pointmasses.size(); i++) {
        PointMass pointmass = (PointMass) pointmasses.get(i);
        pointmass.updateInteractions();
        pointmass.updatePhysics(fixedDeltaTimeSeconds);
      }
    }
  }
  
  void addCircle (Circle c) {
    circles.add(c);  
  }
  void removeCircle (Circle c) {
    circles.remove(c);  
  }
}


// Where we'll store all of the points
ArrayList<PointMass> pointmasses;

// every PointMass within this many pixels will be influenced by the cursor
float mouseInfluenceSize = 20; 
// minimum distance for tearing when user is right clicking
float mouseTearSize = 8;
float mouseInfluenceScalar = 5;

// amount to accelerate everything downward
float gravity = 980; 

// Dimensions for our curtain. These are number of PointMasss for each direction, not actual widths and heights
// the true width and height can be calculated by multiplying restingDistances by the curtain dimensions
final int curtainHeight = 40;
final int curtainWidth = 60;
final int yStart = 25; // where will the curtain start on the y axis?
final float restingDistances = 6;
final float stiffnesses = 1;
final float curtainTearSensitivity = 50; // distance the PointMasss have to go before ripping

World world;

void setup() {
  size(500,250);
  world = new World();
  mouseInfluenceSize = 20; 
  // minimum distance for tearing when user is right clicking
  mouseTearSize = 8;
  mouseInfluenceScalar = 5;
  // we square the mouseInfluenceSize and mouseTearSize so we don't have to use squareRoot when comparing distances with this.
  mouseInfluenceSize *= mouseInfluenceSize; 
  mouseTearSize *= mouseTearSize;
  reset();
}

void draw() {
  background(255);  
  world.update();
  updateGraphics();
}

void updateGraphics() {
  for (PointMass p : pointmasses) {
    p.draw();
  }
  for (Circle c : world.circles) {
    c.draw(); 
  }
}

void addPointMass(PointMass p) {
  pointmasses.add(p); 
}

void removePointMass(PointMass p) {
  pointmasses.remove(p);  
}

void createCurtain() {
  // midWidth: amount to translate the curtain along x-axis for it to be centered
  // (curtainWidth * restingDistances) = curtain's pixel width
  int midWidth = (int) (width/2 - (curtainWidth * restingDistances)/2);
  // Since this our fabric is basically a grid of points, we have two loops
  for (int y = 0; y <= curtainHeight; y++) { // due to the way PointMasss are attached, we need the y loop on the outside
    for (int x = 0; x <= curtainWidth; x++) { 
      PointMass pointmass = new PointMass(midWidth + x * restingDistances, y * restingDistances + yStart);
      
      // attach to 
      // x - 1  and
      // y - 1  
      //  *<---*<---*<-..
      //  ^    ^    ^
      //  |    |    |
      //  *<---*<---*<-..
      //
      // PointMass attachTo parameters: PointMass PointMass, float restingDistance, float stiffness
      // try disabling the next 2 lines (the if statement and attachTo part) to create a hairy effect
      if (x != 0) 
        pointmass.attachTo((PointMass)(pointmasses.get(pointmasses.size()-1)), restingDistances, stiffnesses);
      // the index for the PointMasss are one dimensions, 
      // so we convert x,y coordinates to 1 dimension using the formula y*width+x  
      if (y != 0)
        pointmass.attachTo((PointMass)(pointmasses.get((y - 1) * (curtainWidth+1) + x)), restingDistances, stiffnesses);
      // we pin the very top PointMasss to where they are
      if (y == 0)
        pointmass.pinTo(pointmass.x, pointmass.y);
      // add to PointMass array  
      pointmasses.add(pointmass);
    }
  }
}

void createBodies(int count) {
  for (int i = 0; i < count; i++) {
    new Body(random(width), random(height), 80);
  }  
}

// Controls. The r key resets the curtain, g toggles gravity
void keyPressed() {
  if ((key == 'r') || (key == 'R')) {
      reset();
  } 
  if ((key == 'g') || (key == 'G'))
    toggleGravity();
}

void toggleGravity() {
  if (gravity != 0)
    gravity = 0;
  else
    gravity = 980;
}

// Using http://www.codeguru.com/forum/showpost.php?p=1913101&postcount=16
// We use this to have consistent interaction
// so if the cursor is moving fast, it won't interact only in spots where the applet registers it at
float distPointToSegmentSquared(float lineX1, float lineY1, float lineX2, float lineY2, float pointX, float pointY) {
  float vx = lineX1 - pointX;
  float vy = lineY1 - pointY;
  float ux = lineX2 - lineX1;
  float uy = lineY2 - lineY1;
  
  float len = ux*ux + uy*uy;
  float det = (-vx * ux) + (-vy * uy);
  if ((det < 0) || (det > len)) {
    ux = lineX2 - pointX;
    uy = lineY2 - pointY;
    return min(vx*vx+vy*vy, ux*ux+uy*uy);
  }
  
  det = ux*vy - uy*vx;
  return (det*det) / len;
}

void reset() {
    pointmasses = new ArrayList<PointMass>();
    world.circles = new ArrayList<Circle>();
    //createCurtain();
    createBodies(20);
}
Sketch #1:

Sketch #1 state: Loading...

In the downloadable version of Processing, you can put each class in its own tab.

In [2]:
// PointMass
class PointMass {
  float lastX, lastY; // for calculating position change (velocity)
  float x,y;
  float accX, accY;
  
  float mass = 1;
  float damping = 20;

  // An ArrayList for links, so we can have as many links as we want to this PointMass
  ArrayList links = new ArrayList();
  
  boolean pinned = false;
  float pinX, pinY;
  
  // PointMass constructor
  PointMass(float xPos, float yPos) {
    x = xPos;
    y = yPos;
    
    lastX = x;
    lastY = y;
    
    accX = 0;
    accY = 0;
  }
  
  // The update function is used to update the physics of the PointMass.
  // motion is applied, and links are drawn here
  void updatePhysics(float timeStep) { // timeStep should be in elapsed seconds (deltaTime)
    this.applyForce(0, mass * gravity);
    
    float velX = x - lastX;
    float velY = y - lastY;
    
    // dampen velocity
    velX *= 0.99;
    velY *= 0.99;

    float timeStepSq = timeStep * timeStep;

    // calculate the next position using Verlet Integration
    float nextX = x + velX + 0.5 * accX * timeStepSq;
    float nextY = y + velY + 0.5 * accY * timeStepSq;
    
    // reset variables
    lastX = x;
    lastY = y;
    
    x = nextX;
    y = nextY;
    
    accX = 0;
    accY = 0;
  }
    
  void updateInteractions() {
    // this is where our interaction comes in.
    if (mousePressed) {
      float distanceSquared = distPointToSegmentSquared(pmouseX,pmouseY,mouseX,mouseY,x,y);
      if (mouseButton == LEFT) {
        if (distanceSquared < mouseInfluenceSize) { // remember mouseInfluenceSize was squared in setup()
          // To change the velocity of our PointMass, we subtract that change from the lastPosition.
          // When the physics gets integrated (see updatePhysics()), the change is calculated
          // Here, the velocity is set equal to the cursor's velocity
          lastX = x - (mouseX-pmouseX)*mouseInfluenceScalar;
          lastY = y - (mouseY-pmouseY)*mouseInfluenceScalar;
        }
      }
      else { // if the right mouse button is clicking, we tear the cloth by removing links
        if (distanceSquared < mouseTearSize) 
          links.clear();
      }
    }
  }

  void draw() {
    // draw the links and points
    stroke(0);
    if (links.size() > 0) {
      for (int i = 0; i < links.size(); i++) {
        Link currentLink = (Link) links.get(i);
        currentLink.draw();
      }
    }
    else
      point(x, y);
  }
    
  /* Constraints */
  void solveConstraints() {
    /* Link Constraints */
    // Links make sure PointMasss connected to this one is at a set distance away
    for (int i = 0; i < links.size(); i++) {
      Link currentLink = (Link) links.get(i);
      currentLink.solve();
    }
    
    /* Boundary Constraints */
    // These if statements keep the PointMasss within the screen
    if (y < 1)
      y = 2 * (1) - y;
    if (y > height-1)
      y = 2 * (height - 1) - y;
      
    if (x > width-1)
      x = 2 * (width - 1) - x;
    if (x < 1)
      x = 2 * (1) - x;
    
    /* Other Constraints */
    // make sure the PointMass stays in its place if it's pinned
    if (pinned) {
      x = pinX;
      y = pinY; 
    }
  }
  
  // attachTo can be used to create links between this PointMass and other PointMasses
  void attachTo(PointMass P, float restingDist, float stiff) {
    attachTo(P, restingDist, stiff, 30, true);
  }
  void attachTo(PointMass P, float restingDist, float stiff, boolean drawLink) {
    attachTo(P, restingDist, stiff, 30, drawLink);
  }
  void attachTo(PointMass P, float restingDist, float stiff, float tearSensitivity) {
    attachTo(P, restingDist, stiff, tearSensitivity, true);
  }
  void attachTo(PointMass P, float restingDist, float stiff, float tearSensitivity, boolean drawLink) {
    Link lnk = new Link(this, P, restingDist, stiff, tearSensitivity, drawLink);
    links.add(lnk);
  }
  void removeLink (Link lnk) {
    links.remove(lnk);
  }  
 
  void applyForce(float fX, float fY) {
    // acceleration = (1/mass) * force
    // or
    // acceleration = force / mass
    accX += fX/mass;
    accY += fY/mass;
  }
  
  void pinTo (float pX, float pY) {
    pinned = true;
    pinX = pX;
    pinY = pY;
  }
} 

// The Link class is used for handling distance constraints between PointMasses.
class Link {
  float restingDistance;
  float stiffness;
  float tearSensitivity;
  
  PointMass p1;
  PointMass p2;
  
  // if you want this link to be invisible, set this to false
  boolean drawThis = true;
  
  Link(PointMass which1, PointMass which2, float restingDist, float stiff, float tearSensitivity, boolean drawMe) {
    p1 = which1; // when you set one object to another, it's pretty much a reference. 
    p2 = which2; // Anything that'll happen to p1 or p2 in here will happen to the paticles in our ArrayList
    
    restingDistance = restingDist;
    stiffness = stiff;
    drawThis = drawMe;
    
    this.tearSensitivity = tearSensitivity;
  }
  
  // Solve the link constraint
  void solve() {
    // calculate the distance between the two PointMasss
    float diffX = p1.x - p2.x;
    float diffY = p1.y - p2.y;
    float d = sqrt(diffX * diffX + diffY * diffY);
    
    // find the difference, or the ratio of how far along the restingDistance the actual distance is.
    float difference = (restingDistance - d) / d;
    
    // if the distance is more than curtainTearSensitivity, the cloth tears
    if (d > tearSensitivity) 
      p1.removeLink(this);
    
    // Inverse the mass quantities
    float im1 = 1 / p1.mass;
    float im2 = 1 / p2.mass;
    float scalarP1 = (im1 / (im1 + im2)) * stiffness;
    float scalarP2 = stiffness - scalarP1;
    
    // Push/pull based on mass
    // heavier objects will be pushed/pulled less than attached light objects
    p1.x += diffX * scalarP1 * difference;
    p1.y += diffY * scalarP1 * difference;
    
    p2.x -= diffX * scalarP2 * difference;
    p2.y -= diffY * scalarP2 * difference;
  }

  // Draw if it's visible
  void draw() {
    if (drawThis)
      line(p1.x, p1.y, p2.x, p2.y);
  }
}

// Used as a head for ragdolls
class Circle {
  float radius;
  PointMass attachedPointMass;
  
  Circle (float r) {
    radius = r;
  }
  
  // Constraints
  void solveConstraints () {
    float x = attachedPointMass.x;
    float y = attachedPointMass.y;
    
    // only do a boundary constraint
    if (y < radius)
      y = 2*(radius) - y;
    if (y > height-radius)
      y = 2 * (height - radius) - y;
    if (x > width-radius)
      x = 2 * (width - radius) - x;
    if (x < radius)
      x = 2*radius - x;
      
    attachedPointMass.x = x;
    attachedPointMass.y = y;
  }
  
  void draw () {
    ellipse(attachedPointMass.x, attachedPointMass.y, radius*2, radius*2);
  }
  
  void attachToPointMass (PointMass p) {
    attachedPointMass = p;
  }
}

// Body
// Here we construct and store a ragdoll
class Body {
  /*
     O
    /|\
   / | \
    / \
   |   |
  */
  PointMass head;
  PointMass shoulder;
  PointMass elbowLeft;
  PointMass elbowRight;
  PointMass handLeft;
  PointMass handRight;
  PointMass pelvis;
  PointMass kneeLeft;
  PointMass kneeRight;
  PointMass footLeft;
  PointMass footRight;
  Circle headCircle;
  
  float headWidth;
  float headLength;
  Body (float x, float y, float bodyHeight) {
    headLength = bodyHeight / 7.5;
    headWidth = headLength * 3/4;

    head = new PointMass(x + random(-5,5),y + random(-5,5));
    head.mass = 4;
    shoulder = new PointMass(x + random(-5,5),y + random(-5,5));
    shoulder.mass = 26; // shoulder to torso
    head.attachTo(shoulder, 5/4 * headLength, 1, bodyHeight*2, true);
    
    elbowLeft = new PointMass(x + random(-5,5),y + random(-5,5));
    elbowRight = new PointMass(x + random(-5,5),y + random(-5,5));
    elbowLeft.mass = 2; // upper arm mass
    elbowRight.mass = 2; 
    elbowLeft.attachTo(shoulder, headLength*3/2, 1, bodyHeight*2, true);
    elbowRight.attachTo(shoulder, headLength*3/2, 1, bodyHeight*2, true);
    
    handLeft = new PointMass(x + random(-5,5),y + random(-5,5));
    handRight = new PointMass(x + random(-5,5),y + random(-5,5));
    handLeft.mass = 2;
    handRight.mass = 2;
    handLeft.attachTo(elbowLeft, headLength*2, 1, bodyHeight*2, true);
    handRight.attachTo(elbowRight, headLength*2, 1, bodyHeight*2, true);
    
    pelvis = new PointMass(x + random(-5,5),y + random(-5,5));
    pelvis.mass = 15; // pelvis to lower torso
    pelvis.attachTo(shoulder,headLength*3.5,0.8,bodyHeight*2, true);
    // this restraint keeps the head from tilting in extremely uncomfortable positions
    pelvis.attachTo(head, headLength*4.75, 0.02, bodyHeight*2, false);
    
    kneeLeft = new PointMass(x + random(-5,5),y + random(-5,5));
    kneeRight = new PointMass(x + random(-5,5),y + random(-5,5));
    kneeLeft.mass = 10;
    kneeRight.mass = 10;
    kneeLeft.attachTo(pelvis, headLength*2, 1, bodyHeight*2, true);
    kneeRight.attachTo(pelvis, headLength*2, 1, bodyHeight*2, true);
    
    footLeft = new PointMass(x + random(-5,5),y + random(-5,5));
    footRight = new PointMass(x + random(-5,5),y + random(-5,5));
    footLeft.mass = 5; // calf + foot
    footRight.mass = 5;
    footLeft.attachTo(kneeLeft, headLength*2, 1, bodyHeight*2, true);
    footRight.attachTo(kneeRight, headLength*2, 1, bodyHeight*2, true);
    
    // these constraints resist flexing the legs too far up towards the body
    footLeft.attachTo(shoulder, headLength*7.5, 0.001, bodyHeight*2, false);
    footRight.attachTo(shoulder, headLength*7.5, 0.001, bodyHeight*2, false);
    
    headCircle = new Circle(headLength*0.75);
    headCircle.attachToPointMass(head);
    
    world.addCircle(headCircle);
    addPointMass(head);
    addPointMass(shoulder);
    addPointMass(pelvis);
    addPointMass(elbowLeft);
    addPointMass(elbowRight);
    addPointMass(handLeft);
    addPointMass(handRight);
    addPointMass(kneeLeft);
    addPointMass(kneeRight);
    addPointMass(footLeft);
    addPointMass(footRight);
  }
    
  void removeFromWorld () {
    world.removeCircle(headCircle);
    removePointMass(head);
    removePointMass(shoulder);
    removePointMass(pelvis);
    removePointMass(elbowLeft);
    removePointMass(elbowRight);
    removePointMass(handLeft);
    removePointMass(handRight);
    removePointMass(kneeLeft);
    removePointMass(kneeRight);
    removePointMass(footLeft);
    removePointMass(footRight);
  }
}

// Timesteps are managed here
class World {
  // list of circle constraints
  ArrayList<Circle> circles = new ArrayList<Circle>();
  
  long previousTime;
  long currentTime;
  
  int fixedDeltaTime;
  float fixedDeltaTimeSeconds;
  
  int leftOverDeltaTime;
  
  int constraintAccuracy;
  
  World() {
    fixedDeltaTime = 16;
    fixedDeltaTimeSeconds = (float)fixedDeltaTime / 1000.0;
    leftOverDeltaTime = 0;
    constraintAccuracy = 3;
  }
  
  // Update physics
  void update() {
    // calculate elapsed time
    currentTime = millis();
    long deltaTimeMS = currentTime - previousTime;
    
    previousTime = currentTime; // reset previous time
    
    // break up the elapsed time into manageable chunks
    int timeStepAmt = (int)((float)(deltaTimeMS + leftOverDeltaTime) / (float)fixedDeltaTime);
    
    // limit the timeStepAmt to prevent potential freezing
    timeStepAmt = min(timeStepAmt, 5);
    
    // store however much time is leftover for the next frame
    leftOverDeltaTime = (int)deltaTimeMS - (timeStepAmt * fixedDeltaTime);
  
    // How much to push PointMasses when the user is interacting
    mouseInfluenceScalar = 1.0 / timeStepAmt;
    
    // update physics
    for (int iteration = 1; iteration <= timeStepAmt; iteration++) {
      // solve the constraints multiple times
      // the more it's solved, the more accurate.
      for (int x = 0; x < constraintAccuracy; x++) {
        for (int i = 0; i < pointmasses.size(); i++) {
          PointMass pointmass = (PointMass) pointmasses.get(i);
          pointmass.solveConstraints();
        }
        for (int i = 0; i < circles.size(); i++) {
          Circle c = (Circle) circles.get(i);
          c.solveConstraints();  
        }
      }
      
      // update each PointMass's position
      for (int i = 0; i < pointmasses.size(); i++) {
        PointMass pointmass = (PointMass) pointmasses.get(i);
        pointmass.updateInteractions();
        pointmass.updatePhysics(fixedDeltaTimeSeconds);
      }
    }
  }
  
  void addCircle (Circle c) {
    circles.add(c);  
  }
  void removeCircle (Circle c) {
    circles.remove(c);  
  }
}


// Where we'll store all of the points
ArrayList<PointMass> pointmasses;

// every PointMass within this many pixels will be influenced by the cursor
float mouseInfluenceSize = 20; 
// minimum distance for tearing when user is right clicking
float mouseTearSize = 8;
float mouseInfluenceScalar = 5;

// amount to accelerate everything downward
float gravity = 980; 

// Dimensions for our curtain. These are number of PointMasss for each direction, not actual widths and heights
// the true width and height can be calculated by multiplying restingDistances by the curtain dimensions
final int curtainHeight = 40;
final int curtainWidth = 60;
final int yStart = 25; // where will the curtain start on the y axis?
final float restingDistances = 6;
final float stiffnesses = 1;
final float curtainTearSensitivity = 50; // distance the PointMasss have to go before ripping

World world;

void setup() {
  size(640,480);
  world = new World();
  mouseInfluenceSize = 20; 
  // minimum distance for tearing when user is right clicking
  mouseTearSize = 8;
  mouseInfluenceScalar = 5;
  // we square the mouseInfluenceSize and mouseTearSize so we don't have to use squareRoot when comparing distances with this.
  mouseInfluenceSize *= mouseInfluenceSize; 
  mouseTearSize *= mouseTearSize;
  reset();
}

void draw() {
  background(255);  
  world.update();
  updateGraphics();
}

void updateGraphics() {
  for (PointMass p : pointmasses) {
    p.draw();
  }
  for (Circle c : world.circles) {
    c.draw(); 
  }
}

void addPointMass(PointMass p) {
  pointmasses.add(p); 
}

void removePointMass(PointMass p) {
  pointmasses.remove(p);  
}

void createCurtain() {
  // midWidth: amount to translate the curtain along x-axis for it to be centered
  // (curtainWidth * restingDistances) = curtain's pixel width
  int midWidth = (int) (width/2 - (curtainWidth * restingDistances)/2);
  // Since this our fabric is basically a grid of points, we have two loops
  for (int y = 0; y <= curtainHeight; y++) { // due to the way PointMasss are attached, we need the y loop on the outside
    for (int x = 0; x <= curtainWidth; x++) { 
      PointMass pointmass = new PointMass(midWidth + x * restingDistances, y * restingDistances + yStart);
      
      // attach to 
      // x - 1  and
      // y - 1  
      //  *<---*<---*<-..
      //  ^    ^    ^
      //  |    |    |
      //  *<---*<---*<-..
      //
      // PointMass attachTo parameters: PointMass PointMass, float restingDistance, float stiffness
      // try disabling the next 2 lines (the if statement and attachTo part) to create a hairy effect
      if (x != 0) 
        pointmass.attachTo((PointMass)(pointmasses.get(pointmasses.size()-1)), restingDistances, stiffnesses);
      // the index for the PointMasss are one dimensions, 
      // so we convert x,y coordinates to 1 dimension using the formula y*width+x  
      if (y != 0)
        pointmass.attachTo((PointMass)(pointmasses.get((y - 1) * (curtainWidth+1) + x)), restingDistances, stiffnesses);
      // we pin the very top PointMasss to where they are
      if (y == 0)
        pointmass.pinTo(pointmass.x, pointmass.y);
      // add to PointMass array  
      pointmasses.add(pointmass);
    }
  }
}

void createBodies(int count) {
  for (int i = 0; i < count; i++) {
    new Body(random(width), random(height), 80);
  }  
}

// Controls. The r key resets the curtain, g toggles gravity
void keyPressed() {
  if ((key == 'r') || (key == 'R')) {
      reset();
  } 
  if ((key == 'g') || (key == 'G'))
    toggleGravity();
}

void toggleGravity() {
  if (gravity != 0)
    gravity = 0;
  else
    gravity = 980;
}

// Using http://www.codeguru.com/forum/showpost.php?p=1913101&postcount=16
// We use this to have consistent interaction
// so if the cursor is moving fast, it won't interact only in spots where the applet registers it at
float distPointToSegmentSquared(float lineX1, float lineY1, float lineX2, float lineY2, float pointX, float pointY) {
  float vx = lineX1 - pointX;
  float vy = lineY1 - pointY;
  float ux = lineX2 - lineX1;
  float uy = lineY2 - lineY1;
  
  float len = ux*ux + uy*uy;
  float det = (-vx * ux) + (-vy * uy);
  if ((det < 0) || (det > len)) {
    ux = lineX2 - pointX;
    uy = lineY2 - pointY;
    return min(vx*vx+vy*vy, ux*ux+uy*uy);
  }
  
  det = ux*vy - uy*vx;
  return (det*det) / len;
}

void reset() {
    pointmasses = new ArrayList<PointMass>();
    world.circles = new ArrayList<Circle>();
    createCurtain();
    //createBodies(20);
}
Sketch #2:

Sketch #2 state: Loading...