Objects and Particle Systems
Why Objects?
Your last encounter with objects may have been “houses as objects”. The point of this was to show that you can create objects with multiple properties (such as width, height, color, position, etc.). Grouping properties into objects is much more convenient than managing all the properties separately. For example, if you wanted to “push” a house into an array, it is much simpler to write houses.push(house)
than to manage each property separately: houseWidths.push(houseWidth)
, houseHeights.push(houseHeight)
, houseColors.push(houseColor)
, etc.!
Objects allow us to create new abstractions like houses that have a combination of state (properties that serve as per-object variables) and operations (functions).
A key to thinking about objects is to realize there are two points of view: The user point of view makes instances of objects and calls object methods to accomplish tasks, e.g. flower.draw()
, not worrying about the details of how the object is implemented. The implementer point of view is concerned with setting the object properties and creating functions that manipulate these properties or use them, for example, to draw the object. It is best to take one point of view or the other. Thinking of both at once is very confusing.
In standard terminology, a class is the definition and implementation for a kind of object. An object is a particular instance of the class. For example, we say the “class of houses” or “the ‘house’ class” and “this particular ‘house’ object is an instance of the class of houses.”
Javascript does not have specific syntax for defining a class. You will see different approaches that are conceptually similar but differ in notation. However, Javascript has specific notation for objects, e.g. {x: 10, y: 50}
, so we will focus on creating and using objects, and we will minimize the discussion of classes.
Physics of Particles
Now we will use objects to implement some simple physics: gravity and bouncing. In the next section, we’ll create lots of these objects, attach some graphics and turn them loose.
Specifications
Our particle objects will model a simple physics simulation. The object contains state representing the x and y coordinates, and the x and y velocities (named dx
and dy
). (Note: “d” commonly means “delta” which means “change”, so “dx” is the “change in x (at each time step),” thus “dx” is velocity.) At each time step, the object position is updated according to velocity, the velocity is modified to account for gravity
(a global), and the velocity is modified to account for drag, which is proportional to velocity squared.
In addition, when the object reaches either the left, right, or bottom edges of the canvas, the object “bounces” with a new velocity scaled by the global named springy
.
Using particle objects, we can have many moving and bouncing objects where the details of motion are encapsulated into the properties of the objects themselves.
Implementation
Here are definitions for particle objects:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
var gravity = 0.1; // downward acceleration var springy = 0.9; // how much velocity is retained after bounce var drag = 0.0001; // drag causes particles to slow down function particleStep() { this.age++; this.x += this.dx; this.y += this.dy; if (this.x > width) { // bounce off right wall this.x = width - (this.x - width); this.dx = -this.dx * springy; } else if (this.x < 0) { // bounce off left wall this.x = -this.x; this.dx = -this.dx * springy; } if (this.y > height) { // bounce off bottom this.y = height - (this.y - height); this.dy = -this.dy * springy; } else if (this.y < 0) { // bounce off top this.y = -this.y; this.dy = -this.dy * springy; } this.dy = this.dy + gravity; // force of gravity // drag is proportional to velocity squared // which is the sum of the squares of dy and dy var vs = Math.pow(this.dx, 2) + Math.pow(this.dy, 2); // d is the ratio of old velocty to new velocity var d = vs * drag; // d goes up with velocity squared but can never be // so high that the velocity reverses, so limit d to 1 d = min(d, 1); // scale dx and dy to include drag effect this.dx *= (1 - d); this.dy *= (1 - d); } function particleDraw() { point(this.x, this.y); } // create a "Particle" object with position and velocity function makeParticle(px, py, pdx, pdy) { p = {x: px, y: py, dx: pdx, dy: pdy, age: 0, step: particleStep, draw: particleDraw } return p; } |
Notice how makeParticle
makes an object: We use the {property: value, property: value, ...}
to initialize properties, creating a new object each time makeParticle is called.
Two of the properties, step
and draw
, are initialized to functions (not your typical values!). This lets us, for example, draw a particle by writing someParticle.draw()
. The draw property is initialized to particleDraw
,so it is particleDraw
that is actually called. Within particleDraw
, the keyword this
refers to the object that “contains” the function. In this case, this
means whatever object is stored in the variable someParticle
.
When a function is a property and called using the syntax like someParticle.draw()
, the object is implicitly passed to the function as a formal parameter named this
.
The most complex method here is step
. You should be able to understand this method by reading the comments.
Particle Systems
Now, we can put the particle objects to work by creating a particle system. In the first example, we just launch np particles in random directions from the center of the canvas and draw them as they move:
var gravity = 0.1; // downward acceleration
var springy = 0.9; // how much velocity is retained after bounce
var drag = 0.0001; // drag causes particles to slow down
var np = 100; // how many particles
function particleStep() {
this.age++;
this.x += this.dx;
this.y += this.dy;
if (this.x > width) { // bounce off right wall
this.x = width - (this.x - width);
this.dx = -this.dx * springy;
} else if (this.x < 0) { // bounce off left wall
this.x = -this.x;
this.dx = -this.dx * springy;
}
if (this.y > height) { // bounce off bottom
this.y = height - (this.y - height);
this.dy = -this.dy * springy;
} else if (this.y < 0) { // bounce off top
this.y = -this.y;
this.dy = -this.dy * springy;
}
this.dy = this.dy + gravity; // force of gravity
// drag is proportional to velocity squared
// which is the sum of the squares of dy and dy
var vs = Math.pow(this.dx, 2) + Math.pow(this.dy, 2);
// d is the ratio of old velocty to new velocity
var d = vs * drag;
// d goes up with velocity squared but can never be
// so high that the velocity reverses, so limit d to 1
d = min(d, 1);
// scale dx and dy to include drag effect
this.dx *= (1 - d);
this.dy *= (1 - d);
}
function particleDraw() {
point(this.x, this.y);
}
// create a "Particle" object with position and velocity
function makeParticle(px, py, pdx, pdy) {
p = {x: px, y: py,
dx: pdx, dy: pdy,
age: 0,
step: particleStep,
draw: particleDraw
}
return p;
}
var particles = [];
function setup() {
createCanvas(400, 400);
for (var i = 0; i < np; i++) {
// make a particle
var p = makeParticle(200, 200,
random(-50, 50), random(-50, 50));
// push the particle onto particles array
particles.push(p);
}
frameRate(10);
}
function draw() {
background(230);
stroke(0);
strokeWeight(10);
for (var i = 0; i < np; i++) { // for each particle
var p = particles[i];
p.step();
p.draw();
}
}
Note that setup
creates np
objects, and draw
calls the step
and draw
methods on each particle to implement the simulation of particle motion.
Mouse Particles
The following variation adds particles to the system when the mouse is pressed. Particles start at the mouse position with random velocities. After 200 steps, the particles are removed from the list of particles so that the number does not grow too big as we continue adding particles with the mouse.
mouseparticles – click the mouse in the canvas to create new particles
var gravity = 0.3; // downward acceleration
var springy = 0.7; // how much velocity is retained after bounce
var drag = 0.0001; // drag causes particles to slow down
var np = 100; // how many particles
function particleStep() {
this.age++;
this.x += this.dx;
this.y += this.dy;
if (this.x > width) { // bounce off right wall
this.x = width - (this.x - width);
this.dx = -this.dx * springy;
} else if (this.x < 0) { // bounce off left wall
this.x = -this.x;
this.dx = -this.dx * springy;
}
if (this.y > height) { // bounce off bottom
this.y = height - (this.y - height);
this.dy = -this.dy * springy;
} else if (this.y < 0) { // bounce off top
this.y = -this.y;
this.dy = -this.dy * springy;
}
this.dy = this.dy + gravity; // force of gravity
// drag is proportional to velocity squared
// which is the sum of the squares of dy and dy
var vs = Math.pow(this.dx, 2) + Math.pow(this.dy, 2);
// d is the ratio of old velocty to new velocity
var d = vs * drag;
// d goes up with velocity squared but can never be
// so high that the velocity reverses, so limit d to 1
d = min(d, 1);
// scale dx and dy to include drag effect
this.dx *= (1 - d);
this.dy *= (1 - d);
}
function particleDraw() {
point(this.x, this.y);
}
// create a "Particle" object with position and velocity
function makeParticle(px, py, pdx, pdy) {
p = {x: px, y: py,
dx: pdx, dy: pdy,
age: 0,
step: particleStep,
draw: particleDraw
}
return p;
}
var particles = [];
function setup() {
createCanvas(400, 400);
for (var i = 0; i < np; i++) {
// make a particle
var p = makeParticle(200, 200,
random(-50, 50), random(-50, 50));
// push the particle onto particles array
particles.push(p);
}
frameRate(10);
}
// draw all particles in the particles array
//
function draw() {
background(230);
stroke(0);
strokeWeight(10);
if (mouseIsPressed) {
var newp = makeParticle(mouseX, mouseY,
random(-10, 10), random(-10, 0));
particles.push(newp);
}
// newParticles will hold all the particles that we want to
// retain for the next call to draw() -- we will retain particles
// if the age is < 200 (frames). Initially, newParticle is empty
// because we have not found any "young" particles yet.
newParticles = [];
for (var i = 0; i < particles.length; i++) { // for each particle
var p = particles[i];
p.step();
p.draw();
// since we are "looking" at every particle in order to
// draw it, let's use the opportunity to see if particle[i]
// is younger than 200 frames. If so, we'll push it onto the
// end of newParticles.
if (p.age < 200) {
newParticles.push(p);
}
}
// now, newParticles has EVERY particle with an age < 200 frames.
// these are the particles we want to draw next time, so assign
// particles to this new array. The old value of particles, i.e.
// the entire array, is simply "lost" -- Javascript will reclaim
// and reuse the memory since that array is no longer needed.
particles = newParticles;
}
Note how “old” particles are gotten rid of: Since we are iterating through all particles, we construct a new list with every particle whose age is less than 200. You might expect a method such as p.getAge()
to get the current age of a particle, but here we just access the property directly using the p.age
notation. Some frown on this direct access to the inner representation of an object, while others see it as an efficient and largely harmless shortcut. Anyway, after constructing the new list we simply replace the old list with the new one.
Consider the line particles = newParticles
. You might expect to see something like particles[i] = newParticles[i]
since both variables are arrays. In this case, we are not changing the array referred to by particles
. Instead, we are saying that particles
no longer refers to that array, and now particles
will refer to the new array. After the assignment, particles
and newParticles
both refer to the same array! However, newParticles
was just a temporary variable that allowed us to refer to bothh arrays while we constructed newParticles
. We want to forget about the old particles
and remember newParticles
for the next draw()
, hence the assignment.
The “forgotten” particles
array is reclaimed by the Javascript runtime system so that the array memory can be reused.