Polygon
Flocking Particles Create Geometric Art
Polygon is a generative art piece that combines flocking behavior with geometric visualization. Particles drift across the screen following Perlin noise and each other, connecting to nearby neighbors to form translucent polygonal shapes that shift and evolve over time.
A special thanks to Daniel Shiffman! Your incredible Youtube channel and book Nature of Code inspired me to create this piece along side many others that can be found on my old website. You taught me how to code! Thank you!
The Concept
The idea combines two classic algorithms: Perlin noise flow fields and Craig Reynolds' flocking behaviors. Particles are pushed by noise while simultaneously trying to stay together, align with neighbors, and avoid collisions.
The visual effect comes from drawing polygons between nearby particles. As the flock moves, the polygons constantly reshape, creating an organic, almost biological appearance.
Particle Properties
Each particle has a set of properties that control its behavior:
Particle Class
class Particle {
constructor() {
// Spawn off-screen to the left
this.pos = createVector(random(-100, -10), random(height));
this.vel = createVector(0, 0);
this.acc = createVector(0, 0);
// Random size affects behavior
this.size = random(minSize, maxSize);
// Larger particles see further
this.sight = map(this.size, minSize, maxSize, 50, 100);
this.buddies = []; // Nearby particles
this.maxSpeed = 1;
this.maxForce = 0.5;
}
}
Notice that sight is derived from size - larger particles can "see" further,
giving them more connections and making them more influential in the flock.
Perlin Noise Flow
Each particle is pushed by a force derived from Perlin noise. The noise value is sampled based on the particle's horizontal position, creating bands of similar flow:
Noise-Based Movement
function draw() {
zOff += 0.5; // Animate noise over time
for (let particle of particles) {
// Sample noise based on x position
let xOff = particle.pos.x / width;
let xNoise = noise(xOff, zOff);
// Apply horizontal force
particle.applyForce(createVector(xNoise, 0));
}
}
The zOff variable changes over time, causing the noise field to evolve and
preventing particles from settling into static patterns.
Flocking Behavior
The flocking algorithm has three components, each producing a steering force:
1. Separation
Avoid crowding nearby particles. If another particle is within 2 × size,
steer away from it. Closer particles create stronger repulsion.
Separation Force
separate(others) {
var desiredSeparation = this.size * 2;
var sum = createVector();
var count = 0;
for (var other of others) {
var distance = p5.Vector.dist(this.pos, other.pos);
if (distance > 0 && distance < desiredSeparation) {
// Vector pointing away from neighbor
var opposite = p5.Vector.sub(this.pos, other.pos);
opposite.normalize();
opposite.div(distance); // Closer = stronger
sum.add(opposite);
count++;
}
}
// Average and convert to steering force
if (count > 0) {
sum.div(count);
sum.setMag(this.maxSpeed);
return p5.Vector.sub(sum, this.vel).limit(this.maxForce);
}
return createVector();
}
2. Alignment
Steer toward the average heading of neighbors within sight × 2 range.
This keeps the flock moving together.
3. Cohesion
Steer toward the average position of nearby particles within a close range. This pulls stragglers back toward the group.
Combining Forces
flock() {
var sep = this.separate(this.buddies);
var ali = this.align(this.buddies);
var coh = this.cohesion(this.buddies);
// Weight the behaviors
sep.mult(3); // Strong separation
ali.mult(4); // Strongest alignment
coh.mult(1.5); // Moderate cohesion
this.applyForce(sep);
this.applyForce(ali);
this.applyForce(coh);
}
Mass and Force
Larger particles have more "mass" and respond slower to forces. This is implemented by scaling forces inversely with size:
Mass-Based Force Application
applyForce(force) {
let f = force.copy();
// Larger particles are heavier (smaller multiplier)
let m = map(this.size, minSize, maxSize, 0.005, 0.001);
f.mult(m);
this.acc.add(f);
}
This creates a natural hierarchy where small particles zip around quickly while large particles drift slowly, anchoring the flock.
Polygon Visualization
The visual magic happens in the show() function. Each particle draws a
polygon connecting itself to all nearby "buddies":
Drawing Polygons
show() {
if (this.buddies.length > 0) {
stroke(220, 220, 220, 10); // Very transparent
fill(220, 220, 220, 15);
beginShape();
vertex(this.pos.x, this.pos.y);
for (let bud of this.buddies) {
vertex(bud.pos.x, bud.pos.y);
}
vertex(this.pos.x, this.pos.y); // Close the shape
endShape();
}
}
The extremely low alpha values (10-15 out of 255) create the layered, glowing effect as many overlapping polygons accumulate brightness.
Particle Lifecycle
Particles spawn from the left edge and drift rightward. When they exit the screen, they're removed and replaced:
Spawning and Removal
// Remove particles that exit the screen
if (particle.edge()) {
particles.splice(i, 1);
}
// Maintain population
if (particles.length < population) {
let dif = population - particles.length;
for (let i = 0; i < dif; i++) {
particles.push(new Particle());
}
}
This creates a continuous flow from left to right, with the flock constantly reforming as particles enter and exit.
Key Parameters
| Parameter | Value | Effect |
|---|---|---|
population |
100 | Number of particles on screen |
minSize / maxSize |
5 / 20 | Range of particle sizes |
sight |
50-100 | Connection range (based on size) |
maxSpeed |
1 | Maximum velocity |
maxForce |
0.5 | Maximum steering force |
Technical Details
- Framework: p5.js
- Particles: 100 simultaneous
- Frame Rate: 60 FPS target
- Rendering: Additive blending via low-alpha shapes