five hundred and twenty fourth to come

This sketch creates a mesmerizing swirl of colorful, steam-like particles that drift across a dark canvas using Perlin noise-driven flow fields. Touch or drag anywhere on screen to push and shape the mist in real time, creating an interactive art piece.

🧪 Try This!

Experiment with the code by making these changes:

  1. Add warm colors to the palette — The particleHues array controls which colors particles can be—add orange and red values to see warm tones appear in the mist.
  2. Increase particle density by 50% — More particles create denser, more detailed mist—increase numParticles to see a thicker steam effect.
  3. Make touch repulsion stronger — Increase touchInfluenceForce to see particles scatter more dramatically when you drag your finger across the screen.
  4. Create slower, broader swirls — Decrease flowFieldSpeed and increase flowFieldScale to see larger, slower-moving vortexes in the mist.
  5. Make particles nearly invisible — Decrease the alpha values in the particle objects to create a more subtle, ghostly effect.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch animates thousands of glowing particles that swirl and flow like steam or mist across the screen. The particles follow a Perlin noise-driven flow field that constantly evolves, giving them an organic, natural-looking motion. When you touch or drag on the screen, the particles react by pushing away from your finger, letting you paint and swirl the mist in real time. It combines several powerful p5.js techniques: particle arrays, Perlin noise for flow fields, color palettes with HSB color mode, and interactive touch detection.

The code is organized around a particles array that stores hundreds of particle objects, each with position, velocity, color, and size. The setup() function initializes them, while draw() runs every frame to update positions using both flow field guidance and touch influence, then renders them. By studying this sketch you will learn how to build particle systems, use Perlin noise to create fluid motion, detect multi-touch input, and create responsive visuals that react to user interaction.

⚙️ How It Works

  1. When the sketch loads, setup() creates a dark canvas and initializes 2500 particle objects scattered randomly across it, each with a color from the custom palette (blues, purples, cyans) and a random size and transparency.
  2. Every frame, draw() paints a semi-transparent dark rectangle over the canvas to create fading trails, then loops through every particle to update its motion.
  3. For each particle, the code samples a 3D Perlin noise field at its position to get a flow angle, converting that angle into x and y velocity components that pull the particle in a swirling direction.
  4. The particle's velocity is smoothly interpolated (lerped) toward the flow velocity so motion feels fluid rather than jerky.
  5. If the mouse is pressed or the screen is touched, the code calculates the distance from each particle to every touch point and applies a repulsive force that pushes particles away—the closer to the touch, the stronger the push.
  6. Particles wrap around canvas edges so they never disappear, and finally each particle is drawn as a colored circle with its brightness and transparency, creating the glowing steam effect.

🎓 Concepts You'll Learn

Particle systems and arraysPerlin noise flow fieldsHSB color mode and palettesTouch and mouse interactionVector math (velocity, distance, angle)Interpolation (lerp) for smooth motionCanvas wrapping and edge behaviorTransparency and trail effects

📝 Code Breakdown

preload()

preload() runs once before setup(). Use it to load fonts, images, and sound files so they're ready when the sketch needs them.

function preload() {
  // Load the Roboto font using the reliable Fontsource CDN
  robotoFont = loadFont('https://unpkg.com/@fontsource/roboto@latest/files/roboto-latin-400-normal.woff');
}
Line-by-line explanation (1 lines)
robotoFont = loadFont('https://unpkg.com/@fontsource/roboto@latest/files/roboto-latin-400-normal.woff');
Loads a font file from a CDN before setup() runs—this ensures the custom font is ready to use in draw().

setup()

setup() runs once when the sketch starts. Use it to initialize the canvas, set color modes, and create initial objects. Here it creates the particle array that powers the entire sketch.

function setup() {
  // Create a canvas that fills the window, handling tablet landscape orientation
  createCanvas(windowWidth, windowHeight);
  textFont(robotoFont); // Set the font for text
  colorMode(HSB, 360, 100, 100, 100); // Use HSB color mode for consistent grayscale
  background(0, 0, 10); // Dark background
  noStroke(); // Particles will be drawn without an outline

  // Initialize particles with random positions, colors from our palette, and sizes
  for (let i = 0; i < numParticles; i++) {
    particles.push({
      x: random(width),
      y: random(height),
      vx: 0, // Initial velocity x
      vy: 0, // Initial velocity y
      hue: random(particleHues), // Assign a random hue from our custom palette
      brightness: random(80, 100), // Light gray to white for steam
      alpha: random(20, 50),     // Transparency for steam effect
      size: random(1, 3) // Random particle size
    });
  }
}
Line-by-line explanation (14 lines)

🔧 Subcomponents:

for-loop Particle Initialization Loop for (let i = 0; i < numParticles; i++) {

Creates 2500 particle objects with random positions and properties, populating the particles array

object-creation Particle Object Definition particles.push({

Defines the structure of each particle with position, velocity, color, and size properties

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, making the sketch responsive to different screen sizes.
textFont(robotoFont);
Tells p5.js to use the Roboto font (loaded in preload) for any text drawn later.
colorMode(HSB, 360, 100, 100, 100);
Switches to HSB (Hue, Saturation, Brightness) color mode with ranges 0–360, 0–100, 0–100, 0–100 for easier color control.
background(0, 0, 10);
Paints the initial background almost black (Hue 0, Saturation 0, Brightness 10) to create a dark canvas for the glowing particles.
noStroke();
Disables outlines on all shapes, so particles are drawn as solid circles without borders.
for (let i = 0; i < numParticles; i++) {
Loops 2500 times, creating one particle per iteration.
x: random(width),
Assigns a random x position anywhere from 0 to the canvas width.
y: random(height),
Assigns a random y position anywhere from 0 to the canvas height.
vx: 0,
Sets initial horizontal velocity to 0—the flow field will update this each frame.
vy: 0,
Sets initial vertical velocity to 0—the flow field will update this each frame.
hue: random(particleHues),
Picks a random hue from the particleHues array (210, 270, 180, 240, 300), ensuring particles are blue, purple, or cyan.
brightness: random(80, 100),
Picks a random brightness between 80 and 100, making particles glow brightly like steam.
alpha: random(20, 50),
Picks a random transparency level so some particles are slightly more or less visible, creating depth.
size: random(1, 3)
Picks a random size between 1 and 3 pixels, adding visual variety to the particle cloud.

draw()

draw() runs 60 times per second (by default). It updates every particle's motion based on the flow field and touch input, applies trails to create the ethereal effect, and renders the result. This is where all the animation happens.

🔬 These three lines are the heart of the flow field: noise() generates an angle, then cos/sin convert it to velocity. What happens if you remove the '* TWO_PI * 2' so the angle range shrinks to just 0–1?

    let angle = noise(p.x * flowFieldScale, p.y * flowFieldScale, frameCount * flowFieldSpeed) * TWO_PI * 2;
    let flowVx = cos(angle) * particleSpeed; // Convert angle to x velocity
    let flowVy = sin(angle) * particleSpeed; // Convert angle to y velocity

🔬 This block calculates the repulsion angle from touch to particle using atan2. What happens if you change 'p.y - t.y' to 't.y - p.y' (flip the subtraction)? The repulsion will point backward!

    for (let t of touchPoints) {
      let d = dist(p.x, p.y, t.x, t.y); // Distance from particle to touch
      if (d < touchInfluenceRadius && d > 0) { // If within influence radius and not at the exact touch point
        // Calculate angle from touch to particle
        let angleToTouch = atan2(p.y - t.y, p.x - t.x);
function draw() {
  // Create subtle trails by drawing a semi-transparent rectangle over the canvas
  // This gives the "steam" a fading, ethereal quality
  fill(0, 0, 10, 5); // Dark background, low alpha (5 out of 100)
  rect(0, 0, width, height); // Cover the entire canvas

  for (let p of particles) {
    // Get a vector from a Perlin noise field to guide the particle's movement
    // noise() takes x, y, and an optional third dimension (frameCount for animation)
    // The result is mapped to an angle (0 to TWO_PI * 2 for more dynamic flow)
    let angle = noise(p.x * flowFieldScale, p.y * flowFieldScale, frameCount * flowFieldSpeed) * TWO_PI * 2;
    let flowVx = cos(angle) * particleSpeed; // Convert angle to x velocity
    let flowVy = sin(angle) * particleSpeed; // Convert angle to y velocity

    // Smoothly interpolate the particle's velocity towards the flow field vector
    p.vx = lerp(p.vx, flowVx, 0.1);
    p.vy = lerp(p.vy, flowVy, 0.1);

    // React to touch/mouse input
    let touchPoints = [];
    if (mouseIsPressed) {
      touchPoints.push({ x: mouseX, y: mouseY }); // Add mouse as a touch point
    }
    for (let t of touches) {
      touchPoints.push({ x: t.x, y: t.y }); // Add all active touches
    }

    for (let t of touchPoints) {
      let d = dist(p.x, p.y, t.x, t.y); // Distance from particle to touch
      if (d < touchInfluenceRadius && d > 0) { // If within influence radius and not at the exact touch point
        // Calculate angle from touch to particle
        let angleToTouch = atan2(p.y - t.y, p.x - t.x);
        // Map distance to force magnitude (stronger closer to touch)
        let forceMagnitude = map(d, 0, touchInfluenceRadius, touchInfluenceForce, 0);
        // Apply force to particle velocity, pushing it away from the touch
        p.vx += cos(angleToTouch) * forceMagnitude;
        p.vy += sin(angleToTouch) * forceMagnitude;
      }
    }

    // Update particle position based on velocity
    p.x += p.vx;
    p.y += p.vy;

    // Wrap particles around the edges of the canvas
    if (p.x < 0) p.x = width;
    if (p.x > width) p.x = 0;
    if (p.y < 0) p.y = height;
    if (p.y > height) p.y = 0;

    // Draw the particle with its individual vibrant hue from our palette
    // HSB: p.hue (from palette), Saturation 75 (vibrant), p.brightness (for light/dark variations), p.alpha (for transparency)
    fill(p.hue, 75, p.brightness, p.alpha);
    circle(p.x, p.y, p.size);
  }

  // Draw the logo text "P5JS.AI"
  // Use min(width, height) for responsive text sizing on various screens
  textSize(min(width, height) * 0.15); // Adjust multiplier for desired size
  textAlign(CENTER, CENTER); // Center the text horizontally and vertically
  textFont(robotoFont); // Apply the loaded font
  fill(240, 95, 95); // Vibrant deep blue text (Hue 240, Saturation 95, Brightness 95)
  noStroke(); // Ensure text has no outline
  // Position slightly above the center to leave room for the steam below/around
  text("P5JS.AI", width / 2, height / 2 - min(width, height) * 0.05);
}
Line-by-line explanation (31 lines)

🔧 Subcomponents:

shape Trail Fade Effect rect(0, 0, width, height);

Draws a semi-transparent rectangle that gradually erases old particle positions, creating fading motion trails

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

Iterates through every particle to update its velocity, position, and handle interactions

calculation Perlin Noise Flow Field let angle = noise(p.x * flowFieldScale, p.y * flowFieldScale, frameCount * flowFieldSpeed) * TWO_PI * 2;

Uses 3D Perlin noise to generate a smooth, organic flow direction that varies by position and animates over time

calculation Smooth Velocity Interpolation p.vx = lerp(p.vx, flowVx, 0.1);

Gradually moves the particle's current velocity toward the flow field direction instead of instantly changing it

conditional Touch Point Collection if (mouseIsPressed) {

Gathers all active touch points (mouse and multi-touch) into an array for interaction calculations

for-loop Touch Force Application for (let t of touchPoints) {

For each touch point, calculates the distance to the particle and applies a repulsive force if within range

conditional Canvas Edge Wrapping if (p.x < 0) p.x = width;

When a particle moves past an edge, it reappears on the opposite side, creating a seamless infinite canvas

shape Particle Rendering circle(p.x, p.y, p.size);

Draws the particle as a colored circle at its current position using HSB color from the palette

fill(0, 0, 10, 5);
Sets the fill color to nearly black with very low opacity (5/100), creating a semi-transparent veil that fades previous frames.
rect(0, 0, width, height);
Draws a rectangle covering the entire canvas, effectively dimming everything drawn in the previous frame and creating motion trails.
for (let p of particles) {
Loops through every particle object in the particles array, updating and drawing each one every frame.
let angle = noise(p.x * flowFieldScale, p.y * flowFieldScale, frameCount * flowFieldSpeed) * TWO_PI * 2;
Samples a 3D Perlin noise field at the particle's x and y position, scaled by flowFieldScale, animated by frameCount. This produces a smooth, organic angle (0 to 2π × 2) that guides the particle's direction.
let flowVx = cos(angle) * particleSpeed;
Converts the noise-based angle into an x-component of velocity using cosine, scaled by the base particleSpeed.
let flowVy = sin(angle) * particleSpeed;
Converts the noise-based angle into a y-component of velocity using sine, scaled by the base particleSpeed.
p.vx = lerp(p.vx, flowVx, 0.1);
Smoothly blends the particle's current x-velocity toward the flow field x-velocity by 10% each frame, creating fluid motion instead of jerky direction changes.
p.vy = lerp(p.vy, flowVy, 0.1);
Smoothly blends the particle's current y-velocity toward the flow field y-velocity by 10% each frame.
if (mouseIsPressed) {
Checks if the mouse button is currently held down, allowing the mouse position to influence particles.
touchPoints.push({ x: mouseX, y: mouseY });
Adds the current mouse position as an object with x and y properties to the touchPoints array.
for (let t of touches) {
Loops through the browser's built-in touches array, which contains all active touch points on mobile/tablet devices.
touchPoints.push({ x: t.x, y: t.y });
Adds each touch point's x and y coordinates to the touchPoints array so they can influence particles.
for (let t of touchPoints) {
Loops through every collected touch point (mouse or physical touch) to apply influence to the current particle.
let d = dist(p.x, p.y, t.x, t.y);
Calculates the straight-line distance from the particle to the touch point using p5.js's built-in dist() function.
if (d < touchInfluenceRadius && d > 0) {
Only applies influence if the particle is within the touchInfluenceRadius (100 pixels by default) AND is not exactly at the touch point (d > 0).
let angleToTouch = atan2(p.y - t.y, p.x - t.x);
Calculates the angle pointing from the touch point toward the particle using atan2, which is needed to push the particle away.
let forceMagnitude = map(d, 0, touchInfluenceRadius, touchInfluenceForce, 0);
Maps the distance to a force value: particles right at the touch are pushed with full force, particles at the edge of the radius are barely pushed.
p.vx += cos(angleToTouch) * forceMagnitude;
Adds a push in the x-direction away from the touch by converting the angle to an x-component and scaling it by the force magnitude.
p.vy += sin(angleToTouch) * forceMagnitude;
Adds a push in the y-direction away from the touch by converting the angle to a y-component and scaling it by the force magnitude.
p.x += p.vx;
Updates the particle's x position by adding its current x-velocity, moving it one frame step along its current direction.
p.y += p.vy;
Updates the particle's y position by adding its current y-velocity, moving it one frame step down its current direction.
if (p.x < 0) p.x = width;
If the particle moves past the left edge, teleport it to the right edge, creating a wraparound effect.
if (p.x > width) p.x = 0;
If the particle moves past the right edge, teleport it to the left edge.
if (p.y < 0) p.y = height;
If the particle moves above the top edge, teleport it to the bottom edge.
if (p.y > height) p.y = 0;
If the particle moves below the bottom edge, teleport it to the top edge.
fill(p.hue, 75, p.brightness, p.alpha);
Sets the fill color using HSB: the particle's stored hue, a fixed saturation of 75 (vibrant), its stored brightness, and its stored alpha (transparency).
circle(p.x, p.y, p.size);
Draws a circle at the particle's current x and y position with its assigned size, creating the visual particle on screen.
textSize(min(width, height) * 0.15);
Sets the text size to 15% of the smaller screen dimension, making it scale responsively on different devices.
textAlign(CENTER, CENTER);
Centers the text both horizontally and vertically around the position specified in the text() call.
fill(240, 95, 95);
Sets the text color to a vibrant blue using HSB (Hue 240 is blue, high saturation and brightness for boldness).
text("P5JS.AI", width / 2, height / 2 - min(width, height) * 0.05);
Draws the text 'P5JS.AI' centered horizontally and positioned slightly above the vertical center of the screen.

touchMoved()

touchMoved() is called in p5.js whenever the user's fingers move on the screen. Returning false prevents the browser from scrolling or zooming, letting your sketch have full control of touch input.

function touchMoved() {
  // Prevent default browser behavior (scrolling) when touching the canvas
  return false;
}
Line-by-line explanation (1 lines)
return false;
Tells the browser to ignore its default touch behavior (like scrolling or zooming), so all touch movement is captured by the sketch instead.

windowResized()

windowResized() is called in p5.js whenever the window is resized (including mobile device orientation changes). Here it resizes the canvas and reinitializes particles so the sketch looks fresh and fills the new screen size.

function windowResized() {
  // Resize the canvas when the window (or tablet orientation) changes
  resizeCanvas(windowWidth, windowHeight);
  // Reinitialize particles to fit the new canvas size, creating a fresh flow
  particles = [];
  for (let i = 0; i < numParticles; i++) {
    particles.push({
      x: random(width),
      y: random(height),
      vx: 0,
      vy: 0,
      hue: random(particleHues), // Assign a random hue from our custom palette
      brightness: random(80, 100),
      alpha: random(20, 50),
      size: random(1, 3)
    });
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

for-loop Particle Reinitialization Loop for (let i = 0; i < numParticles; i++) {

Recreates the entire particle array with new random positions sized to fit the new canvas dimensions

resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the new window dimensions (triggered when the browser is resized or device orientation changes).
particles = [];
Clears the particles array, removing all old particle objects so new ones can be created to fit the resized canvas.
for (let i = 0; i < numParticles; i++) {
Loops 2500 times to recreate the full particle population with random positions in the new canvas size.

📦 Key Variables

robotoFont object

Stores the loaded Roboto font file so text can be drawn with a custom typeface instead of the default browser font.

let robotoFont;
particles array

Stores an array of 2500 particle objects, each with position, velocity, color, size, and transparency—the core data structure for the sketch.

let particles = [];
numParticles number

Controls how many particles are created and animated—higher values create denser mist but may slow performance.

let numParticles = 2500;
flowFieldScale number

Controls how zoomed-in the Perlin noise field is—smaller values create tighter, more chaotic swirls; larger values create broader, slower flows.

let flowFieldScale = 0.01;
flowFieldSpeed number

Controls how fast the Perlin noise field animates over time—higher values make flows change more dynamically.

let flowFieldSpeed = 0.005;
particleSpeed number

The base speed at which particles travel along the flow field vectors—higher values create faster, more energetic motion.

let particleSpeed = 2;
touchInfluenceRadius number

The distance in pixels from a touch point where particles react and are pushed away—larger values create broader influence zones.

let touchInfluenceRadius = 100;
touchInfluenceForce number

The strength of the repulsive force applied to particles near a touch point—higher values push them further away.

let touchInfluenceForce = 0.5;
particleHues array

An array of HSB hue values (0–360) that define the color palette for particles—currently [210, 270, 180, 240, 300] for blues, purples, and cyans.

let particleHues = [210, 270, 180, 240, 300];

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

PERFORMANCE draw() - trail effect line

Drawing a semi-transparent rect every frame over the entire canvas and iterating 2500 particles is computationally expensive. On older devices or lower-end phones, this can cause frame drops.

💡 Consider using a lower numParticles value on mobile devices (detect with touch support), or use requestAnimationFrame timing to skip some frames on slow devices. Alternatively, reduce the alpha of the trail rectangle to 2 instead of 5 to use a lighter touch effect.

BUG windowResized() function

When the window is resized, all particles are recreated at new random positions, which causes a visual jump/reset. Users don't see a smooth transition—the mist suddenly appears in different locations.

💡 Instead of clearing and reinitializing particles, just update their positions proportionally: p.x = (p.x / oldWidth) * newWidth and p.y = (p.y / oldHeight) * newHeight. This keeps the visual flow continuous while fitting the new canvas size.

STYLE Variable naming and organization

The global variables are scattered at the top without clear grouping. As the sketch grows, it becomes harder to find which tunable affects which behavior.

💡 Group related variables into logical sections with comments: // Particle System, // Flow Field Parameters, // Touch Interaction, // Visual Appearance. This makes the code easier to maintain and modify.

FEATURE particleHues array

The palette is hard-coded and can't be changed during runtime. Users might want to cycle through different color schemes or set a custom palette.

💡 Create a function like setColorPalette(newHues) that updates particleHues and reinitializes particles with the new colors. Or detect time of day and automatically use a palette (warm in morning, cool at night).

STYLE Perlin noise parameters

The flowFieldScale and flowFieldSpeed multipliers (0.01 and 0.005) are magic numbers with no clear relationship. It's not obvious why these specific values were chosen or how they interact.

💡 Define named constants like const NOISE_DETAIL_LEVEL = 0.01 and const NOISE_TIME_SCALE = 0.005 that make the intent clearer. Even better, add comments explaining that flowFieldScale controls spatial frequency (tightness) and flowFieldSpeed controls temporal frequency (animation speed).

🔄 Code Flow

Code flow showing preload, setup, draw, touchmoved, windowresized

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

graph TD start[Start] --> setup[setup] click setup href "#fn-setup" setup --> particle-init-loop[Particle Initialization Loop] click particle-init-loop href "#sub-particle-init-loop" particle-init-loop --> particle-object[Particle Object Definition] click particle-object href "#sub-particle-object" particle-object --> draw[draw loop] click draw href "#fn-draw" draw --> particle-loop[Particle Update Loop] click particle-loop href "#sub-particle-loop" draw --> trail-effect[Trail Fade Effect] click trail-effect href "#sub-trail-effect" draw --> touch-collection[Touch Point Collection] click touch-collection href "#sub-touch-collection" draw --> edge-wrapping[Canvas Edge Wrapping] click edge-wrapping href "#sub-edge-wrapping" draw --> particle-draw[Particle Rendering] click particle-draw href "#sub-particle-draw" particle-loop --> flow-field[Perlin Noise Flow Field] click flow-field href "#sub-flow-field" flow-field --> velocity-lerp[Smooth Velocity Interpolation] click velocity-lerp href "#sub-velocity-lerp" touch-collection --> touch-influence[Touch Force Application] click touch-influence href "#sub-touch-influence" draw --> touchmoved[touchMoved] click touchmoved href "#fn-touchmoved" draw --> windowresized[windowResized] click windowresized href "#fn-windowresized" windowresized --> particle-reinit-loop[Particle Reinitialization Loop] click particle-reinit-loop href "#sub-particle-reinit-loop" particle-reinit-loop --> particle-object particle-object --> setup

❓ Frequently Asked Questions

What visual effects does the p5.js sketch 'five hundred and twenty fourth to come' produce?

The sketch showcases swirling clouds of colorful, steam-like particles that drift across a dark background, leaving soft, glowing trails.

How can users interact with the 'five hundred and twenty fourth to come' sketch?

Users can touch or drag on the screen to gently push and swirl the mist, shaping its motion in real time.

What creative coding techniques are demonstrated in this p5.js sketch?

The sketch illustrates particle systems and flow fields, utilizing HSB color mode for vibrant visuals and real-time interaction.

Preview

five hundred and twenty fourth to come - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of five hundred and twenty fourth to come - Code flow showing preload, setup, draw, touchmoved, windowresized
Code Flow Diagram