Programming for Interactive Digital Arts
CS 2, Fall 2023

13. Particle systems

Reusing

Arrays let us have lots of objects doing things simultaneously. But when we create an array, we have to tell Processing the number of objects it could contain. What if we don't know? Sometimes, we can just create way more objects than we need. But if the sketch runs long enough, we might have underestimated. Shiffman 10-9 shows how to expand an array. But what if most of the objects have made their appearance and then left the screen? Then we really don't want or need to have them all still around, and rather than creating a much bigger array than we really need, and then making it bigger and bigger as time goes on, we can simply reuse or recycle existing objects.

One example, inspired by Reas & Fry 43-12, places ripples in a pond, expanding from each mouse press. Each Ring object keeps track of its center (where the mouse was pressed) and its current diameter. It grows until it reaches a fixed diameter; after that point, it also won't be displayed. We draw a ring by looping from the diameter inward, drawing concentric circles. (We also make them more and more transparent as we work inwards.) Note that since the Ring class doesn't have a constructor, the fields all take their default values of 0.

At the beginning, we create an array of rings. A variable "current" keeps track of the next one we'll use. When the mouse is pressed, the next Ring object in the array is started at the mouse position. When we've used up all the objects in the array, we just wrap around and reuse the zeroth one. As long as we have enough rings so that they finish expanding before we have to reuse them, it'll look like we have an infinite supply of them.

[sketch1]
// Variation on Reas & Fry (43-12)

let rings = new Array(50);  // at most 50 at a time
var current = 0;  // for next mouse press

function setup() 
{
  createCanvas(400,400);
  for (let i=0; i<rings.length; i++) {
    rings[i] = new Ring();
  }
}

function draw() 
{
  background(0);
  for (let i=0; i<rings.length; i++) rings[i].display();
  for (let i=0; i<rings.length; i++) rings[i].update();
}

// click to create a new Ring
function mousePressed() 
{
  rings[current].start(mouseX, mouseY);
  current++; // add 1
  if (current == rings.length) current = 0; // wrap around -- reuse rings
}
class Ring {
  x; y;
  diameter;
  on = false;  // whether or not it's being shown

  // Turn ring on, starting to expand from (x0,y0)
  start(x, y) 
  {
    this.x = x; this.y = y;
    this.on = true;
    this.diameter = 1;
  }

  update() 
  {
    if (this.on) {
      this.diameter += 1;
      if (this.diameter > width*2) {  // too big -- stop it
        this.on = false;
      }
    }
  }

  // show ring only if it is on
  display()
  {
    if (this.on) {
      noFill();
      let d = round(this.diameter);
      let a = 255; // auxillary (local) variables a, d
      // More opaque at larger diameter
      while (d > 0 && a > 0) {
        strokeWeight(2);
        stroke(255, a);
        ellipse(this.x, this.y, d, d);
        d -= 20;
        a -= 40;
      }
    }
  }
}

Recycling

The previous example reused objects when necessary, because we'd run out of new ones in the array. Another practice is to recycle them immediately -- once they're off-screen (or in some way not useful), set them back. We can use this, for example, to create an everlasting sparkler, based on an example from Reas and Fry (50-08) and one that comes with Processing (Topics | Simulate | SimpleParticleSystem).

The key idea is to detect when a Particle is no longer visible (off the screen or too transparent), and then re-initialize it back at the center. One other change afunctions having the sparkler start out with a "bang" (like our earlier example when all the balls shoot out of the center together), by randomly turning on particles.

// Based on Processing example Topics | Simulate | SimpleParticleSystem
var particles = new Array(200);

function setup()
{
  createCanvas(400,400);
  // Create the particles
  for (let i=0; i<particles.length; i++) {
    particles[i] = new Particle();
  }
}

function draw()
{
  background(0);
  // Draw and update however many have been allowed to enter
  for (let i=0; i<particles.length; i++) particles[i].draw();
  for (let i=0; i<particles.length; i++) particles[i].update();
}

class Particle {
  x; y;    // current position
  vx; vy;  // velocity
  timer;   // time left before estinguishing
  dt = 3;  // estinguishing speed
  g = 0.05; // gravity
  r = 5;   // radius
  on=false;
  
  constructor() { 
  // Empty
  }

  // Put at original position, fully opaque, with random velocity
  initialize()
  {
    this.on = true;
    this.x = mouseX; this.y = mouseY;
    // More vertical than horizontal
    this.vx = random(-1,1); this.vy = random(-2,-1);
    this.timer = 255;
    this.dt = random(0.1,5);
  }
  
  draw()
  {
    if(!this.on) return;
    fill(64,this.timer); stroke(255,this.timer);
    ellipse(this.x,this.y,2*this.r,2*this.r);
  }
  
  update()
  {
    // initialize if necessary
    if(!this.on) { if(random(0,1) < 0.5) this.initialize(); return; }
    
    this.timer -= this.dt;    // decay the transparency
    // Usual gravity stuff
    this.vy += this.g;
    this.x += this.vx;
    this.y += this.vy;
    // When exit screen or totally transparent, re-initialize
    if (this.timer < 0) {
       this.on = false;
    }
  }
}

Arrays of objects with arrays

The fields of an object can hold anything -- even an array. This lets an object collect a bunch of related objects. For example, we can group similar particles together and manipulate them as one entity (called a particle system). Each particle system has its own particles. In the following sketch, the main array holds ParticleSystems objects, each of which has an array of Particle objects. The ParticleSystem constructor creates all the Particles for that one system. The Particle class is almost identical to one before, but when drawing and updating, it gets some values from the ParticleSystem. As far as interaction goes, this sketch allows you to grab and move the systems themselves, just like they were balls.

The Particles for a Firework are shot out in random angles from the center; we use sine and cosine to convert the angle and an overall speed to velocities in the x and y directions. Particles then move in the usual way, with gravity pulling them to the ground.

let sparkles = new Array(4);

function setup()
{
  createCanvas(400,400);
  smooth();
  
  for(var i = 0; i < sparkles.length; i ++) {
    sparkles[i] = new ParticleSystem(200,
      random(25,width-25),random(25,height-25),
      color(random(128,255),random(128,255),random(128,255)));
  }
}

function draw()
{
  background(0);
  for(let i = 0; i < sparkles.length; i ++) {
    sparkles[i].draw();
    sparkles[i].update();
  }
}
class ParticleSystem {
  particles;        // the individual bits exploding
  c; // common color
  x; // common origin
  y; // common origin
  r = 10; // source radius
  transparency; dt=3;    // transparency decays over time
  grabbed=false; // grabbed or not
  
  constructor(numParticles, x, y, c)
  {
    this.c = c;
    this.x = x;
    this.y = y;
    // Create the particles for the firework
    this.particles = new Array(numParticles);
    for (let i=0; i<this.particles.length; i++) this.particles[i] = new Particle();
  }
  
  draw()
  {
    fill(this.c); stroke(this.c);
    ellipse(this.x,this.y,2*this.r,2*this.r);
    // Draw the individual particles with the shared color
    for (let i=0; i<this.particles.length; i++) this.particles[i].draw(this.c);
  }

  updateGrabbed() 
  {
    // if I am not grabbed yet, grab me if the mouse is clicked on me 
    if(!this.grabbed) this.grabbed = mouseIsPressed && dist(mouseX,mouseY,this.x,this.y) < this.r;
    // otherwise stop grabbing me when I release the mouse
    else this.grabbed = mouseIsPressed;
  }

  update()
  {
    // check if grabbed
    this.updateGrabbed();
    // follow the mouse if grabbed
    if(this.grabbed) { this.x = mouseX; this.y = mouseY; }
    // Update the individual particles
    for (var i=0; i<this.particles.length; i++) this.particles[i].update(this.x,this.y);
  }  
}
class Particle {
  x; y;    // current position
  vx; vy;  // velocity
  timer;   // time left before estinguishing
  dt = 3;  // estinguishing speed
  g = 0.05; // gravity
  r = 5;   // radius
  on=false;
  
  constructor() { }

  // Put at original position, fully opaque, with random velocity
  initialize(x, y)
  {
    this.on = true;
    this.x = x; this.y = y;
    // More vertical than horizontal
    this.vx = random(-1,1); this.vy = random(-2,-1);
    this.timer = 255;
    this.dt = random(0.1,5);
  }
  
  draw(c)
  {
    if(!this.on) return;
    fill(red(c)/2,green(c)/2,blue(c)/2,this.timer); stroke(c,this.timer);
    ellipse(this.x,this.y,2*this.r,2*this.r);
  }
  
  update(x, y)
  {
    // initialize if necessary
    if(!this.on) { if(random(0,1) < 0.5) this.initialize(x,y); return; }
    
    this.timer -= this.dt;    // decay the transparency
    // Usual gravity stuff
    this.vy += this.g;
    this.x += this.vx;
    this.y += this.vy;
    // When exit screen or totally transparent, re-initialize
    if (this.timer < 0) {
      this.on = false;
    }
  }
}