🔬 This code teleports particles to the opposite edge when they leave the canvas. What happens if you replace the teleport with a random respawn, like this.pos = createVector(random(width), random(height))?
if (this.pos.x > width) this.pos.x = 0;
if (this.pos.x < 0) this.pos.x = width;
if (this.pos.y > height) this.pos.y = 0;
if (this.pos.y < 0) this.pos.y = height;
🔬 Right now noise output maps to a FULL circle of directions (0 to TWO_PI). What do you think happens to the flow pattern if you map it to only half a circle, 0 to PI, so particles can never point 'backwards'?
let angle = noise(
this.pos.x * noiseScale + noiseOffset,
this.pos.y * noiseScale + noiseOffset
);
// Map the noise value (0 to 1) to a full circle (0 to 2π radians)
angle = map(angle, 0, 1, 0, TWO_PI);
class Particle {
constructor() {
// Random initial position within the canvas
this.pos = createVector(random(width), random(height));
this.vel = createVector(0, 0); // Initial velocity
this.acc = createVector(0, 0); // Initial acceleration
this.maxSpeed = 4; // Maximum speed of the particle
}
update() {
// Calculate the noise value at the particle's current position
// We add noiseOffset to the x and y coordinates to make the noise field evolve
let angle = noise(
this.pos.x * noiseScale + noiseOffset,
this.pos.y * noiseScale + noiseOffset
);
// Map the noise value (0 to 1) to a full circle (0 to 2π radians)
angle = map(angle, 0, 1, 0, TWO_PI);
// Create a force vector based on the angle
let force = p5.Vector.fromAngle(angle);
// Scale the force by the flowStrength
force.mult(flowStrength);
// Apply the force to the particle's acceleration
this.acc.add(force);
// Update velocity and position
this.vel.add(this.acc);
this.vel.limit(this.maxSpeed); // Limit the velocity to prevent particles from moving too fast
this.pos.add(this.vel);
// Reset acceleration for the next frame
this.acc.mult(0);
// Wrap particles around the canvas edges
if (this.pos.x > width) this.pos.x = 0;
if (this.pos.x < 0) this.pos.x = width;
if (this.pos.y > height) this.pos.y = 0;
if (this.pos.y < 0) this.pos.y = height;
}
show() {
// Set stroke weight and color
strokeWeight(1);
noFill();
// Map the particle's x position to a hue value (0-255)
// This creates the colorful trails
let hue = map(this.pos.x, 0, width, 0, 255);
// Set stroke color with hue, high saturation, high brightness, and semi-transparent alpha
stroke(hue, 200, 255, 100);
// Draw a short line segment in the direction of the particle's velocity
// This creates a more visible trail than just a point
line(this.pos.x, this.pos.y, this.pos.x - this.vel.x, this.pos.y - this.vel.y);
}
}
Line-by-line explanation (16 lines)
🔧 Subcomponents:
calculation
Constructor / Initial State
this.pos = createVector(random(width), random(height));
Gives each new particle a random starting position and zeroed velocity/acceleration vectors
calculation
Noise-to-Angle Conversion
angle = map(angle, 0, 1, 0, TWO_PI);
Converts the raw 0-1 noise value sampled at the particle's position into a full-circle direction angle
conditional
Edge Wrapping
if (this.pos.x > width) this.pos.x = 0;
Teleports particles that leave one edge of the canvas back to the opposite edge, keeping them on screen forever
this.pos = createVector(random(width), random(height));
- Creates a p5.Vector for position and gives the particle a random spot anywhere on the canvas.
this.vel = createVector(0, 0); // Initial velocity
- Starts the particle with zero velocity - it will speed up gradually as forces are applied.
this.maxSpeed = 4; // Maximum speed of the particle
- Stores a speed cap unique to this particle, used later to prevent runaway acceleration.
let angle = noise(
this.pos.x * noiseScale + noiseOffset,
this.pos.y * noiseScale + noiseOffset
);
- Samples Perlin noise using the particle's scaled position (plus the global time offset) as coordinates, returning a smooth value between 0 and 1 that's similar for nearby points.
angle = map(angle, 0, 1, 0, TWO_PI);
- Rescales that 0-1 noise value into a full rotation (0 to 2π radians) so it can be used as a direction.
let force = p5.Vector.fromAngle(angle);
- Builds a unit-length vector pointing in that direction using p5's built-in vector helper.
force.mult(flowStrength);
- Scales the force vector's magnitude by flowStrength, making the push stronger or weaker.
this.acc.add(force);
- Adds the flow force to the particle's acceleration for this frame.
this.vel.add(this.acc);
- Integrates acceleration into velocity - the standard 'accelerate then move' physics pattern.
this.vel.limit(this.maxSpeed); // Limit the velocity to prevent particles from moving too fast
- Clamps the velocity vector's length so particles never exceed maxSpeed, keeping motion controlled.
this.pos.add(this.vel);
- Integrates velocity into position, actually moving the particle.
this.acc.mult(0);
- Resets acceleration to zero so forces don't accumulate forever across frames.
if (this.pos.x > width) this.pos.x = 0;
- If the particle drifts off the right edge, it reappears on the left edge, creating an infinite wrap-around canvas.
let hue = map(this.pos.x, 0, width, 0, 255);
- Maps the particle's horizontal position to a hue value, so color shifts smoothly from left to right across the screen.
stroke(hue, 200, 255, 100);
- Sets the line color using HSB with high saturation and brightness, and partial transparency (100 out of 255 alpha) so trails glow rather than look solid.
line(this.pos.x, this.pos.y, this.pos.x - this.vel.x, this.pos.y - this.vel.y);
- Draws a short line from the particle's current position back to where it was roughly one frame ago, visualizing both its position and direction of travel.