Quick Reference
SWParticle is a SketchWave entity class that simulates a single physics-driven particle. Particles are born at a spawn point, launched in a random (or fixed) direction, decelerate under drag, accelerate downward under gravity, and fade out as their alpha decays each frame. When isDead() returns true the particle should be removed from your array.
- Design Pattern: Entity — one object per particle; manage an array of them yourself
- Physics: drag → gravity → position update → alpha fade, one step per
update()call - Shapes:
'circle','square','star', or'mix'(randomly resolved per particle at spawn) - Color: Per-particle random HSB hue from a configurable range; fixed saturation & brightness
- Dependencies: p5.js (
colorMode(HSB, 360, 100, 100, 100)required), SWColor, SWPoint, SWDisk, SWRectangle - Key Features: Configurable gravity, drag, alpha decay, speed/size/hue ranges, directional angle + angular spread, star point range, grid-aware rendering,
reset()for respawning - Common Uses: Explosions, fireworks, fire emitters, sparks, confetti, magic effects, trail particles
📜 Origins: Crosby’s Bomb Saga
Particle class was designed and built during
Crosby’s Bomb Saga
(SWCrosbyBombSaga2026-05-01-Stg1). That saga produced the first working explosion particle
system and served as the proving ground for the particle lifecycle pattern (spawn → update
→ draw → dead check → splice). The
allAboutTheParticleClassAndClassDesign.html
document in that saga provides a detailed walkthrough of
how the class was designed and why it was structured as a per-particle entity rather than
a pooled or manager-based system.
The SWParticle class in the main SketchWaveJS library is a direct evolution of
that original design. Several features were added — shape variety (square,
star, mix), directional angle + spread, star point range,
and grid-aware rendering via drawOnGrid() — while preserving
the simple, learner-friendly API. The finished() method is kept as an alias
for isDead() for backward compatibility with sketches written during the saga.
Overview
Each SWParticle object manages the full lifecycle of a single particle: spawn, move, fade, die. Particles operate in canvas pixel coordinates. The rendering methods draw() and drawOnGrid() delegate to SWDisk (circle), SWRectangle (square), or a built-in star polygon helper, using the particle’s current position, size, and HSB color.
Physics Model
Each call to update() performs four steps in order:
vel *= drag // velocity drag: slows the particle each frame
vel += acc // acceleration: gravity always pointing down
pos += vel // position moves by current velocity
alpha -= alphaDecay // alpha decrements until particle fades out
The acceleration vector is set once at construction (and again if setGravity() is called) as (0, gravity). Positive gravity pulls particles downward; a small negative value can simulate rising sparks or fire.
colorMode(HSB, 360, 100, 100, 100),
so alpha runs from 0–100, not 0–255. An alphaDecay of 1.5
means the particle survives roughly 67 frames (about 2 seconds at 30 fps)
before it reaches zero.
Particle Shapes
'circle'(default) — draws anSWDiskwith no center dot'square'— draws anSWRectanglewith no stroke, no vertices marker'star'— draws a star polygon usingSWParticle._drawStar(); outer radius =size/2, inner radius = outer × 0.45; point count chosen randomly from[starPointsMin, starPointsMax]'mix'— each particle independently resolves to circle, square, or star at spawn time and keeps that shape for its lifetime
The resolved shape is stored in _resolvedShape. Calling reset() re-runs _init(), which picks a new shape for 'mix' particles and a new star point count for 'star' and 'mix' particles.
Directional Launch
By default, particles launch in a random direction over the full circle. Set angle (and optionally spread) in the constructor options to constrain the launch cone:
// Fire: shoots upward in a narrow cone (p5.js: -HALF_PI = straight up)
new SWParticle(x, y, { angle: -HALF_PI, spread: 0.4 });
// Fountain: shoots upward in a wider cone
new SWParticle(x, y, { angle: -HALF_PI, spread: PI / 2 });
// Full random (default)
new SWParticle(x, y); // spread defaults to TWO_PI
Typical Lifecycle Pattern
let particles = [];
function draw() {
background(0, 0, 10, 20); // semi-transparent to leave trails
// Walk array backwards so splice() doesn't skip elements
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
particles[i].draw();
if (particles[i].isDead()) particles.splice(i, 1);
}
}
function mousePressed() {
for (let i = 0; i < 80; i++) {
particles.push(new SWParticle(mouseX, mouseY, { hueMin: 0, hueMax: 40 }));
}
}
particles.length - 1 down to 0 ensures that
removing a dead particle with splice(i, 1) does not shift
the index of any particle you have not yet visited.
Grid Rendering
drawOnGrid(grid) renders the particle in SWGrid user-space coordinates. Because particles move in pixel space, the method converts pos.x / pos.y to user coordinates via grid.screenToUser() and scales the radius by 1 / grid.xScale. Use this when your sketch uses a grid and you want particles to respect the same coordinate system as your other shapes.
Constructor
new SWParticle(x, y, options)Creates a new particle at pixel position (x, y). All physics constants and appearance options are set from the options object at construction; the live physics state (position, velocity, alpha, size, hue, shape) is randomized immediately by the internal _init() call.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
x | number | required | Spawn x in canvas pixel coordinates |
y | number | required | Spawn y in canvas pixel coordinates |
| options (all optional) | |||
speedMin | number | 5 | Minimum initial speed in pixels/frame |
speedMax | number | 20 | Maximum initial speed in pixels/frame |
gravity | number | 0.15 | Downward acceleration added to velocity each frame (positive = down). Negative values push particles upward. |
drag | number | 0.95 | Velocity multiplier each frame (0–1). 1 = no drag; 0 = instant stop. |
alphaDecay | number | 1.5 | Alpha units lost per frame on the 0–100 scale. Higher = shorter lifetime. |
sizeMin | number | 4 | Minimum particle diameter in pixels |
sizeMax | number | 20 | Maximum particle diameter in pixels |
hueMin | number | 0 | Minimum HSB hue (0–360) |
hueMax | number | 30 | Maximum HSB hue (0–360). Default 0–30 produces red-orange fire colors. |
saturation | number | 100 | HSB saturation applied to all particles (0–100) |
brightness | number | 100 | HSB brightness applied to all particles (0–100) |
shape | string | 'circle' | 'circle', 'square', 'star', or 'mix' |
starPointsMin | number | 3 | Minimum star points when shape is 'star' or 'mix'. Clamped to [3, 9]. |
starPointsMax | number | 6 | Maximum star points when shape is 'star' or 'mix'. Clamped to [3, 9]. |
rotSpeedMin | number | −0.15 | Minimum rotation speed in radians/frame for square and star particles. Negative = counter-clockwise. |
rotSpeedMax | number | 0.15 | Maximum rotation speed in radians/frame. Each spawned square or star picks a random speed in [rotSpeedMin, rotSpeedMax]. |
angle | number | undefined | Fixed launch angle in radians. undefined = random full-circle each spawn. Use p5 constants: -HALF_PI = up, HALF_PI = down, 0 = right, PI = left. |
spread | number | TWO_PI | Angular spread in radians around angle. Only meaningful when angle is defined. 0 = perfectly aligned; PI/4 = narrow cone. |
Example
// Basic explosion particle — red-orange hues, full random direction
const p = new SWParticle(mouseX, mouseY, {
hueMin: 0,
hueMax: 40,
speedMin: 4,
speedMax: 16,
gravity: 0.2,
drag: 0.94,
alphaDecay: 1.8,
});
// Upward fire particle — yellow-orange, constrained direction
const fire = new SWParticle(cx, cy, {
hueMin: 10,
hueMax: 55,
speedMin: 2,
speedMax: 7,
gravity: -0.04, // slight upward drift
drag: 0.97,
alphaDecay: 2.0,
sizeMin: 3,
sizeMax: 12,
angle: -HALF_PI, // shoot upward
spread: 0.5, // in a narrow cone
});
// Confetti particle — any hue, mixed shapes
const c = new SWParticle(width / 2, height / 2, {
hueMin: 0, hueMax: 360,
gravity: 0.08, drag: 0.98,
alphaDecay: 0.4,
sizeMin: 5, sizeMax: 12,
shape: 'mix',
starPointsMin: 4, starPointsMax: 6,
});
Live Properties
These properties reflect the particle’s current physics state. They are initialized by the constructor and updated by update() and reset(). Direct writes are possible but the setter methods are preferred for physics constants.
| Property | Type | Description |
|---|---|---|
pos | p5.Vector | Current position in pixel coordinates. Updated by update(). |
vel | p5.Vector | Current velocity in pixels/frame. Scaled by drag and added to by acceleration each frame. |
acc | p5.Vector | Constant acceleration vector (0, gravity). Updated when setGravity() is called. |
alpha | number | Current alpha on 0–100 scale. Decrements by alphaDecay each frame. Particle is dead when ≤ 0. |
size | number | Particle diameter in pixels. Randomized in [sizeMin, sizeMax] at each spawn. |
hue | number | HSB hue value. Randomized in [hueMin, hueMax] at each spawn. |
saturation | number | HSB saturation (0–100). Constant; set by options at construction. |
brightness | number | HSB brightness (0–100). Constant; set by options at construction. |
gravity | number | Downward acceleration per frame. Updated by setGravity(). |
drag | number | Velocity multiplier per frame (0–1). Updated by setDrag(). |
alphaDecay | number | Alpha reduction per frame. Updated by setAlphaDecay(). |
shape | string | Shape setting ('circle', 'square', 'star', or 'mix'). |
_resolvedShape | string | Concrete shape for this particle’s lifetime ('mix' resolves to circle, square, or star at spawn). Read-only; set by _init(). |
_starPoints | number | Number of star points for this particle (3–9). Randomized each spawn for 'star' and 'mix'. Read-only; set by _init(). |
_rot | number | Current rotation angle in radians. Incremented by _rotVel each frame in update(). Applies to square and star particles. |
_rotVel | number | Rotation velocity in radians/frame. Randomized in [rotSpeedMin, rotSpeedMax] at each spawn by _init(). |
Methods
update()Advances the particle physics by one frame. Call once per frame, before draw().
Applies drag to vel, adds acc to vel, adds vel to pos, then subtracts alphaDecay from alpha.
particles[i].update();draw()Draws the particle at its current canvas pixel position using its resolved shape (circle, square, or star).
Requires colorMode(HSB, 360, 100, 100, 100) to be active. Clamps alpha to 0 before rendering so negative alpha values do not cause errors.
particles[i].draw();drawOnGrid(grid)Draws the particle with its pixel position converted to user-space coordinates via grid.screenToUser(). The pixel radius is scaled to user units via 1 / grid.xScale.
Use this when your sketch sets up an SWGrid and all shapes draw in user coordinates. The particle still moves in pixel space — only rendering is remapped.
particles[i].drawOnGrid(grid);isDead() → booleanReturns true when the particle’s alpha has reached zero (fully faded out). Check this after calling update() and remove the particle from your array.
if (particles[i].isDead()) particles.splice(i, 1);finished() → booleanAlias for isDead(). Kept for backward compatibility with sketches from Crosby’s Bomb Saga that used p.finished().
if (particles[i].finished()) particles.splice(i, 1);reset(x?, y?)Respawns the particle with fresh random velocity, size, hue, and full alpha. All physics constants (gravity, drag, alphaDecay, etc.) are preserved from construction — only the live state is re-randomized.
If x or y is omitted, the particle respawns at its original construction coordinates.
// Respawn at new location (e.g. new click)
p.reset(mouseX, mouseY);
// Respawn at original spawn point
p.reset();Setters
Setters update physics constants. Changes to gravity, drag, alpha decay, ranges, or direction take effect on the next update() or spawn. They do not retroactively affect a particle’s current live state (position, velocity, size, hue) — use reset() to re-randomize those.
setGravity(g)Sets downward acceleration per frame. Also rebuilds the internal acc vector to (0, g) immediately, so the running particle’s trajectory changes at once.
p.setGravity(0.3); // stronger pull
p.setGravity(-0.1); // gentle upward drift (fire/smoke)setDrag(d)Sets the velocity multiplier applied each frame. Values close to 1 let particles travel farther; values close to 0 stop them almost immediately.
p.setDrag(0.99); // very little drag (comet-like)
p.setDrag(0.85); // heavy drag (floaty sparks)setAlphaDecay(a)Sets how many alpha units are lost per frame on the 0–100 scale. Lifetime in frames ≈ 100 / alphaDecay.
p.setAlphaDecay(0.5); // long-lived (~200 frames)
p.setAlphaDecay(3.0); // fast fade (~33 frames)setHueRange(minHue, maxHue)Updates the HSB hue range. New particles spawned (or reset) after this call will pick a hue from the new range. Does not change the hue of already-spawned particles.
p.setHueRange(200, 260); // blue-purple palettesetSizeRange(minSize, maxSize)Updates the diameter range in pixels for future spawns. Does not resize the currently-live particle.
p.setSizeRange(2, 8); // fine sparks
p.setSizeRange(10, 30); // big chunkssetSpeedRange(minSpeed, maxSpeed)Updates the speed range (pixels/frame) for future spawns. Does not change the current velocity.
p.setSpeedRange(1, 5); // gentle drift
p.setSpeedRange(10, 25); // violent explosionsetDirectionAngle(angle, spread?)Fixes the launch direction for future spawns. angle is in radians (use p5 constants: -HALF_PI = up, HALF_PI = down, 0 = right, PI = left). spread is the full angular width of the cone in radians. Pass undefined as the angle to restore full random direction.
p.setDirectionAngle(-HALF_PI, PI / 3); // upward, 60° cone
p.setDirectionAngle(undefined); // restore randomsetShape(shape)Changes the shape setting. Takes effect on the next spawn or reset(). The currently-live particle keeps its existing _resolvedShape.
p.setShape('star'); // future spawns are stars
p.setShape('mix'); // future spawns randomly pick shapesetStarPointsRange(minPts, maxPts)Sets the star point count range for future star/mix spawns. Values are clamped to [3, 9].
p.setStarPointsRange(5, 8); // 5- to 8-pointed starssetRotationSpeed(minSpeed, maxSpeed)Sets the rotation speed range for square and star particles. Each spawned particle picks a random angular velocity in [minSpeed, maxSpeed] radians/frame. Negative = counter-clockwise; positive = clockwise. Circles are symmetric and ignore rotation.
p.setRotationSpeed(-0.3, 0.3); // faster spin in either direction
p.setRotationSpeed(0.05, 0.2); // always clockwise, gentle to moderate
p.setRotationSpeed(0, 0); // lock rotation (no spin)Usage Examples
Example 1: Click-to-Burst Explosion
Classic firework-style burst: every mouse click spawns 80 particles in all directions with fire colors, leaving glowing trails via a semi-transparent background.
let particles = [];
function setup() {
createCanvas(600, 400);
colorMode(HSB, 360, 100, 100, 100);
}
function draw() {
// Semi-transparent background produces motion trails
background(0, 0, 8, 20);
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
particles[i].draw();
if (particles[i].isDead()) particles.splice(i, 1);
}
}
function mousePressed() {
for (let i = 0; i < 80; i++) {
particles.push(new SWParticle(mouseX, mouseY, {
hueMin: 0,
hueMax: 40,
speedMin: 3,
speedMax: 18,
gravity: 0.2,
drag: 0.94,
alphaDecay: 1.5,
sizeMin: 4,
sizeMax: 18,
}));
}
}
Example 2: Continuous Fire Emitter
Emits a small number of upward-shooting particles each frame, simulating a flame. Negative gravity and a narrow directional cone keep the fire rising.
let particles = [];
const CX = 300, CY = 380; // emitter base
function setup() {
createCanvas(600, 400);
colorMode(HSB, 360, 100, 100, 100);
}
function draw() {
background(0, 0, 8, 30);
// Emit 4 new fire particles per frame
for (let i = 0; i < 4; i++) {
particles.push(new SWParticle(CX + random(-5, 5), CY, {
hueMin: 0,
hueMax: 50,
speedMin: 2,
speedMax: 7,
gravity: -0.04, // slight upward drift to keep fire rising
drag: 0.97,
alphaDecay: 2.0,
sizeMin: 3,
sizeMax: 14,
angle: -HALF_PI, // shoot straight up
spread: 0.45, // narrow cone
}));
}
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
particles[i].draw();
if (particles[i].isDead()) particles.splice(i, 1);
}
}
Example 3: Confetti Burst with Mixed Shapes
Launches a one-time confetti burst from center canvas with the full hue range, mixed shapes, slow alpha decay, and low gravity for a floating effect.
let confetti = [];
function setup() {
createCanvas(600, 400);
colorMode(HSB, 360, 100, 100, 100);
// Launch all confetti at once on startup
for (let i = 0; i < 150; i++) {
confetti.push(new SWParticle(width / 2, height / 2, {
hueMin: 0,
hueMax: 360, // full rainbow range
speedMin: 2,
speedMax: 12,
gravity: 0.06, // gentle fall
drag: 0.98, // slow down gradually
alphaDecay: 0.35, // long lifetime (~285 frames)
sizeMin: 5,
sizeMax: 14,
shape: 'mix',
starPointsMin: 4,
starPointsMax: 6,
}));
}
}
function draw() {
background(0, 0, 12, 25);
for (let i = confetti.length - 1; i >= 0; i--) {
confetti[i].update();
confetti[i].draw();
if (confetti[i].isDead()) confetti.splice(i, 1);
}
}
Example 4: Particle System with Reset
Reuses a fixed pool of particles by calling reset() instead of creating new objects. This avoids garbage collection overhead in performance-critical sketches.
const POOL_SIZE = 100;
let pool = [];
function setup() {
createCanvas(600, 400);
colorMode(HSB, 360, 100, 100, 100);
// Pre-allocate the pool
for (let i = 0; i < POOL_SIZE; i++) {
pool.push(new SWParticle(300, 300, {
hueMin: 180, hueMax: 260, // blue-purple sparks
alphaDecay: 1.2,
}));
}
}
function draw() {
background(0, 0, 8, 20);
for (const p of pool) {
p.update();
p.draw();
// When dead, respawn at new location
if (p.isDead()) p.reset(mouseX, mouseY);
}
}
Source Code
Show / Hide swParticle.js source
/*
File: swParticle.js
Date: 2026-05-13
Author: klp + GitHub Copilot
Workspace: SketchWaveTNT2026-05-01-Stg9
Purpose: SWParticle class for SketchWaveJS
SWParticle represents a single physics-driven particle for explosion, fire, confetti,
sparks, and other burst or emitter effects. Each particle launches from a spawn point
with a random velocity, decelerates under configurable drag, accelerates downward under
gravity, and fades out as its alpha decreases each frame. When alpha reaches zero the
particle is "dead" and should be removed from your array.
Physics model (applied once per call to update()):
vel *= drag — velocity drag (slows the particle over time)
vel += acc — constant acceleration (gravity, always pointing down)
pos += vel — position update
alpha -= alphaDecay — fade out
Typical usage pattern:
// Spawn a burst of 100 particles at the click position
for (let i = 0; i < 100; i++) {
particles.push(new SWParticle(mouseX, mouseY, { hueMin: 0, hueMax: 30 }));
}
// In draw():
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
particles[i].draw();
if (particles[i].isDead()) particles.splice(i, 1);
}
Constructor:
new SWParticle(x, y, options)
x, y : number — spawn position in canvas pixel coordinates
options :
speedMin : number — min initial speed in pixels/frame (default 5)
speedMax : number — max initial speed in pixels/frame (default 20)
gravity : number — downward acceleration per frame in pixels (default 0.15)
drag : number — velocity multiplier per frame, 0–1 (default 0.95)
alphaDecay : number — alpha reduction per frame on 0–100 scale (default 1.5)
sizeMin : number — min particle diameter in pixels (default 4)
sizeMax : number — max particle diameter in pixels (default 20)
hueMin : number — min HSB hue, 0–360 (default 0)
hueMax : number — max HSB hue, 0–360 (default 30)
saturation : number — HSB saturation, 0–100 (default 100)
brightness : number — HSB brightness, 0–100 (default 100)
shape : string — 'circle' | 'square' | 'star' | 'mix' (default 'circle')
starPointsMin : number — min star points for random selection, 3–9 (default 3)
starPointsMax : number — max star points for random selection, 3–9 (default 6)
angle : number — fixed launch angle in radians (default: random full circle)
spread : number — angular spread in radians around angle (default: TWO_PI)
rotSpeedMin : number — min rotation speed in radians/frame (default -0.15)
rotSpeedMax : number — max rotation speed in radians/frame (default 0.15)
'mix' shape: each particle randomly resolves to circle, square, or star at spawn time.
Star geometry: outer radius = size/2, inner radius = outer * 0.45, points chosen
randomly between starPointsMin and starPointsMax each time a star particle spawns.
Key methods:
update() — advance physics one frame; call once per frame
draw() — draw at current pixel position (primary draw method)
drawOnGrid(grid) — draw with position converted via grid.screenToUser()
isDead() — returns true when alpha ≤ 0 (particle fully faded)
finished() — alias for isDead() (backward compatibility)
reset(x, y) — respawn at (x, y) with fresh random vel/size/hue/alpha
setGravity(g) — update gravity acceleration
setDrag(d) — update drag coefficient
setAlphaDecay(a) — update alpha decay rate
setHueRange(min, max) — update hue range
setSizeRange(min, max)— update size range
setSpeedRange(min, max)— update speed range
setDirectionAngle(angle, spread) — fix launch direction with optional spread
setShape(shape) — change particle shape ('circle' | 'square' | 'star' | 'mix')
setRotationSpeed(min, max) — set spin speed range for square and star particles (rad/frame)
Dependencies: p5.js, SWColor, SWPoint, SWDisk, SWGrid
*/
console.log("[swParticle.js] SWParticle class loaded.");
class SWParticle {
/**
* Creates a new particle at the given pixel coordinates.
*
* @param {number} x - Spawn x in canvas pixel coordinates
* @param {number} y - Spawn y in canvas pixel coordinates
* @param {Object} [options={}]
* speedMin : number — min initial speed in pixels/frame (default 5)
* speedMax : number — max initial speed in pixels/frame (default 20)
* gravity : number — downward acceleration per frame (default 0.15)
* drag : number — velocity damping per frame, 0–1 (default 0.95)
* alphaDecay : number — alpha lost per frame on 0–100 scale (default 1.5)
* sizeMin : number — minimum particle diameter in pixels (default 4)
* sizeMax : number — maximum particle diameter in pixels (default 20)
* hueMin : number — minimum HSB hue, 0–360 (default 0)
* hueMax : number — maximum HSB hue, 0–360 (default 30)
* saturation : number — HSB saturation, 0–100 (default 100)
* brightness : number — HSB brightness, 0–100 (default 100)
* shape : string — 'circle' | 'square' | 'star' | 'mix' (default 'circle')
* starPointsMin : number — min star points when shape is 'star' or 'mix' (default 3)
* starPointsMax : number — max star points when shape is 'star' or 'mix' (default 6)
* angle : number — fixed launch angle in radians (default: random)
* spread : number — angular spread in radians around angle (default TWO_PI)
* rotSpeedMin : number — min rotation speed in radians/frame (default -0.15)
* rotSpeedMax : number — max rotation speed in radians/frame (default 0.15)
*/
constructor(x, y, options = {}) {
this.speedMin = options.speedMin !== undefined ? options.speedMin : 5;
this.speedMax = options.speedMax !== undefined ? options.speedMax : 20;
this.gravity = options.gravity !== undefined ? options.gravity : 0.15;
this.drag = options.drag !== undefined ? options.drag : 0.95;
this.alphaDecay = options.alphaDecay !== undefined ? options.alphaDecay : 1.5;
this.sizeMin = options.sizeMin !== undefined ? options.sizeMin : 4;
this.sizeMax = options.sizeMax !== undefined ? options.sizeMax : 20;
this.hueMin = options.hueMin !== undefined ? options.hueMin : 0;
this.hueMax = options.hueMax !== undefined ? options.hueMax : 30;
this.saturation = options.saturation !== undefined ? options.saturation : 100;
this.brightness = options.brightness !== undefined ? options.brightness : 100;
this.shape = options.shape !== undefined ? options.shape : 'circle';
this.starPointsMin = options.starPointsMin !== undefined ? options.starPointsMin : 3;
this.starPointsMax = options.starPointsMax !== undefined ? options.starPointsMax : 6;
this.rotSpeedMin = options.rotSpeedMin !== undefined ? options.rotSpeedMin : -0.15;
this.rotSpeedMax = options.rotSpeedMax !== undefined ? options.rotSpeedMax : 0.15;
this._angle = options.angle; // undefined = random each time
this._spread = options.spread !== undefined ? options.spread : TWO_PI;
// Store original spawn point for reset() with no arguments
this._spawnX = x;
this._spawnY = y;
// Live physics state — initialized by _init()
this.pos = null;
this.vel = null;
this.acc = null;
this.alpha = 100;
this.size = 0;
this.hue = 0;
this._resolvedShape = this.shape; // resolved concrete shape (no 'mix')
this._starPoints = 5; // point count for star, set in _init()
this._rot = 0; // current rotation angle (radians)
this._rotVel = 0; // rotation velocity (radians/frame)
this._init(x, y);
}//end constructor
// ── Internal ──────────────────────────────────────────────────────────
/**
* Initializes (or re-initializes) all live physics state with fresh random values.
* Called by the constructor and by reset().
* @param {number} x - spawn x in pixel coordinates
* @param {number} y - spawn y in pixel coordinates
*/
_init(x, y) {
this.pos = createVector(x, y);
this.acc = createVector(0, this.gravity);
this.alpha = 100;
this.size = random(this.sizeMin, this.sizeMax);
this.hue = random(this.hueMin, this.hueMax);
// Resolve 'mix' to a concrete shape for this particle's lifetime
if (this.shape === 'mix') {
const shapes = ['circle', 'square', 'star'];
this._resolvedShape = shapes[Math.floor(random(shapes.length))];
} else {
this._resolvedShape = this.shape;
}
// Pick a random star point count within the configured range
this._starPoints = Math.round(random(
Math.max(3, this.starPointsMin),
Math.min(9, this.starPointsMax)
));
// Launch velocity: random direction, or within a spread cone around a fixed angle
const speed = random(this.speedMin, this.speedMax);
let launchAngle;
if (this._angle !== undefined) {
launchAngle = this._angle + random(-this._spread / 2, this._spread / 2);
} else {
launchAngle = random(TWO_PI);
}
this.vel = createVector(
cos(launchAngle) * speed,
sin(launchAngle) * speed
);
// Rotation state — randomized each spawn for square and star particles
this._rot = random(TWO_PI);
this._rotVel = random(this.rotSpeedMin, this.rotSpeedMax);
}//_init
// ── Physics ───────────────────────────────────────────────────────────
/**
* Advances the particle physics one frame.
* Applies drag to velocity, adds gravitational acceleration,
* moves the position, and decrements alpha.
* Call once per frame before draw().
*/
update() {
this.vel.mult(this.drag);
this.vel.add(this.acc);
this.pos.add(this.vel);
this.alpha -= this.alphaDecay;
this._rot += this._rotVel;
}//end update
// ── Rendering ─────────────────────────────────────────────────────────
/**
* Draws the particle at its current canvas pixel position.
* Requires p5.js colorMode(HSB, 360, 100, 100, 100).
*/
draw() {
const safeAlpha = Math.max(0, this.alpha);
const pColor = new SWColor(this.hue, this.saturation, this.brightness, safeAlpha);
const r = this.size / 2;
if (this._resolvedShape === 'square') {
push();
colorMode(HSB, 360, 100, 100, 100);
fill(this.hue, this.saturation, this.brightness, safeAlpha);
noStroke();
translate(this.pos.x, this.pos.y);
rotate(this._rot);
rectMode(CENTER);
rect(0, 0, this.size, this.size);
pop();
} else if (this._resolvedShape === 'star') {
push();
colorMode(HSB, 360, 100, 100, 100);
fill(this.hue, this.saturation, this.brightness, safeAlpha);
noStroke();
translate(this.pos.x, this.pos.y);
rotate(this._rot);
SWParticle._drawStar(0, 0, r * 0.45, r, this._starPoints);
pop();
} else {
// Default: circle
const pt = new SWPoint(this.pos.x, this.pos.y);
const disk = new SWDisk(pt, r, 0, pColor);
disk.setShowCenter(false);
disk.draw();
}
}//end draw
/**
* Draws the particle with its position converted from pixel coordinates
* to user/grid coordinates via the provided SWGrid.
*
* Because particles move in pixel space, this method uses grid.screenToUser()
* to find the current user-space position, then converts the pixel-sized radius
* to user units via grid.xScale before rendering.
*
* @param {SWGrid} grid
*/
drawOnGrid(grid) {
const safeAlpha = Math.max(0, this.alpha);
const pColor = new SWColor(this.hue, this.saturation, this.brightness, safeAlpha);
// Convert pixel position to user coordinates
const uc = grid.screenToUser(this.pos.x, this.pos.y);
// Convert pixel radius to user units using the grid's horizontal scale
const rUser = (this.size / 2) / grid.xScale;
if (this._resolvedShape === 'square') {
const screenPt = grid.userToScreen(uc.x, uc.y);
push();
colorMode(HSB, 360, 100, 100, 100);
fill(this.hue, this.saturation, this.brightness, safeAlpha);
noStroke();
translate(screenPt.x, screenPt.y);
rotate(this._rot);
rectMode(CENTER);
rect(0, 0, this.size, this.size);
pop();
} else if (this._resolvedShape === 'star') {
const screenPt = grid.userToScreen(uc.x, uc.y);
push();
colorMode(HSB, 360, 100, 100, 100);
fill(this.hue, this.saturation, this.brightness, safeAlpha);
noStroke();
translate(screenPt.x, screenPt.y);
rotate(this._rot);
SWParticle._drawStar(0, 0, rUser * 0.45 * grid.xScale, rUser * grid.xScale, this._starPoints);
pop();
} else {
const pt = new SWPoint(uc.x, uc.y);
const disk = new SWDisk(pt, rUser, 0, pColor);
disk.setShowCenter(false);
disk.drawOnGrid(grid);
}
}//end drawOnGrid
// ── Lifecycle ─────────────────────────────────────────────────────────
/**
* Returns true when the particle has fully faded out (alpha ≤ 0).
* Check this after update() and remove dead particles from your array.
* @returns {boolean}
*/
isDead() {
return this.alpha <= 0;
}//end isDead
/**
* Alias for isDead(). Provided for backward compatibility with sketches
* that used the original Particle class from Crosby's Bomb Saga.
* @returns {boolean}
*/
finished() {
return this.isDead();
}//end finished
/**
* Respawns the particle at a new position with fresh random velocity,
* size, hue, and full alpha. All physics constants (gravity, drag, etc.)
* are preserved from construction.
* @param {number} [x] - new spawn x in pixel coords (defaults to original spawn x)
* @param {number} [y] - new spawn y in pixel coords (defaults to original spawn y)
*/
reset(x, y) {
this._init(
x !== undefined ? x : this._spawnX,
y !== undefined ? y : this._spawnY
);
}//end reset
// ── Setters ───────────────────────────────────────────────────────────
/**
* Set gravity (downward acceleration added to velocity each frame, in pixels).
* @param {number} g
*/
setGravity(g) {
this.gravity = g;
this.acc = createVector(0, g);
}//end setGravity
/**
* Set drag (velocity multiplier applied each frame; 0 = instant stop, 1 = no drag).
* @param {number} d
*/
setDrag(d) {
this.drag = d;
}//end setDrag
/**
* Set the alpha decay rate (alpha units lost per frame on 0–100 scale).
* Higher values make particles disappear faster.
* @param {number} a
*/
setAlphaDecay(a) {
this.alphaDecay = a;
}//end setAlphaDecay
/**
* Set the HSB hue range. New particles spawned after this call will use the updated range.
* @param {number} minHue — 0–360
* @param {number} maxHue — 0–360
*/
setHueRange(minHue, maxHue) {
this.hueMin = minHue;
this.hueMax = maxHue;
}//end setHueRange
/**
* Set the particle diameter range in pixels.
* @param {number} minSize
* @param {number} maxSize
*/
setSizeRange(minSize, maxSize) {
this.sizeMin = minSize;
this.sizeMax = maxSize;
}//end setSizeRange
/**
* Set the initial speed range in pixels/frame.
* @param {number} minSpeed
* @param {number} maxSpeed
*/
setSpeedRange(minSpeed, maxSpeed) {
this.speedMin = minSpeed;
this.speedMax = maxSpeed;
}//end setSpeedRange
/**
* Fix the launch direction. Pass undefined as angle to restore full random direction.
* @param {number|undefined} angle - launch angle in radians (undefined = random)
* @param {number} [spread=TWO_PI] - angular spread in radians around angle
*/
setDirectionAngle(angle, spread = TWO_PI) {
this._angle = angle;
this._spread = spread;
}//end setDirectionAngle
/**
* Set the particle shape.
* @param {string} shape - 'circle' | 'square' | 'star' | 'mix'
*/
setShape(shape) {
this.shape = shape;
}//end setShape
/**
* Set the star point count range. Used when shape is 'star' or 'mix'.
* Values are clamped to [3, 9].
* @param {number} minPts - minimum number of star points
* @param {number} maxPts - maximum number of star points
*/
setStarPointsRange(minPts, maxPts) {
this.starPointsMin = Math.max(3, Math.min(9, minPts));
this.starPointsMax = Math.max(3, Math.min(9, maxPts));
}//end setStarPointsRange
/**
* Set the rotation speed range for square and star particles (radians/frame).
* Each spawned square or star picks a random speed in [minSpeed, maxSpeed].
* Negative values rotate counter-clockwise; positive values rotate clockwise.
* Circles are symmetric and ignore rotation.
* @param {number} minSpeed — most counter-clockwise speed (e.g. -0.3)
* @param {number} maxSpeed — most clockwise speed (e.g. 0.3)
*/
setRotationSpeed(minSpeed, maxSpeed) {
this.rotSpeedMin = minSpeed;
this.rotSpeedMax = maxSpeed;
}//end setRotationSpeed
// ── Static helper ─────────────────────────────────────────────────────
/**
* Draws a star polygon centered at (cx, cy) using p5.js beginShape/vertex.
* Caller is responsible for setting fill/stroke and any translate before calling.
* @param {number} cx - center x
* @param {number} cy - center y
* @param {number} innerRadius
* @param {number} outerRadius
* @param {number} npoints - number of points
*/
static _drawStar(cx, cy, innerRadius, outerRadius, npoints) {
const angle = TWO_PI / npoints;
const halfAngle = angle / 2;
beginShape();
for (let a = -HALF_PI; a < TWO_PI - HALF_PI; a += angle) {
vertex(cx + cos(a) * outerRadius, cy + sin(a) * outerRadius);
vertex(cx + cos(a + halfAngle) * innerRadius, cy + sin(a + halfAngle) * innerRadius);
}
endShape(CLOSE);
}//end _drawStar
}//end class SWParticle