Sketch 2026-07-03 12:08 daily theme

This sketch creates a mesmerizing quicksand simulation where particles gently fall from above, land on a rippling sandy surface, and slowly sink out of view. The undulating quicksand surface is animated using Perlin noise, while individual particles accelerate as they sink deeper, creating a continuous, hypnotic flow.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make particles spawn faster — Lower the spawn interval number so more sand grains fall at once, creating a denser, faster-moving effect
  2. Speed up the surface ripples — Increase the noiseOffset increment so the wavy quicksand surface animates faster
  3. Make particles disappear slower — Increase the time before particles reach max sinking speed, making the sinking animation more drawn-out
  4. Change the sand to a different color — Modify the brown RGB values to create sand of a completely different tone—try gold, red, or even blue
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch animates a soothing quicksand scene where tiny particles continuously fall from the sky, land on a soft rippling sand surface, and gradually sink below the screen. The visual effect combines three powerful p5.js techniques: a particle system that tracks hundreds of falling grains, Perlin noise to create a smoothly undulating surface, and physics simulation so each grain accelerates realistically as it sinks deeper into the sand.

The code is organized into three main pieces: a setup() that initializes the canvas and colors, a draw() loop that updates all particles and redraws the animated quicksand each frame, and a Particle class that handles the physics for each individual grain. By studying this sketch you will learn how to build a particle system from scratch, animate a surface using noise, and implement acceleration-based motion that feels natural and lifelike.

⚙️ How It Works

  1. When the sketch loads, setup() creates a fullscreen canvas, defines soft brown colors for the quicksand, and spawns 50 initial particles above the screen
  2. Every frame, draw() clears the background and calls drawQuicksand() to render the rippling sand surface using Perlin noise that shifts every frame
  3. The particle array loops backward through each grain: calling update() to move it, display() to draw it, and checking isSunk() to remove it when it falls below the canvas
  4. For each particle, update() checks whether it has reached the quicksand surface using a collision condition; if not, it falls with gravity, and if yes, it sinks with acceleration that speeds up over time
  5. The sinking acceleration uses millis() to track elapsed time and map() to smoothly transition from slow to fast sinking over 3 seconds
  6. Every 60 frames, a new particle spawns at a random x position above the canvas, keeping the effect continuous; the noiseOffset increments each frame to animate the wavy surface ripples

🎓 Concepts You'll Learn

Particle systemsPerlin noise animationCollision detectionPhysics accelerationVector math (position, velocity, acceleration)

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch starts. It's the perfect place to initialize the canvas size, define colors, and create your first objects. This sketch uses it to set up a fullscreen responsive canvas and spawn an initial wave of particles.

function setup() {
  createCanvas(windowWidth, windowHeight);
  // Tip: Use windowWidth/windowHeight for responsive canvas
  
  // Initialize quicksand colors
  quicksandColor1 = color(100, 70, 40); // Darker brown
  quicksandColor2 = color(150, 110, 80); // Lighter brown
  
  // Add some initial particles
  for (let i = 0; i < 50; i++) {
    addParticle();
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

function-call Canvas initialization createCanvas(windowWidth, windowHeight);

Creates a fullscreen canvas that responds to the window size

assignment Color definitions quicksandColor1 = color(100, 70, 40); // Darker brown

Stores color objects used for the quicksand gradient and particle coloring

for-loop Initial particle spawn for (let i = 0; i < 50; i++) { addParticle(); }

Creates 50 particles at the start so the sketch doesn't begin empty

createCanvas(windowWidth, windowHeight);
Creates a canvas matching the full browser window size, making the sketch responsive to window resizing
quicksandColor1 = color(100, 70, 40); // Darker brown
Defines a darker brown color object used for the quicksand base; stored in a global variable so draw() can access it
quicksandColor2 = color(150, 110, 80); // Lighter brown
Defines a lighter brown color object used for particle variation; creates visual depth with two shades
for (let i = 0; i < 50; i++) {
A for-loop that repeats 50 times, calling addParticle() each time to populate the particles array

draw()

draw() runs 60 times per second, updating and redrawing everything. This is where animation happens—every loop clears the screen and redraws particles at their new positions, creating the illusion of motion.

🔬 This loop updates and draws every particle. What happens if you add a line like `particles[i].size *= 0.99;` inside the loop so particles shrink as they sink?

  for (let i = particles.length - 1; i >= 0; i--) {
    particles[i].update();
    particles[i].display();
function draw() {
  // Draw the background with a subtle gradient or solid color
  background(240); // Light background above quicksand
  
  // Draw the quicksand
  drawQuicksand();
  
  // Update and display particles
  for (let i = particles.length - 1; i >= 0; i--) {
    particles[i].update();
    particles[i].display();
    
    // Remove particles that have sunk below the canvas
    if (particles[i].isSunk()) {
      particles.splice(i, 1);
    }
  }
  
  // Optionally add new particles over time or on mouse press
  if (frameCount % 60 === 0) { // Add a new particle every second
    addParticle();
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

function-call Frame clear background(240); // Light background above quicksand

Erases the previous frame so particles don't leave trails; creates a clean animation loop

for-loop Particle update and removal for (let i = particles.length - 1; i >= 0; i--) {

Loops backward through particles so removing one doesn't skip any; each particle is updated, displayed, and checked for removal

conditional New particle spawning if (frameCount % 60 === 0) { // Add a new particle every second

Uses frameCount modulo to spawn a new particle only every 60th frame (once per second at 60fps)

background(240); // Light background above quicksand
Fills the entire canvas with light gray (value 240 in grayscale), erasing all previous drawings and preventing motion trails
drawQuicksand();
Calls the drawQuicksand() function to render the rippling sand surface and its body with the current noiseOffset value
for (let i = particles.length - 1; i >= 0; i--) {
Loops backward from the last particle to the first; this is critical because we'll remove particles inside the loop, and backward iteration prevents skipping
particles[i].update();
Calls the update() method on each particle to recalculate its position, velocity, and acceleration based on physics
particles[i].display();
Calls the display() method to draw the particle as a circle at its current position with its color
if (particles[i].isSunk()) {
Checks whether the particle has sunk below the canvas using the isSunk() method
particles.splice(i, 1);
Removes the particle from the array using splice(), preventing the particles array from growing infinitely
if (frameCount % 60 === 0) { // Add a new particle every second
Uses the modulo operator (%) to check if frameCount is evenly divisible by 60; true only once per second at 60fps
addParticle();
Spawns a new particle at a random x position above the canvas to maintain a continuous flow of falling sand

drawQuicksand()

This function demonstrates Perlin noise in action. Rather than random jagged motion, noise creates smooth, natural-looking waves. By updating noiseOffset each frame and using that value as input to noise(), the ripples flow continuously without jumping or glitching.

🔬 This line uses map() to convert noise (0 to 1) into a y-position. What happens if you change the range to `height * quicksandHeight - 50, height * quicksandHeight + 50` to make bigger ripples?

  let y = map(noise(xoff), 0, 1, height * quicksandHeight - 30, height * quicksandHeight + 10);
function drawQuicksand() {
  fill(quicksandColor1);
  noStroke();
  
  // Draw the main quicksand body
  rect(0, height * quicksandHeight, width, height * (1 - quicksandHeight));
  
  // Draw the wavy surface using noise
  beginShape();
  vertex(0, height * quicksandHeight);
  
  let xoff = noiseOffset;
  for (let x = 0; x <= width; x += 10) {
    // Use noise to create an undulating line
    let y = map(noise(xoff), 0, 1, height * quicksandHeight - 30, height * quicksandHeight + 10);
    vertex(x, y);
    xoff += 0.05; // Adjust for more or less waviness
  }
  
  vertex(width, height * quicksandHeight);
  endShape(CLOSE);
  
  // Animate the noise offset for fluid motion
  noiseOffset += 0.005; // Adjust for faster or slower motion
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

function-call Solid quicksand rectangle rect(0, height * quicksandHeight, width, height * (1 - quicksandHeight));

Draws the main body of quicksand as a filled rectangle below the wavy surface

for-loop Wavy surface generation for (let x = 0; x <= width; x += 10) {

Loops across the width of the canvas every 10 pixels, using Perlin noise to calculate each point on the wavy surface

assignment Surface animation noiseOffset += 0.005; // Adjust for faster or slower motion

Increments noiseOffset each frame so the noise pattern shifts, creating the rippling animation

fill(quicksandColor1);
Sets the fill color to the darker brown, used for all shapes drawn after this line
noStroke();
Disables outlines for shapes, making the quicksand look smoother and more natural
rect(0, height * quicksandHeight, width, height * (1 - quicksandHeight));
Draws a rectangle that fills the bottom 40% of the canvas (if quicksandHeight is 0.6); this is the solid sand body below the ripples
beginShape();
Starts recording vertices for a custom polygon shape—in this case, the wavy surface
vertex(0, height * quicksandHeight);
Places the first vertex at the left edge of the quicksand surface, establishing the starting point
let xoff = noiseOffset;
Initializes a local variable xoff to the global noiseOffset, used as the input to the noise function
for (let x = 0; x <= width; x += 10) {
Loops from x=0 to x=width in steps of 10 pixels, sampling the noise function at each position
let y = map(noise(xoff), 0, 1, height * quicksandHeight - 30, height * quicksandHeight + 10);
Calls noise(xoff) which returns a value between 0 and 1; map() rescales it to a y position between 30 pixels above and 10 pixels below the quicksand surface, creating ripples
vertex(x, y);
Records the calculated wavy point as a vertex in the shape
xoff += 0.05; // Adjust for more or less waviness
Increments xoff for the next loop iteration; higher steps create choppier waves, lower steps create smoother waves
vertex(width, height * quicksandHeight);
Places a final vertex at the right edge of the quicksand surface to close the wavy line
endShape(CLOSE);
Closes the shape by connecting the last vertex back to the first, creating a filled polygon of the wavy surface
noiseOffset += 0.005; // Adjust for faster or slower motion
Increments the global noiseOffset by a small amount each frame; this shifts all the noise values, making the ripples animate fluidly

addParticle()

addParticle() is a helper function that encapsulates the logic for creating a new particle. By putting this in its own function, the code is cleaner and addParticle() can be called from multiple places—here it's called in setup() and every frame in draw().

function addParticle() {
  let x = random(width);
  let y = random(-100, 0); // Start above the canvas
  let size = random(10, 30); // Random size
  let color = random(quicksandColor1, quicksandColor2); // Random quicksand-like color
  particles.push(new Particle(x, y, size, color));
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

assignment Random spawn position let x = random(width);

Randomly selects a horizontal position across the canvas width for the new particle

function-call Particle instantiation particles.push(new Particle(x, y, size, color));

Creates a new Particle object with the generated properties and adds it to the particles array

let x = random(width);
Uses random() to generate a random x position between 0 and the canvas width, ensuring particles spawn across the entire top
let y = random(-100, 0); // Start above the canvas
Generates a random y position between -100 and 0, placing the particle above the visible canvas so it falls into view smoothly
let size = random(10, 30); // Random size
Randomly picks a particle diameter between 10 and 30 pixels, creating visual variety
let color = random(quicksandColor1, quicksandColor2); // Random quicksand-like color
Uses random() to choose a color between the two brown tones, blending them for particles with varied but cohesive coloring
particles.push(new Particle(x, y, size, color));
Creates a new Particle object using the constructor with the randomly generated parameters, then adds it to the particles array with push()

windowResized()

windowResized() is a p5.js lifecycle function that fires automatically when the user resizes their browser. By calling resizeCanvas(), the sketch stays responsive—particles and quicksand adapt to fill the new window. This is essential for modern web sketches.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  // Recalculate quicksand height if necessary
}
Line-by-line explanation (2 lines)

🔧 Subcomponents:

function-call Canvas resizing resizeCanvas(windowWidth, windowHeight);

Updates the canvas dimensions to match the new browser window size

function windowResized() {
This is a special p5.js function that automatically runs whenever the browser window is resized
resizeCanvas(windowWidth, windowHeight);
Calls p5's resizeCanvas() function to update the canvas to match the current browser window dimensions

Particle.update()

The update() method is where particle physics happens. It implements two modes: falling (above quicksand with gravity) and sinking (in quicksand with accelerating speed). The use of millis() and map() creates realistic time-based acceleration that feels natural and organic.

🔬 This code sets vertical velocity and adds random horizontal drift. What happens if you increase the drift range to `random(-0.5, 0.5)` so particles zigzag more as they sink?

      this.vel.y = sinkSpeed;
      
      // Add a slight random horizontal drift
      this.vel.x = random(-0.1, 0.1);
  update() {
    let quicksandSurfaceY = height * quicksandHeight;
    
    // Check if particle is in quicksand
    if (this.pos.y >= quicksandSurfaceY) {
      this.sinking = true;
      
      // Calculate sinking speed: starts slow, accelerates
      let elapsed = millis() - this.sinkStartTime;
      let sinkSpeed = map(elapsed, 0, 3000, this.initialSinkSpeed, this.maxSinkSpeed, true); // Accelerate over 3 seconds
      
      // Apply sinking velocity
      this.vel.y = sinkSpeed;
      
      // Add a slight random horizontal drift
      this.vel.x = random(-0.1, 0.1);
      
      // Apply acceleration (if any)
      this.vel.add(this.acc);
      // Limit velocity
      this.vel.limit(this.maxSinkSpeed);
      // Update position
      this.pos.add(this.vel);
      
      // Reset acceleration for next frame
      this.acc.mult(0);
      
    } else {
      // If not in quicksand, just fall with gravity
      this.acc.y = 0.1; // Simple gravity
      this.vel.add(this.acc);
      this.pos.add(this.vel);
      
      // If it just entered, record the start time
      if (this.pos.y >= quicksandSurfaceY) {
        this.sinkStartTime = millis();
      }
    }
  }
Line-by-line explanation (16 lines)

🔧 Subcomponents:

conditional Sinking behavior if (this.pos.y >= quicksandSurfaceY) {

Checks if the particle has reached the quicksand surface and applies sinking physics

calculation Accelerating sink speed let sinkSpeed = map(elapsed, 0, 3000, this.initialSinkSpeed, this.maxSinkSpeed, true);

Maps elapsed time to a gradually increasing sink speed, making particles accelerate realistically over 3 seconds

conditional Falling before quicksand } else {

If the particle hasn't reached quicksand yet, apply simple gravity to make it fall

let quicksandSurfaceY = height * quicksandHeight;
Calculates the y position of the quicksand surface (60% down the canvas) using the quicksandHeight variable
if (this.pos.y >= quicksandSurfaceY) {
Checks if the particle's y position has reached or passed the quicksand surface; if true, the particle is sinking
this.sinking = true;
Sets the sinking flag to true so the isSunk() method can later check whether to remove this particle
let elapsed = millis() - this.sinkStartTime;
Calculates how many milliseconds have passed since the particle entered the quicksand by subtracting the start time from the current time
let sinkSpeed = map(elapsed, 0, 3000, this.initialSinkSpeed, this.maxSinkSpeed, true);
Maps elapsed time to a sink speed: at 0ms the speed is slow (0.1), at 3000ms it's fast (0.5), and the true parameter constrains it to that range so it doesn't overshoot
this.vel.y = sinkSpeed;
Sets the vertical velocity to the calculated sink speed, making the particle move downward at an accelerating rate
this.vel.x = random(-0.1, 0.1);
Adds a small random horizontal drift to make the sinking motion less uniform and more natural
this.vel.add(this.acc);
Adds any acceleration (though it's reset to zero each frame) to the velocity, following physics conventions
this.vel.limit(this.maxSinkSpeed);
Caps the velocity magnitude at maxSinkSpeed so the particle never moves faster than the intended maximum, ensuring realism
this.pos.add(this.vel);
Updates the particle's position by adding the velocity, moving it one step in the direction and speed determined by vel
this.acc.mult(0);
Resets acceleration to zero for the next frame; this is standard physics engine practice
this.acc.y = 0.1; // Simple gravity
In the else branch, applies a constant downward acceleration (gravity) to particles still falling before they hit sand
this.vel.add(this.acc);
Adds the gravity acceleration to velocity, speeding up the falling motion
this.pos.add(this.vel);
Updates position by adding velocity, moving the particle downward due to gravity
if (this.pos.y >= quicksandSurfaceY) {
Checks if the particle just entered the quicksand this frame; if so, records the current time as sinkStartTime for acceleration calculations
this.sinkStartTime = millis();
Stores the current millisecond time so elapsed time can be calculated in future frames

Particle.display()

display() is kept intentionally simple: it only draws the particle. By separating logic (update) from rendering (display), the code is cleaner and easier to debug. If particles don't appear, the problem is likely here; if they move wrongly, check update().

  display() {
    fill(this.color);
    noStroke();
    // Draw as a simple ellipse
    ellipse(this.pos.x, this.pos.y, this.size, this.size);
  }
Line-by-line explanation (3 lines)

🔧 Subcomponents:

function-call Particle rendering ellipse(this.pos.x, this.pos.y, this.size, this.size);

Draws the particle as a circle at its current position with its stored size and color

fill(this.color);
Sets the fill color to this particle's color (a blend of the two brown tones), affecting all shapes drawn after this
noStroke();
Disables outlines for this particle, making it render as a smooth filled circle without borders
ellipse(this.pos.x, this.pos.y, this.size, this.size);
Draws a circle (ellipse with equal width and height) at the particle's position vector (pos.x, pos.y) with a diameter of this.size

Particle.isSunk()

isSunk() is a simple boolean method that determines when a particle should be removed from the array. By checking both sinking status and position, it prevents premature removal and ensures only particles fully below the canvas get deleted, keeping the array clean and efficient.

  isSunk() {
    return this.sinking && this.pos.y > height + this.size;
  }
Line-by-line explanation (1 lines)

🔧 Subcomponents:

calculation Sunk detection return this.sinking && this.pos.y > height + this.size;

Returns true only if the particle has started sinking AND moved below the bottom of the canvas

return this.sinking && this.pos.y > height + this.size;
Returns true only if BOTH conditions are met: this.sinking is true (particle entered quicksand) AND this.pos.y is below the canvas (particle has fully exited the screen). The && operator ensures both must be true.

📦 Key Variables

particles array

Stores all active Particle objects in the scene; particles are added via addParticle() and removed when isSunk() returns true

let particles = [];
noiseOffset number

A counter that increments each frame to animate the Perlin noise pattern; controls the rippling motion of the quicksand surface

let noiseOffset = 0;
quicksandHeight number

A proportion (0 to 1) that defines where the quicksand surface sits vertically; 0.6 means 60% down the canvas

let quicksandHeight = 0.6;
quicksandColor1 color

Stores a dark brown p5.js color object used for the quicksand body and as the darker tone in particle color variation

let quicksandColor1 = color(100, 70, 40);
quicksandColor2 color

Stores a light brown p5.js color object used as the lighter tone in particle color variation, creating visual depth

let quicksandColor2 = color(150, 110, 80);

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE update() method

Calling millis() inside update() 60 times per frame (once per particle) can be inefficient; millis() involves system calls that add up

💡 Store millis() in a variable in draw() at the top of each frame, then pass it to update() as a parameter: `particles[i].update(millis())`

STYLE Particle constructor

The sinkStartTime is initialized in the constructor but only used after the particle enters quicksand, which is conceptually odd

💡 Initialize sinkStartTime as null in the constructor, then set it to millis() only when the particle enters quicksand (already done in update)

FEATURE draw() function

Particles are spawned automatically every 60 frames, but there's no way for the user to interact with the effect

💡 Add mousePressed() function to spawn particles where the mouse clicks, or add keyboard interaction like pressing 'space' to pause/resume animation

BUG drawQuicksand() function

The wavy surface can go slightly above or below its intended range due to the noise amplitude; particles near the surface may appear to float or sink prematurely

💡 Clamp the noise-generated y value: `let y = constrain(map(noise(xoff), 0, 1, height * quicksandHeight - 30, height * quicksandHeight + 10), height * quicksandHeight - 30, height * quicksandHeight + 10);`

🔄 Code Flow

Code flow showing setup, draw, drawquicksand, addparticle, windowresized, update, display, issunk

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

graph TD start[Start] --> setup[setup] setup --> canvas-creation[Canvas Initialization] setup --> color-setup[Color Definitions] setup --> initial-particles[Initial Particle Spawn] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-creation href "#sub-canvas-creation" click color-setup href "#sub-color-setup" click initial-particles href "#sub-initial-particles" draw --> background-clear[Frame Clear] draw --> particle-loop[Particle Update and Removal] particle-loop --> spawn-timer[New Particle Spawning] particle-loop --> update[Update Method] click draw href "#fn-draw" click background-clear href "#sub-background-clear" click particle-loop href "#sub-particle-loop" click spawn-timer href "#sub-spawn-timer" click update href "#fn-update" update --> sinking-branch[Sinking Behavior] update --> gravity-branch[Gravity Branch] sinking-branch --> acceleration-calculation[Accelerating Sink Speed] gravity-branch --> shape-drawing[Particle Rendering] click sinking-branch href "#sub-sinking-branch" click gravity-branch href "#sub-gravity-branch" click acceleration-calculation href "#sub-acceleration-calculation" click shape-drawing href "#sub-shape-drawing" draw --> drawquicksand[drawquicksand] drawquicksand --> quicksand-body[Solid Quicksand Rectangle] drawquicksand --> noise-loop[Wavy Surface Generation] noise-loop --> noise-animation[Surface Animation] noise-loop --> random-position[Random Spawn Position] click drawquicksand href "#fn-drawquicksand" click quicksand-body href "#sub-quicksand-body" click noise-loop href "#sub-noise-loop" click noise-animation href "#sub-noise-animation" click random-position href "#sub-random-position" particle-loop --> collision-check[Sunk Detection] collision-check --> issunk[isSunk] click collision-check href "#sub-collision-check" click issunk href "#fn-issunk" windowresized[windowResized] --> canvas-resize[Canvas Resizing] click windowresized href "#fn-windowresized" click canvas-resize href "#sub-canvas-resize"

❓ Frequently Asked Questions

What visual effect does the p5.js sketch create?

The sketch visually simulates soft brown quicksand that ripples gently, with tiny particles falling in, sinking, and disappearing in a continuous flow.

Is there any user interaction available in this sketch?

While the sketch primarily runs autonomously, new particles are added every second to enhance the visual effect, and users can explore the animation by resizing their browser window.

What creative coding techniques are demonstrated in this quicksand sketch?

This sketch showcases the use of particle systems and Perlin noise to create realistic movement and a dynamic surface for the quicksand effect.

Preview

Sketch 2026-07-03 12:08 daily theme - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-07-03 12:08 daily theme - Code flow showing setup, draw, drawquicksand, addparticle, windowresized, update, display, issunk
Code Flow Diagram