Magic numbers
Avoid magic numbers. Magic numbers are just numbers in your code that represent some abstract data value. The problem is that they are spreed within the code. To make changes to these values requires searching the code and often the same values are repeated making the task even more difficult.
Defining the magic numbers as named constants makes making changes very easy. See rewrite
Using native API's
P5 is great but using it to create such a simple rendering task is not worth the need to load a huge chunk of code to do what the native API will do quicker (No need for the P5's overhead)
Naming
There are a lot of problems with your naming style.
JS naming convention
The naming convention for JavaScript is to use lowerCamelCase for all names with the exception of objects created with the new token which uses UpperCamelCase (Static objects and object factories can also use UpperCamelCase). For constants you can use UPPER_SNAKE_CASE
_underscoredNames
Use underscore to avoid possible naming classes in systems where you have no control of names.
Using underscore should only ever be a very last option and should never be used if there is no need. Eg the argument _n
in push_particles(_n) {
should be pushParticles(n)
Avoid redundancy
It is easy to add too much to a name. Keep names short and use as few words as possible. Names are never created in isolation and like a natural language requires context to have meaning.
Eg
Particles_Manipulation
is overkill when Particles
is all that is needed
my_Particles_Manipulation
Never use my
(unless its instructional code) This name can just be particles
, or smoke
push_particles
(don't repeat names) What does this function do? "Adds particles". push becomes
add. What does it add particles to? "The Particles object", thus
particlescan be inferred. The name
addis all that is needed. If you were adding monkeys then maybe
addMonkeys`
Avoid repeated long references
Avoid doing the same long reference path and array indexing by store the object reference in a variable.
this.particles[i].update();
this.particles[i].show();
if (this.particles[i].finished()) {
// remove this particle
this.particles.splice(i, 1);
}
Becomes
const p = this.particles[i];
p.update();
p.show();
if (p.finished()) {
this.particles.splice(i, 1);
}
Keep an eye on Logic
The render system reduces the particle alpha each step. You use the alpha value to check when to remove particles alpha < 0
. However you reduce the alpha, then render, then remove. That means than once going every frame you are trying to render 5 invisible particles. Remove befor rendering.
JS and Memory
Javascript is a managed language. Meaning that it manages memory for you.
This is great in most cases but it comes with a serious performance hit in applications like particle systems where many small independent objects are frequently being created and destroyed .
Object pool
To avoid the memory management overhead we use object pools.
An object pool is just an array of objects that are not being used. When an object is no longer needed we remove it from the main array and put it in the pool. When a new object is needed we check the pool, if there are any object in the pool we use it rather than create a new one.
This means that the particle object will need a function that resets it.
See rewrite
Note you can also pool the particles in the one array by using a bubble iterator to avoid the expensive @MDN.JS.OBJ.Array.slice@. Dead particle bubble to one end of the array. This is even faster but makes the inner update loop a lot more complex. You would only do this if you where using 10K+ particles.
JavaScript OO design
Many believe that using class
is how to write good OO JavaScript. Sorry to say it is not and is in fact the worst way to write OO JavaScript. The class syntax does not provide a good encapsulation model, its only plus is easy inheritance, which is kind of redundant in lieu of JavaScript's lose polymorphism
In this case factory functions (a function that creates Object AKA Object factory) is best suited. Because we use an object pool when need not even bother with assigning a prototytpe
for particle functions.
Some advantages of factory functions
This eliminates the need to use this
and new
.
Lets you define private properties without the need to #hack
names
Lets you create objects before the first execution pass has completed (move the init and setups to the top where it belongs)
Some disadvantages of factory functions
Rewrite
The rewrite uses the CanvasRenderingContext2D API to avoid the overhead of P5, Some replacement functions are at the bottom.
Note that when defining constants it is always a good idea to include unit types, ranges, and or a description as a comment
animate();
;(() => {
const CANVAS_WIDTH = 600; // in CSS pixels
const CANVAS_HEIGHT = 400;
const PARTICLE_SPREAD = 1; // max x speed in canvas pixels
const PARTICLE_POWER = 3; // max y speed in canvas pixels
const PARTICLE_RADIUS = 12; // max y speed in canvas pixels
const MAX_PARTICLES = 1000; // Approx max particle count
const PARTICLES_RATE = 3; // new particle per frame
const SMOKE_PARTICLES_PATH = new Path2D; // circle used to render particle
const PARTICLE_COLOR = "#FFF"; // any valid CSS color value
const START_ALPHA = 1; // particle start alpha max
const START_ALPHA_MIN = 0.5; // particle start alpha
// Rate particle alpha decays to match MAX_PARTICLES. The life time in seconds
// can be calculates as Math.ceil(1 / ALPHA_DECAY_SPEED) * 60
const ALPHA_DECAY_SPEED = 1 / (MAX_PARTICLES / PARTICLES_RATE);
SMOKE_PARTICLES_PATH.arc(0, 0, PARTICLE_RADIUS, 0, Math.PI * 2);
const canvas = createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
const smoke = new Particles(canvas);
animate.draw = function() {
canvas.ctx.setTransform(1, 0, 0, 1, 0, 0);
canvas.ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
smoke.add(PARTICLES_RATE);
smoke.update();
}
function Particle() {
var x, y, vx, vy, alpha;
const PARTICLE = {
init() {
x = CANVAS_WIDTH / 2;
y = CANVAS_HEIGHT;
vx = random(-PARTICLE_SPREAD, PARTICLE_SPREAD);
vy = random(-PARTICLE_POWER, -PARTICLE_POWER);
alpha = random(START_ALPHA_MIN, START_ALPHA);
return PARTICLE;
},
update() {
x += vx;
y += vy;
alpha -= ALPHA_DECAY_SPEED;
return alpha < 0;
},
draw(ctx) {
ctx.globalAlpha = alpha;
ctx.setTransform(1, 0, 0, 1, x, y);
ctx.fill(SMOKE_PARTICLES_PATH);
},
};
return Object.freeze(PARTICLE.init());
}
function Particles(canvas) {
const particles = [];
const pool = [];
const ctx = canvas.ctx;
return Object.freeze({
add(n) {
while (n-- > 0) {
pool.length ?
particles.push(pool.pop().init()) :
particles.push(Particle());
}
},
update() {
var i = 0;
ctx.fillStyle = PARTICLE_COLOR;
while (i < particles.length) {
const p = particles[i];
p.update() ?
pool.push(particles.splice(i--, 1)[0]) :
p.draw(ctx);
i++;
}
}
});
}
})();
// functions to replace P5
function createCanvas(width, height) {
const canvas = Object.assign(document.createElement("canvas"), {width, height, className: "renderCanvas"});
document.body.appendChild(canvas);
canvas.ctx = canvas.getContext("2d");
return canvas;
}
function random(min, max) { return Math.random() * (max - min) + min }
function animate() {
animate.draw?.();
requestAnimationFrame(animate);
}
.renderCanvas {
background: black;
}