A Processing script that simulates colliding particles and strings of connected particles within a boundary.

This sketch uses Box2D, an open-source physics and shape collision library, and is based on The Nature of Code. View the full Box2D for Processing project on GitHub: https://github.com/shiffman/Box2D-for-Processing

MainGUIParticleSurface
import shiffman.box2d.*;
import org.jbox2d.common.*;
import org.jbox2d.dynamics.joints.*;
import org.jbox2d.collision.shapes.*;
import org.jbox2d.collision.shapes.Shape;
import org.jbox2d.common.*;
import org.jbox2d.dynamics.*;
import org.jbox2d.dynamics.contacts.*;

//--------------------------------------------------------------------
//   create lists to add all of our object types to
//--------------------------------------------------------------------

ArrayList<Particle> particles; 
ArrayList<Surface> surfaces; // objects to collide with our particles

//--------------------------------------------------------------------
//   initialize our global variables
//--------------------------------------------------------------------

Box2DProcessing box2d; // the Box2D class
GUI gui; 
boolean keys[]; 
float  r;
int clickCount;
PImage img;
int rate;

//--------------------------------------------------------------------
//   setup
//--------------------------------------------------------------------

void setup() {
  fullScreen();
  background(255);
  smooth();
  frameRate(30);

  box2d = new Box2DProcessing(this); // start Box2D
  box2d.createWorld(); // create a new Box2D world to simulate in

  particles = new ArrayList<Particle>();
  surfaces = new ArrayList<Surface>();

  Surface s = new Surface(); 
  surfaces.add(s); // add the collision surfaces before we start our draw loop
  gui = new GUI(); 

  r=20; 

  rate = 1;

  clickCount=0;

  for (int i=0; i<height; i++) {
    //line
    stroke((height-i)/20+220);
    line(0, i, width, i);
  }
  save("background.jpg");
  img = loadImage("background.jpg");

  keys=new boolean[5];
  keys[0] = false;
  keys[1] = false;
  keys[2] = false;
  keys[3] = false;
  keys[4] = false;
}

//--------------------------------------------------------------------
//   draw
//--------------------------------------------------------------------

void draw() {
  background(255);
  imageMode(CORNER);
  image(img, 0, 0);
  int pCount = particles.size();

  box2d.step(); // every frame we need to step through time

  for (Surface s : surfaces) {
    s.display();
  }
  if (mousePressed && (mouseButton == LEFT)) {
    for (int i=0; i< rate; i++) {
      Particle p = new Particle(mouseX+random(-1, 1), mouseY+random(-1, 1), r, clickCount);
      particles.add(p); // add particles to the scene
    }
  }
  if (mousePressed && (mouseButton == RIGHT)) {
    float rand = random(-1, 1);
    for (int i=0; i< rate; i++) {
      Particle p = new Particle(mouseX+rand, mouseY+i*r, r, clickCount);
      particles.add(p);
      p.connect(pCount+i); // connect particles together
    }
  }

  if (keys[3]) {
    r = r + 2;
  }

  if (keys[4]) {
    r = r - 2;
  }

  if (r<=2) {
    r = 2;
  }
  if (keys[0]) {
    for (Particle p : particles) {
      p.attract(mouseX, mouseY, r); // attract particles to the mouse
    }
  }
  for (int i = pCount-1; i >= 0; i--) {
    Particle p = particles.get(i);
    if (p.isOffCanvas()) {
      particles.remove(i); // remove particles that are off the screen
      pCount = pCount-1;
    }
  }
  if (keys[2]) {
    gui.displayEraser(mouseX, mouseY);
    for (int i = pCount-1; i >= 0; i--) {
      Particle p = particles.get(i);
      if (p.isInRange(mouseX, mouseY, r)) {
        particles.remove(i); // remove particles that are in range of the mouse
      }
    }
  }
  for (Particle p : particles) {
    p.displayShadow();
  }
  for (Particle p : particles) {
    p.display();
  }
  if (keys[1]) {
    gui.expandMenu();
  }
  gui.collapseMenu(pCount, rate);
  gui.displayBrush(mouseX, mouseY, r);
}

//--------------------------------------------------------------------
//   a function that checks which keys are pressed
//--------------------------------------------------------------------

void keyPressed() {
  if (keyCode==CONTROL) {
    keys[0] = true;
  }
  if (keyCode==TAB) {
    keys[1] = true;
  }  
  if (keyCode==ALT) {
    keys[2] = true;
  }
  if (keyCode==UP) {
    keys[3] = true;
  }
  if (keyCode==DOWN) {
    keys[4] = true;
  }
}

//--------------------------------------------------------------------
//   a function that checks which keys are released
//--------------------------------------------------------------------

void keyReleased() {
  if (keyCode==CONTROL) {
    keys[0] = false;
  }
  if (keyCode==TAB) {
    keys[1] = false;
  }  
  if (keyCode==ALT) {
    keys[2] = false;
  }
  if (keyCode==UP) {
    keys[3] = false;
  }
  if (keyCode==DOWN) {
    keys[4] = false;
  }
}

//--------------------------------------------------------------------
//   a function that counts the number of clicks
//--------------------------------------------------------------------

void mouseReleased() {
  clickCount = clickCount+1;
  if (clickCount >= 10) {
    clickCount = 0;
  }
}
class GUI {
  GUI() {
  }

  //--------------------------------------------------------------------
  //   a function that creates a text
  //--------------------------------------------------------------------

  void collapseMenu(int count, int rate) {
    fill(0);
    textSize(20);
    textAlign(LEFT, TOP);
    text("TAB for Menu", 40, 40);

    textAlign(RIGHT, TOP);
    text("Count:"+count, width-40, 40);
  }

  //--------------------------------------------------------------------
  //   a function that displays the full controls
  //--------------------------------------------------------------------

  void expandMenu() {
    rectMode(CENTER);
    noStroke();
    fill(1, 1, 1, 25);
    rect(width/2, height/2, width, height);
    fill(255, 255, 255, 150);
    rect(width/2, height/2, 500, 240);
    textAlign(CENTER, CENTER);
    fill(30);
    textSize(12);
    text("-", width/2, height/2+90);
    textSize(20);
    text("Right-click to add connected particles.", width/2, height/2+60);
    text("Left-click to add connected particles.", width/2, height/2+30);
    text("Press CTRL to attract particles", width/2, height/2);
    text("Press ALT to delete particles", width/2, height/2-30);
    textSize(12);
    text("Controls", width/2, height/2-90);
  }

  //--------------------------------------------------------------------
  //   a function that displays the brush
  //--------------------------------------------------------------------

  void displayBrush(float x, float y, float r) {
    ellipseMode(CENTER);
    noFill();
    strokeWeight(1);
    stroke(150);
    ellipse(x, y, 2*r, 2*r);
    //stroke(255);
    line(x-10, y, x+10, y);
    line(x, y-10, x, y+10);
  }

  //--------------------------------------------------------------------
  //   a function that displays the eraser
  //--------------------------------------------------------------------

  void displayEraser(float x, float y) {
    ellipseMode(CENTER);
    fill(215);
    ellipse(x+10, y+10, 2*r, 2*r);
    fill(255, 15, 200);
    strokeWeight(2);
    stroke(255, 15, 15);
    ellipse(x, y, 2*r, 2*r);
  }
}
class Particle {

  //--------------------------------------------------------------------
  //   initialize our global variables
  //--------------------------------------------------------------------

  Body body;
  float attWt = 10;
  float gravity = 1;
  float offset = 1;
  float connectionMultiplier;
  float r;
  CircleShape cs;
  FixtureDef fd;
  float c;

  //--------------------------------------------------------------------
  //   particle object main
  //--------------------------------------------------------------------

  Particle(float x, float y, float rad, int clickCount) {
    c = (clickCount*20)+55; //assign the color with the click count

    r = rad;
    BodyDef bd = new BodyDef(); // define a body
    bd.position = box2d.coordPixelsToWorld(x, y); // set its position with x,y
    bd.type = BodyType.DYNAMIC; // set its type to dynamic (can also be STATIC or KINEMATIC)
    body = box2d.world.createBody(bd); // create the body

    CircleShape cs = new CircleShape(); // define a circle shape
    cs.m_radius = box2d.scalarPixelsToWorld(r); //set its radius

    FixtureDef fd = new FixtureDef(); // define a fixture
    fd.shape = cs; // associate the shape to the fixture

    fd.density = 1; // phisical properties for fixtures
    fd.friction = 0.5;
    fd.restitution = 0;

    body.createFixture(fd); // attach the shape to the body with a fixture
    body.setLinearVelocity(new Vec2(random(-1, 1), -50)); // set the initial velocity
    body.setGravityScale(gravity); // scale gravity
  }

  //--------------------------------------------------------------------
  //   a function that checks if the body is off the screen
  //--------------------------------------------------------------------

  boolean isOffCanvas() {
    Vec2 pos = box2d.getBodyPixelCoord(body);
    if (pos.y > height+r*2) {
      delete();
      return true;
    }
    if (pos.y < 0-r*2) {
      delete();
      return true;
    }
    if (pos.x > width+r*2) {
      delete();
      return true;
    }
    if (pos.x < 0-r*2) {
      delete();
      return true;
    }
    return false;
  }

  //--------------------------------------------------------------------
  //   a function that tests if a body is in range of a coordinate
  //--------------------------------------------------------------------

  boolean isInRange(float x, float y, float rad) {
    Vec2 pos = box2d.getBodyPixelCoord(body);
    if (dist(pos.x, pos.y, x, y)<rad+r) {
      delete();
      return true;
    }
    return false;
  }

  //--------------------------------------------------------------------
  //   a function that deletes a body
  //--------------------------------------------------------------------

  void delete() {
    box2d.destroyBody(body);
  }

  //--------------------------------------------------------------------
  //   a function that attracts the bodies
  //--------------------------------------------------------------------

  void attract(float x, float y, float rad) {
    Vec2 worldTarget = box2d.coordPixelsToWorld(x, y);   
    Vec2 bodyVec = body.getWorldCenter();

    worldTarget.subLocal(bodyVec);

    worldTarget.normalize();
    worldTarget.mulLocal((float) rad*10);

    body.applyForce(worldTarget, bodyVec);
  }

  //--------------------------------------------------------------------
  //   a function that connects bodies together
  //--------------------------------------------------------------------

  void connect(int pCount) {
    
    connectionMultiplier = 3;
    
    if (pCount>0) {

      Particle current = particles.get(pCount); // get the current particle
      Vec2 currentPos = box2d.getBodyPixelCoord(current.body); // get its location
      Particle previous = particles.get(pCount-1); // get the previous particle
      Vec2 previousPos = box2d.getBodyPixelCoord(previous.body); // get its location

      if (dist(currentPos.x, currentPos.y, previousPos.x, previousPos.y)<r*connectionMultiplier) { // if within range

        DistanceJointDef djd = new DistanceJointDef(); // define a joint
        djd.bodyA = current.body; // set the current particle as the start point
        djd.bodyB = previous.body; // set the previous particle as the end point
        djd.length = box2d.scalarPixelsToWorld(r);
        box2d.world.createJoint(djd); // create a joint
      }
    }
  }

  //--------------------------------------------------------------------
  //   a function that displays all the bodies
  //--------------------------------------------------------------------

  void display() {
    Vec2 pos = box2d.getBodyPixelCoord(body);   
    //draw bodies
    pushMatrix();
    translate(pos.x, pos.y);
    noStroke();
    fill(0, c, c);
    ellipse(0, 0, r*2-offset, r*2-offset);
    popMatrix();
  }
  void displayShadow() {
    Vec2 pos = box2d.getBodyPixelCoord(body);
    pushMatrix();
    translate(pos.x, pos.y);
    rotate(45);
    noStroke();
    fill(0);
    rectMode(CORNER);
    rect(0,-r, 25, r*2);
    ellipse(25,0, r*2-offset, r*2-offset);
    popMatrix();
  }
}
class Surface { 

  //--------------------------------------------------------------------
  //   create a list to add our surface vertices to
  //--------------------------------------------------------------------

  ArrayList<Vec2> surface;

  //--------------------------------------------------------------------
  //   surface collider object main
  //--------------------------------------------------------------------

  Surface() {
    surface = new ArrayList<Vec2>(); //create a list to store our surface verts
    ChainShape chain = new ChainShape(); // This is a box2d surface object

    float cx = width/2;
    float cy = height/2;
    float rad = height/3;
    int seg = 200;

    for (int i = 0; i <= seg; i++) {
      float angle = i * TWO_PI / seg;
      float x = cx + cos(angle) * rad;
      float y = cy + sin(angle) * rad;

      surface.add(new Vec2(x, y)); // Store our surface vertices
    }

    Vec2[] vertices = new Vec2[surface.size()]; 
    for (int i = 0; i < vertices.length; i++) {
      Vec2 edge = box2d.coordPixelsToWorld(surface.get(i)); 
      vertices[i] = edge; // add all our surface verts to a list
    }

    chain.createChain(vertices, vertices.length); // create a chain with the verts

    BodyDef bd = new BodyDef(); 
    bd.position.set(0, 0); // set the position
    Body body = box2d.createBody(bd); //create the body
    body.createFixture(chain, 1); // attach the chain to a fixture
  }

  //--------------------------------------------------------------------
  //   a function that displays all the surfaces
  //--------------------------------------------------------------------

  void display() {
    strokeWeight(0.5); 
    stroke(225); 
    noFill(); 
    beginShape(); 
    for (Vec2 v : surface) { 
      vertex(v.x, v.y);
    }
    endShape();
    ellipseMode(CENTER);
    fill(250);
    noStroke();
    ellipse(width/2, height/2, height/3*2+10, height/3*2+10);
  }
}

Controls
  • ALT – delete particles
  • CTRL – attract particles to mouse location
  • LEFT-CLICK – add particles
  • RIGHT-CLICK – add connected particles
  • UP/DN – change particle size
  • TAB – show menu