starter thing

This sketch fills the screen with 70 glowing, color-shifting particles that drift, connect into thin constellation lines when they're close together, and leave soft trails behind thanks to a translucent background. Moving the mouse gently tugs nearby particles toward the pointer, and clicking or tapping bursts fresh particles into the scene.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make trails last longer — Lowering the background's alpha value makes previous frames fade out more slowly, producing long streaky trails behind every particle.
  2. Widen the constellation network — Increasing LINK_DIST lets particles connect across much larger distances, turning the scene into a dense web instead of a sparse constellation.
  3. Supercharge the click burst — Spawning more particles per frame while clicking creates a much denser, more explosive burst of stars from the cursor.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a field of soft, glowing particles that drift across the canvas, cycle through hues, and draw faint lines between any two particles that happen to be close together, forming shifting constellations. A translucent background call creates gentle motion trails instead of hard erasing, mouse proximity gently pulls particles toward the cursor, and clicking bursts new particles into existence. It leans on p5.js techniques like HSB color mode, the dist() function for proximity checks, object literals to represent each particle, and nested for...of loops to compare every particle against every other one.

The code is organized around a simple particle-system pattern: a spawn() factory function builds a fresh particle object, setup() creates an initial batch of them, and draw() updates, connects, redraws, and eventually recycles each one every single frame. Studying it will teach you how to manage an array of objects over time, how translucent backgrounds fake motion trails cheaply, how default parameters make a helper function flexible, and how an O(n²) neighbor-comparison loop can produce a surprisingly organic constellation effect.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas, switches to HSB color mode so hues can be cycled smoothly, and calls spawn() 70 times to fill the particles array with randomly placed dots that each have their own velocity, size, hue, and lifespan.
  2. Every frame, draw() paints a slightly transparent background over the previous frame instead of fully clearing it, which is what produces the soft trailing streaks behind each particle.
  3. For each particle, its position is updated using its velocity, its hue drifts slightly, and its remaining lifespan counts down; if the mouse is within 140 pixels it also gets nudged gently toward the pointer.
  4. If a particle runs out of life or drifts off the edge of the canvas, Object.assign() overwrites it in place with a brand new particle from spawn(), so the array size stays constant while particles are endlessly recycled.
  5. Still inside the same loop, a nested for...of loop measures the distance from the current particle to every other particle and draws a thin line whenever two particles are closer than LINK_DIST, creating the constellation effect, before the particle itself is drawn as a pulsing circle.
  6. Finally, while the mouse button is held down, draw() pushes three new particles near the mouse position into the array every frame and trims the array back down to 220 particles if it grows too large, so bursts feel continuous without the sketch slowing to a crawl.

🎓 Concepts You'll Learn

Particle systems with plain objectsHSB color mode and hue cyclingTranslucent backgrounds for motion trailsdist() for proximity/collision-style checksDefault parameters in functionsNested for...of loops for pairwise comparisonObject.assign() for recycling objectsMouse interaction (mouseX, mouseY, mouseIsPressed)

📝 Code Breakdown

setup()

setup() runs once when the sketch first loads. It's the ideal place to configure the canvas, set drawing modes like colorMode(), and populate arrays with starting data before the animation loop takes over.

🔬 This loop decides how many particles exist at the start. What happens visually if you drop it to 10? What if you push it up to 400 - does the sketch still run smoothly?

  for (let i = 0; i < 70; i++) particles.push(spawn());
function setup() {
  createCanvas(windowWidth, windowHeight);
  colorMode(HSB, 360, 100, 100, 100);
  noStroke();
  for (let i = 0; i < 70; i++) particles.push(spawn());
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

for-loop Initial Particle Creation for (let i = 0; i < 70; i++) particles.push(spawn());

Fills the particles array with 70 freshly spawned particle objects before the sketch starts animating

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, using the window's current width and height.
colorMode(HSB, 360, 100, 100, 100);
Switches p5's color system from the default RGB to HSB (Hue, Saturation, Brightness, Alpha), with hue ranging 0-360 and the other values 0-100. This makes it easy to smoothly cycle colors by just changing a single hue number.
noStroke();
Turns off shape outlines by default so circles are drawn as solid filled dots unless stroke() is turned back on later (as it is for the constellation lines).
for (let i = 0; i < 70; i++) particles.push(spawn());
Runs 70 times, each time calling spawn() to create a new particle object and pushing it into the particles array.

spawn()

spawn() acts like a lightweight 'constructor' function. Instead of writing an ES6 class, it just returns a fresh object literal each time it's called, which is a common lightweight pattern for particle systems in p5.js.

function spawn(x = random(width), y = random(height)) {
  return {
    x, y,
    vx: random(-0.8, 0.8), vy: random(-0.8, 0.8),
    size: random(3, 10), hue: random(360),
    life: random(200, 500)
  };
}
Line-by-line explanation (6 lines)
function spawn(x = random(width), y = random(height)) {
Declares spawn() with default parameter values. If you call spawn() with no arguments, x and y default to random positions anywhere on the canvas; if you pass in coordinates (like near the mouse), those are used instead.
return {
Builds and returns a plain JavaScript object literal representing one particle - this is the 'class-free' way p5 sketches often model simple entities.
x, y,
Shorthand object property syntax - stores the particle's position using the x and y values passed in (or defaulted).
vx: random(-0.8, 0.8), vy: random(-0.8, 0.8),
Gives the particle a small random horizontal and vertical velocity, so particles drift slowly in slightly different directions.
size: random(3, 10), hue: random(360),
Assigns a random diameter between 3 and 10 pixels, and a random starting hue between 0 and 360 (matching the HSB color mode set in setup()).
life: random(200, 500)
Gives the particle a random lifespan in frames; once life counts down to zero in draw(), the particle gets recycled into a new one.

draw()

draw() runs continuously at roughly 60 frames per second and is where nearly all of this sketch's behavior lives: motion, color cycling, lifespan management, proximity-based line drawing, and mouse interaction all happen inside this single loop over the particles array.

🔬 This inner loop draws a line to every nearby particle. What happens if you remove the dq > 0 check - can you predict why every particle would suddenly connect to itself?

    for (let q of particles) {
      let dq = dist(p.x, p.y, q.x, q.y);
      if (dq > 0 && dq < LINK_DIST) line(p.x, p.y, q.x, q.y);
    }

🔬 This is the mouse-pull effect. What happens if you change 0.0004 to a much bigger number like 0.01 - do particles start orbiting or slingshotting past the cursor?

    let d = dist(p.x, p.y, mouseX, mouseY);
    if (d < 140) {
      p.vx += (mouseX - p.x) * 0.0004;
      p.vy += (mouseY - p.y) * 0.0004;
    }
function draw() {
  background(250, 20, 10, 28); // translucent = soft motion trails
  hueShift += 0.4;

  for (let p of particles) {
    p.x += p.vx; p.y += p.vy;
    p.hue = (p.hue + 0.4) % 360;
    p.life--;

    // gentle pull toward the pointer so touching feels alive
    let d = dist(p.x, p.y, mouseX, mouseY);
    if (d < 140) {
      p.vx += (mouseX - p.x) * 0.0004;
      p.vy += (mouseY - p.y) * 0.0004;
    }

    if (p.life <= 0 || p.x < -20 || p.x > width + 20 || p.y < -20 || p.y > height + 20) {
      Object.assign(p, spawn());
    }

    // constellation lines to close neighbors
    stroke((p.hue + hueShift) % 360, 45, 75, 12);
    strokeWeight(0.6);
    for (let q of particles) {
      let dq = dist(p.x, p.y, q.x, q.y);
      if (dq > 0 && dq < LINK_DIST) line(p.x, p.y, q.x, q.y);
    }
    noStroke();

    fill(p.hue, 65, 95, min(map(p.life, 0, 120, 0, 70), 70));
    circle(p.x, p.y, p.size * (1 + sin(frameCount * 0.05 + p.size) * 0.25));
  }

  // press or tap to burst new particles from your finger
  if (mouseIsPressed) {
    for (let i = 0; i < 3; i++) {
      particles.push(spawn(mouseX + random(-12, 12), mouseY + random(-12, 12)));
    }
    while (particles.length > 220) particles.shift();
  }
}
Line-by-line explanation (19 lines)

🔧 Subcomponents:

calculation Trail Background background(250, 20, 10, 28); // translucent = soft motion trails

Draws a mostly-transparent background over the previous frame instead of erasing it, which leaves faint trails behind moving particles

for-loop Particle Update Loop for (let p of particles) {

Iterates over every particle to update its position, color, lifespan, and draw it

conditional Mouse Pull Check if (d < 140) {

Nudges the particle's velocity toward the mouse if it's within 140 pixels

conditional Recycle Dead/Offscreen Particle if (p.life <= 0 || p.x < -20 || p.x > width + 20 || p.y < -20 || p.y > height + 20) {

Checks if a particle has expired or drifted off-canvas, and replaces its data with a freshly spawned particle

for-loop Neighbor Connection Loop for (let q of particles) {

Compares the current particle against every other particle and draws a line if they're close enough, building the constellation effect

conditional Mouse Press Burst if (mouseIsPressed) {

While the mouse is held down, spawns 3 new particles per frame near the cursor and trims the array so it doesn't grow forever

background(250, 20, 10, 28); // translucent = soft motion trails
Paints a dark, slightly pink-hued background with only 28 out of 100 alpha (opacity), so it doesn't fully cover the previous frame - this is what creates the soft trailing streaks behind each particle.
hueShift += 0.4;
Increments a global variable that slowly shifts the color of the constellation lines over time, independent of each particle's own hue.
p.x += p.vx; p.y += p.vy;
Moves the particle by adding its velocity to its position - the core of all motion in this sketch.
p.hue = (p.hue + 0.4) % 360;
Slowly rotates the particle's hue value, wrapping back to 0 after passing 360 thanks to the modulo operator, so colors cycle endlessly.
p.life--;
Counts down the particle's remaining lifespan by one frame.
let d = dist(p.x, p.y, mouseX, mouseY);
Calculates the straight-line distance between the particle and the mouse cursor using p5's dist() function.
if (d < 140) {
Only applies the pull effect if the particle is within 140 pixels of the mouse.
p.vx += (mouseX - p.x) * 0.0004;
Nudges the particle's horizontal velocity toward the mouse - the difference between mouseX and the particle's x position, scaled down to a tiny fraction so the pull feels gentle.
if (p.life <= 0 || p.x < -20 || p.x > width + 20 || p.y < -20 || p.y > height + 20) {
Checks whether the particle has run out of life OR drifted more than 20 pixels past any edge of the canvas.
Object.assign(p, spawn());
Copies all the properties of a brand new spawn() object onto the existing particle object p, effectively 'reincarnating' it in place without needing to remove and re-add it to the array.
stroke((p.hue + hueShift) % 360, 45, 75, 12);
Sets the line color for constellation connections, mixing the particle's own hue with the slowly shifting global hueShift, at low saturation/brightness and very low alpha so lines look faint and glowy.
for (let q of particles) {
Loops through every other particle to check how close it is to the current particle p - this nested loop is what creates the constellation network.
let dq = dist(p.x, p.y, q.x, q.y);
Measures the distance between the current particle and this other particle q.
if (dq > 0 && dq < LINK_DIST) line(p.x, p.y, q.x, q.y);
Draws a line between the two particles only if they're close enough (less than LINK_DIST) but not the exact same particle (dq > 0 skips comparing a particle to itself).
fill(p.hue, 65, 95, min(map(p.life, 0, 120, 0, 70), 70));
Sets the fill color for the particle's dot using its hue, with an alpha that fades out as life approaches zero (map() converts remaining life into an opacity value, capped at 70).
circle(p.x, p.y, p.size * (1 + sin(frameCount * 0.05 + p.size) * 0.25));
Draws the particle as a circle whose diameter gently pulses over time using a sine wave based on frameCount, so particles appear to softly breathe in and out.
if (mouseIsPressed) {
Checks if the mouse button is currently held down (or the screen is being touched).
particles.push(spawn(mouseX + random(-12, 12), mouseY + random(-12, 12)));
Creates a new particle near the mouse position (with a little random scatter) and adds it to the particles array.
while (particles.length > 220) particles.shift();
Keeps the particles array from growing forever by removing the oldest particle from the front of the array whenever there are more than 220.

windowResized()

windowResized() is a built-in p5.js callback function you can optionally define to make your sketch responsive to browser resizing, which is especially important for full-window sketches like this one.

function windowResized() { resizeCanvas(windowWidth, windowHeight); }
Line-by-line explanation (1 lines)
function windowResized() { resizeCanvas(windowWidth, windowHeight); }
p5.js automatically calls windowResized() whenever the browser window changes size. Calling resizeCanvas() here keeps the canvas matching the new window dimensions so the sketch always fills the screen.

📦 Key Variables

particles array

Holds every particle object currently active in the sketch; each frame the code loops over this array to update, connect, and draw particles.

let particles = [];
hueShift number

A slowly increasing value added to each particle's hue when coloring the constellation lines, giving the connecting lines their own independent color drift separate from the dots.

let hueShift = 0;
LINK_DIST number

The maximum distance (in pixels) between two particles for a constellation line to be drawn between them; a constant that controls how connected or sparse the network looks.

const LINK_DIST = 90; // how close two particles must be to connect

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE draw() constellation nested loop

For every particle, the code loops over the entire particles array again to check distances, giving an O(n²) comparison every single frame. With the array able to grow to 220 particles, that's up to ~48,000 distance checks per frame, which can slow the frame rate on less powerful devices.

💡 Only compare each pair once (e.g. loop q from the current particle's index onward instead of from the start) to halve the work, or use a spatial grid / quadtree to only check particles in nearby cells instead of every other particle.

BUG draw() burst cleanup

while (particles.length > 220) particles.shift(); removes particles from the front of the array using shift(), which is an O(n) operation because every remaining element has to be re-indexed. Doing this repeatedly while the mouse is held down adds unnecessary overhead.

💡 Use particles.splice(0, particles.length - 220) once to trim excess particles in a single operation, or use pop() to remove from the end instead if order doesn't matter.

STYLE Global scope

Several 'magic numbers' (140 for mouse pull radius, 0.0004 for pull strength, 220 for max particles, 20 for offscreen margin) are hardcoded directly inside draw(), making them harder to find and tune.

💡 Pull these into named constants near the top of the file (like LINK_DIST already is), e.g. const PULL_RADIUS = 140; const PULL_STRENGTH = 0.0004; const MAX_PARTICLES = 220;, so they're easy to locate and adjust.

FEATURE spawn() / particle object

Every particle currently uses the same size range, velocity range, and life range, so bursts from clicking look visually identical to the ambient particles.

💡 Pass extra options into spawn() (like a shorter life or a burst of outward velocity) when creating particles from a mouse click, so clicked bursts feel distinct from the ambient drifting particles - for example giving them an initial velocity pointing away from the mouse for a firework-like effect.

🔄 Code Flow

Code flow showing setup, spawn, draw, windowresized

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> setup-init-loop[Initial Particle Creation] setup-init-loop --> draw[draw loop] click setup href "#fn-setup" click setup-init-loop href "#sub-setup-init-loop" draw --> translucent-bg[Trail Background] translucent-bg --> main-particle-loop[Particle Update Loop] main-particle-loop --> mouse-pull-check[Mouse Pull Check] main-particle-loop --> respawn-check[Recycle Dead/Offscreen Particle] main-particle-loop --> constellation-nested-loop[Neighbor Connection Loop] main-particle-loop --> burst-conditional[Mouse Press Burst] mouse-pull-check --> main-particle-loop respawn-check --> main-particle-loop constellation-nested-loop --> main-particle-loop burst-conditional --> main-particle-loop click draw href "#fn-draw" click translucent-bg href "#sub-translucent-bg" click main-particle-loop href "#sub-main-particle-loop" click mouse-pull-check href "#sub-mouse-pull-check" click respawn-check href "#sub-respawn-check" click constellation-nested-loop href "#sub-constellation-nested-loop" click burst-conditional href "#sub-burst-conditional" draw --> draw windowresized[windowResized] --> draw click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What does this p5.js sketch create visually?

This sketch generates glowing, shifting dots that drift across the screen, forming colorful constellations. As the particles move, they leave soft trails behind them, creating a dynamic and visually engaging experience.

How can users interact with this p5.js sketch?

Users can interact with the sketch by moving their mouse to gently pull the particles towards the cursor. Additionally, clicking or tapping on the canvas bursts new stars into the scene, adding to the visual display.

What creative coding technique does this p5.js sketch demonstrate?

This sketch demonstrates a particle system, where individual particles are created and manipulated based on user interaction and physics. The particles exhibit behaviors like drifting, connecting into constellations, and responding to mouse movements.

How could someone recreate a similar effect in p5.js?

To recreate a similar effect, you can use a particle system where each particle has properties like position, velocity, size, and color. Implement movement, connection logic between close particles, and allow for user interaction to spawn new particles using mouse events.

Preview

starter thing - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of starter thing - Code flow showing setup, spawn, draw, windowresized
Code Flow Diagram