November 2

Particle Systems

These examples start with simple linear motion and incrementally become more complex to consider forces, acceleration, friction, and attraction.

Constant Motion

In constant motion, position changes gradually. We accomplish this by updating position variables. The formula is:
x = x + vx;
y = y + vy;

Here, vx,vy represents the velocity, which combines speed and direction.

In these examples, we use a Particle object that defines a number of methods. Basically, the idea is to use the following methods:

  • set(x, y) – set the initial location.
  • addForce(fx, fy) – inside draw call this for each force acting on the object.
  • update() – after adding forces, call this to update the object location.
  • render() – after update(), call this in draw to draw the object.

Each mouse click adds a particle.
particle

// Particle -- simple starting point for particle examples

var myParticles = [];
var nParticles = 0; 
 
//==========================================================
function setup() {
    createCanvas(400, 400);
}
 
function mousePressed() {
    var p = new Particle();
    p.set(width / 2, height / 10);
    // initialize each particle with random velocity:
    p.vx = random(-1, 1);
    p.vy = random(0.01, 1);
    p.damping = 1.0;

    // add p to set of particles
    myParticles[nParticles] = p;
    nParticles++;
}

function keyPressed() {
}

 
//==========================================================
function draw() {
    background(200);
 
    for (var i = 0; i < myParticles.length; i++) {
        var p = myParticles[i]; // get the particle to work on
        p.update(); // update all locations
        p.render(); // draw all particles
    }
}
 
 
//==========================================================
function Particle() {
    this.px = 0;
    this.py = 0;
    this.vx = 0;
    this.vy = 0;
    this.damping = 0.96;
    this.mass = 1.0;
    this.bLimitVelocities = true;
    this.bPeriodicBoundaries = false;
    this.bElasticBoundaries = false;
 
    // Constructor 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() {
        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.inBounds = function() {
        return (this.px < width && this.px > 0 &&
                this.py < height && this.py > 0);
    }        

    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.bElasticBoundaries) {
            if (this.px > width ) this.vx = -this.vx;
            if (this.px < 0         ) this.vx = -this.vx;
            if (this.py > height) this.vy = -this.vy;
            if (this.py < 0         ) this.vy = -this.vy;
        }
    }
 
    this.render = function(){
        fill(0); 
        ellipse(this.px, this.py, 9, 9);
    }
}

Force

Just as objects in the real world do not instantly change direction, we do not want to instantly change vx, vy. Instead, we use the same trick of incrementing vx, vy by small amounts in each call to draw:
vx = vx + ax;
vy = vy + ay;

Here, ax, ay is the acceleration, or the change in velocity at each time step.

In the real world, acceleration is caused by force being applied to the object. The amount of acceleration is inversely proportional to mass: acceleration = force / mass, which should make sense intuitively; the bigger the object, the smaller effect when you push it.

Here is the previous example with force added to each particle. Click to create a particle. Move the mouse to change the force. The direction and magnitude of force is shown by the line segment. To focus on the code changes, I have “compressed” the object definition, which is identical to the code in the first example above.

force

// Force

var myParticles = [];
var nParticles = 0; 
 
//==========================================================
function setup() {
    createCanvas(400, 400);
}
 
// moved particle initialization here to make it easy to
// re-initialize a particle when it goes off-canvas.
//
function initializeParticle(p) {
    p.set(width / 2, height / 10);
    p.vx = random(-1, 1);
    p.vy = random(0.01, 1);
    p.damping = 1.0;
}

function mousePressed() {
    var p = new Particle();
    initializeParticle(p);

    // add p to set of particles
    myParticles[nParticles] = p;
    nParticles++;
}

function keyPressed() {
}

 
//==========================================================
function draw() {
    background(200);
    var fx = mouseX - width/2;
    var fy = mouseY - height/2;
    for (var i = 0; i < myParticles.length; i++) {
        var p = myParticles[i];
        p.addForce(fx / 1000, fy / 1000);
        p.update(); // update paricle location
        p.render(); // draw all particles
        if (!(p.inBounds())) { // particle off-canvas
            initializeParticle(p); // put it back
        }
    }        
    // draw a line indicating the force applied to each particle
    line(width/2, height/10, width/2 + fx, height/10 + fy);
}
 
 
function Particle(){this.px=0;this.py=0;this.vx=0;this.vy=0;this.damping=0.96;this.mass=1.0;this.bLimitVelocities=true;this.bPeriodicBoundaries=false;this.bElasticBoundaries=false;
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;}
this.addForce=function(fx,fy){var ax=fx/this.mass;var ay=fy/this.mass;this.vx+=ax;this.vy+=ay;}
this.update=function(){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.inBounds=function(){return(this.px<width&&this.px>0&&this.py<height&&this.py>0);}
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.bElasticBoundaries){if(this.px>width)this.vx=-this.vx;if(this.px<0)this.vx=-this.vx;if(this.py>height)this.vy=-this.vy;if(this.py<0)this.vy=-this.vy;}}
this.render=function(){fill(0);ellipse(this.px,this.py,9,9);}}

Friction and Drag

Why can’t you keep going faster when you pedal a bicycle? You would think friction from the wheels is slowing you down, but it’s actually mostly wind resistance. Wind resistance increases with the square of velocity. At highway speeds, in spite of the weight and big tires, most of a car’s power is pushing aside invisible molecules of air!

A simple model of friction or drag is already built into the Particle object. The damping property is automatically used to scale the velocity at each update. The code is:
this.vx *= this.damping;
this.vy *= this.damping;

Notice that this decreases the speed (in any direction) when damping is between one and zero. By setting damping to a near-one value (the default is 0.96), high speeds are quickly reduced by repeated scaling by damping unless new force is also constantly applied to increase the speed.

Summary: frictional force that is proportional to, and in opposition to velocity is a simple multiplication by damping.

Now, let’s model friction (drag) that opposes motion and increases with the square of velocity. Here is some code. Notice the addition of force (addForce) to each particle. Also notice how the code computes the x and y components of drag separately.

friction

// Friction

// How should we model friction? A reasonable and simple model is that
// frictional force is a constant force in opposition to the direction
// of motion.
// Drag such as wind resistance increases with the SQUARE of the 
// velocity.
// We'll model drag in this example.

var myParticles = [];
var nParticles = 0; 
var dragFactor = 0.01;
 
//==========================================================
function setup() {
    createCanvas(400, 400);
}
 
function initializeParticle(p) {
    p.set(width / 2, height / 10);
    p.vx = random(-1, 1);
    p.vy = random(0.01, 1);
    p.damping = 1.0;
}

function mousePressed() {
    var p = new Particle();
    initializeParticle(p);

    // add p to set of particles
    myParticles[nParticles] = p;
    nParticles++;
}

function keyPressed() {
}

 
//==========================================================
function draw() {
    background(200);
    var fx = mouseX - width/2;
    var fy = mouseY - height/2;
 
    for (var i = 0; i < myParticles.length; i++) {
        var p = myParticles[i];
        p.addForce(fx / 1000, fy / 1000);
        // add drag proportional to square of velocity
        var dragX = (p.vx * p.vx) * dragFactor;
        // p.vx * p.cx is always positive. See if the direction of
        // drag should be negative:
        if (p.vx > 0) dragX = -dragX;
        var dragY = (p.vy * p.vy) * dragFactor;
        if (p.vy > 0) dragY = -dragY;
        p.addForce(dragX, dragY);
        p.update(); // update all locations
        p.render(); // draw all particles
        if (!p.inBounds()) {
            initializeParticle(p);
        }
    }        
    line(width/2, height/10, width/2 + fx, height/10 + fy);
}
 
 
function Particle(){this.px=0;this.py=0;this.vx=0;this.vy=0;this.damping=0.96;this.mass=1.0;this.bLimitVelocities=true;this.bPeriodicBoundaries=false;this.bElasticBoundaries=false;
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;}
this.addForce=function(fx,fy){var ax=fx/this.mass;var ay=fy/this.mass;this.vx+=ax;this.vy+=ay;}
this.update=function(){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.inBounds=function(){return(this.px<width&&this.px>0&&this.py<height&&this.py>0);}
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.bElasticBoundaries){if(this.px>width)this.vx=-this.vx;if(this.px<0)this.vx=-this.vx;if(this.py>height)this.vy=-this.vy;if(this.py<0)this.vy=-this.vy;}}
this.render=function(){fill(0);ellipse(this.px,this.py,9,9);}}

Attraction

Finally, we consider attraction. In the real world, attraction often obeys the inverse square law: attraction is governed by:
attraction = 1 / (distance * distance);
This holds for gravity, electrical charge and magnetism. You could imagine other forms of attraction, e.g. the force exerted by an ideal spring is 1/distance.

As usual, we split attraction into x and y components. In this example, we have an array of two attractors in fixed locations. The force of the attractors (and any other forces) are all additive, so we simply call addForce for each force on each particle:

attract

// Attract -- add attraction to some objects

var myParticles = [];
var nParticles = 0; 
var dragFactor = 0.01;
var attractors = [];
var attrFactor = 10;
 
//==========================================================
function setup() {
    createCanvas(400, 400);
    attractors[0] = {x: width/4,   y: height/2, m: 10};
    attractors[1] = {x: width*3/4, y: height/2, m: 20};
}
 
function initializeParticle(p) {
    p.set(width / 2, height / 10);
    p.vx = random(-1, 1);
    p.vy = random(0.01, 1);
    p.damping = 1.0;
}

function mousePressed() {
    var p = new Particle();
    initializeParticle(p);

    // add p to set of particles
    myParticles[nParticles] = p;
    nParticles++;
}

function keyPressed() {
}

 
//==========================================================
function draw() {
    background(200);
    var fx = mouseX - width/2;
    var fy = mouseY - height/2;
 
    for (var i = 0; i < myParticles.length; i++) {
        var p = myParticles[i];
        p.addForce(fx / 1000, fy / 1000);
        // add drag proportional to square of velocity
        var dragX = (p.vx * p.vx) * dragFactor;
        // p.vx * p.vx is always positive. See if the direction of
        // drag should be negative:
        if (p.vx > 0) dragX = -dragX;
        var dragY = (p.vy * p.vy) * dragFactor;
        if (p.vy > 0) dragY = -dragY;
        p.addForce(dragX, dragY);
        // add attractors forces:
        for (var j = 0; j < attractors.length; j++) {
            var attr = attractors[j];
            var dx = attr.x - p.px;
            var dy = attr.y - p.py;
            var dist = sqrt(dx * dx + dy * dy);
            // to avoid dividing by zero or extreme forces, only apply
            // attractor force when distance is >1:
            if (dist > 1) {
                var ax = (dx/dist) * attr.m * attrFactor / (dist * dist);
                var ay = (dy/dist) * attr.m * attrFactor / (dist * dist);
                p.addForce(ax, ay);
            }
        }
        p.update(); // update all locations
        p.render(); // draw all particles
        if (!p.inBounds()) {
            initializeParticle(p);
        }
    }        
    line(width/2, height/10, width/2 + fx, height/10 + fy);
    for (var j = 0; j < attractors.length; j++) {
        var attr = attractors[j];
        ellipse(attr.x, attr.y, sqrt(attr.m), sqrt(attr.m));
    }
}
 
function Particle(){this.px=0;this.py=0;this.vx=0;this.vy=0;this.damping=0.96;this.mass=1.0;this.bLimitVelocities=true;this.bPeriodicBoundaries=false;this.bElasticBoundaries=false;
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;}
this.addForce=function(fx,fy){var ax=fx/this.mass;var ay=fy/this.mass;this.vx+=ax;this.vy+=ay;}
this.update=function(){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.inBounds=function(){return(this.px<width&&this.px>0&&this.py<height&&this.py>0);}
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.bElasticBoundaries){if(this.px>width)this.vx=-this.vx;if(this.px<0)this.vx=-this.vx;if(this.py>height)this.vy=-this.vy;if(this.py<0)this.vy=-this.vy;}}
this.render=function(){fill(0);ellipse(this.px,this.py,9,9);}}