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

var dropX = 30;
var dropY = 30;
var dropR = 50;
var buttonCol = 255;
var dropBool = false;

var master = []; //holds all spring lines
var drawing = false;

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

//-------------------------
function setup() {
  createCanvas(640, 480);

  for (var i = 1; i < 5; i++) {
    var newBox = makeCollider(random(50, width - 50), random(50, height - 50), random(40, 100), 10);
    colliders.push(newBox);
  }
}

//-------------------------
function createParticles(rx, ry, rc) { //make a particle for each point in each line
  var aParticle = new Particle();
  aParticle.set(rx, ry, rc);
  aParticle.bPeriodicBoundaries = false;
  aParticle.bHardBoundaries = true;
  myParticles.push(aParticle);
}

//-------------------------
function createSpringMeshConnectingParticles() {
  // Stitch the particles together into a mesh, 
  // by connecting them to their neighbors with springs.

  // The spring constant. 
  var K = 0.005;

  // stitch the particles together into a blob.
  for (var i = 0; i < master.length; i++) {
    for (var j = 0; j < master[i].length - 1; j++) {

      var p = master[i][j];
      var q = master[i][j + 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

  if (keyIsDown(SHIFT)) {
    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(102);

  fill(255);
  noStroke();
  for (var b = 0; b < colliders.length; b++) {
    // rect(this.x, this.y, this.w, this.h);
    rect(colliders[b].x1, colliders[b].y1, colliders[b].w, colliders[b].h);
  }

  if (!mouseIsPressed) drawing = false;

  buttonStuff();

  for (var a = 0; a < master.length; a++) {
    beginShape();
    noFill();
    // stroke(a * 50);
    strokeWeight(10);
    if (master[a].length > 0) vertex(master[a][0].px, master[a][0].py);
    for (var b = 0; b < master[a].length; b++) {
      var p = master[a][b];
      stroke(p.col);
      //p.render(); // draw all particles
      curveVertex(p.px, p.py);
    }
    //vertex at line's last point
    if (master[a].length > 0) vertex(master[a][master[a].length - 1].px, master[a][master[a].length - 1].py);
    endShape();
  }
}

function buttonStuff() {
  noStroke();
  if (!dropBool) {

    fill(buttonCol);
    ellipse(dropX, dropY, dropR, dropR);
    fill(map(buttonCol, 0, 255, 255, 0));

    dropButtonDist = dist(dropX, dropY, mouseX, mouseY);
    text("DROP", dropX - 17, dropY + 5);
    if (mouseIsPressed && dropButtonDist < dropR) {
      buttonCol = 0;
      dropBool = true;
    }
  } else { //if (dropBool) { 
    drop();
    fill(255);
    text("Pick your spaghetti off the floor!", dropX - 17, dropY);
    text("Hold shift and drag to pick up lines", dropX - 17, dropY + 15);
  }

}

var newColor = [100, 100, 100];

function mouseDragged() {
  if (!keyIsDown(SHIFT)) {
    if (!drawing) {
      master[master.length] = []; //create array to hold new line
      newColor = [random(100, 255), random(100, 255), random(100, 255)];
    }
    drawing = true;
    var tooClose = false;
    for (var i = 0; i < myParticles.length; i++) {
      var p = myParticles[i];
      var d = dist(mouseX, mouseY, p.px, p.py);
      if (d < 10) tooClose = true;
    }
    if (!tooClose) {
      createParticles(mouseX, mouseY, newColor);
      master[master.length - 1].push(myParticles[myParticles.length - 1]);
      createSpringMeshConnectingParticles();
    }
  }
}
//==========================================================

function makeCollider(x, y, w, h) {
  var collider = {
    x1: x,
    y1: y,
    w: w,
    h: h,
    x2: x + w,
    y2: y + h
  };
  return collider;
}
//==========================================================
function drop() {

  if (mouseIsPressed && (whichParticleIsGrabbed > -1) && keyIsDown(SHIFT)) {
    // 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 < myParticles.length; i++) {
    myParticles[i].update(); // update all locations
  }
  var gravityForceX = 0;
  var gravityForceY = 0.3;

  for (var i = 0; i < myParticles.length; i++) {
    myParticles[i].addForce(gravityForceX, gravityForceY);
  }

  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(); // draw all springs
  }

}
//==========================================================
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, c) {
    this.px = x;
    this.py = y;
    this.vx = 0;
    this.vy = 0;
    this.damping = 0.96;
    this.mass = 1.0;
    this.col = c;
  };

  // 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);
      }

      for (var i = 0; i < colliders.length; i++) {
        var box = colliders[i];
        if (this.px >= box.x1 - 10 && this.px <= box.x2 + 10 && this.py <= box.y1 && this.py >= box.y1 - 10) this.vy = abs(this.vy) * -1;
      }

    }
  };

  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() {
    line(p.px, p.py, q.px, q.py);
  };
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.js