fading colors

This sketch creates an interactive visual trail effect where moving your mouse leaves behind a series of fading, shrinking circles that gradually disappear. Each circle represents a 'clone' of your mouse position frozen in time, creating a ghostly echo effect that reveals your recent movement path across the dark canvas.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make trails last longer — Increase CLONE_LIFESPAN to 5000 so each clone stays visible for 5 seconds instead of 2, creating much longer ghostly trails behind your mouse
  2. Reverse the color fade — Clones start bright cyan and fade to dim pink as they age. Swap the lerp arguments to reverse this: new clones start dim and brighten as they age
  3. Make clones stay the same size — Remove the size shrinking by making all clones the same diameter throughout their lifespan—just the fade effect remains
  4. Add a rainbow of colors — Use the hue parameter based on age to make each clone a different color, cycling through the spectrum as time passes
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a beautiful motion-trail effect by recording your mouse position and spawning glowing circles that fade away over time. As you move, you see a ghostly echo of your path—each circle is slightly smaller and more transparent than the one before it, creating the illusion that time itself is visible. It's powered by three key p5.js techniques: arrays to store many clones at once, the lerp() function to smoothly animate size and transparency, and millis() to track how old each clone is.

The code is organized into three simple functions: setup() configures the canvas, draw() runs every frame to spawn new clones and animate existing ones, and windowResized() keeps the sketch full-screen when the window resizes. By studying this sketch you will learn how to build interactive particle systems, manage object lifespans, and use time-based animation to create sophisticated visual effects from surprisingly simple code.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-window canvas and hides the default cursor for a cleaner visual effect
  2. Every frame, draw() starts by painting a dark blue-black background that's slightly transparent, allowing previous frames to leave faint visual traces
  3. The current millisecond timestamp (now) is captured, and a new clone object is created containing the mouse's current x and y position plus a 'born' timestamp, then added to the clones array
  4. The code loops through every clone, calculates its age by subtracting its birth time from the current time, and checks if it's still alive (younger than 2000 milliseconds)
  5. For each living clone, lerp() is used twice: once to smoothly shrink the circle from size 40 down to size 5, and once to fade its alpha from fully opaque (255) to fully transparent (0), both based on the clone's age fraction
  6. The circle's color shifts from white-cyan to dim cyan as it ages, using a calculation that makes (1 - t) decrease the brightness over time
  7. Dead clones are removed from the array to save memory, and a bright yellow circle is drawn at the current mouse position to show the 'present moment' at the end of the trail

🎓 Concepts You'll Learn

Arrays and object storageTime-based animation with millis()The lerp() function for smooth transitionsAlpha transparency and color animationObject lifecycle managementMouse interactionParticle systems

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It's the perfect place to configure your canvas size and apply one-time settings like hiding the cursor.

function setup() {
  createCanvas(windowWidth, windowHeight);
  noCursor(); // hide default cursor for a cleaner effect
}
Line-by-line explanation (2 lines)
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, using windowWidth and windowHeight so it adapts to any screen size
noCursor();
Hides the default mouse cursor, making the glowing clone at your mouse position become the visual cursor instead

draw()

draw() runs 60 times per second, creating the continuous animation. Each frame it spawns a fresh clone, ages all existing clones, draws the ones still alive, and removes the expired ones. This is the heartbeat of the entire effect.

🔬 This loop checks each clone's age and only draws it if it's younger than CLONE_LIFESPAN. What happens if you change the condition to age > CLONE_LIFESPAN? When would clones appear instead?

  for (let c of clones) {
    const age = now - c.born;
    if (age < CLONE_LIFESPAN) {

🔬 These two lerp() calls shrink the size and fade the alpha. What happens if you swap them—lerp(5, 40, t) for size and lerp(0, 255, t) for alpha? What visual effect does reversing the animation create?

      const size = lerp(40, 5, t);
      const alpha = lerp(255, 0, t);
function draw() {
  // Dark background with slight transparency for a soft trail effect
  background(10, 10, 25);

  const now = millis();

  // Add a new clone of the current mouse position each frame
  clones.push({
    x: mouseX,
    y: mouseY,
    born: now
  });

  // Draw and filter clones based on their age
  noStroke();
  let aliveClones = [];

  for (let c of clones) {
    const age = now - c.born;
    if (age < CLONE_LIFESPAN) {
      const t = age / CLONE_LIFESPAN; // 0 → just born, 1 → about to disappear

      // Size shrinks and alpha fades with time
      const size = lerp(40, 5, t);
      const alpha = lerp(255, 0, t);

      fill(120 + 80 * (1 - t), 200 * (1 - t), 255, alpha);
      circle(c.x, c.y, size);

      aliveClones.push(c);
    }
  }

  // Keep only alive clones
  clones = aliveClones;

  // Draw current "source" point (the present)
  fill(255, 250, 200);
  circle(mouseX, mouseY, 10);
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

calculation Clone Spawning clones.push({ x: mouseX, y: mouseY, born: now });

Creates a new clone object with the current mouse position and timestamp, then adds it to the clones array

for-loop Clone Animation Loop for (let c of clones) { const age = now - c.born; if (age < CLONE_LIFESPAN) { const t = age / CLONE_LIFESPAN; const size = lerp(40, 5, t); const alpha = lerp(255, 0, t); fill(120 + 80 * (1 - t), 200 * (1 - t), 255, alpha); circle(c.x, c.y, size); aliveClones.push(c); } }

Iterates through all clones, calculates their age, animates their size and transparency, draws them, and keeps track of which ones are still alive

calculation Clone Cleanup clones = aliveClones;

Removes dead clones from the array to prevent the array from growing infinitely and slowing down the sketch

background(10, 10, 25);
Paints a very dark blue-black color over the entire canvas each frame, partially erasing the previous frame but leaving faint traces
const now = millis();
Captures the current time in milliseconds since the sketch started—used to calculate how old each clone is
clones.push({
Begins adding a new clone object to the clones array
x: mouseX,
Stores the mouse's current horizontal position in the new clone object
y: mouseY,
Stores the mouse's current vertical position in the new clone object
born: now
Records the exact timestamp when this clone was created, so we can calculate its age later
const age = now - c.born;
Calculates how many milliseconds have passed since this clone was born
if (age < CLONE_LIFESPAN) {
Only processes the clone if it's younger than 2000 milliseconds (or whatever CLONE_LIFESPAN is set to)
const t = age / CLONE_LIFESPAN;
Converts the clone's age into a decimal from 0 (just born) to 1 (about to die)—this fraction controls animation
const size = lerp(40, 5, t);
Uses lerp() to smoothly shrink the clone from size 40 down to size 5 based on its age fraction t
const alpha = lerp(255, 0, t);
Uses lerp() to fade the clone's transparency from fully opaque (255) to fully invisible (0) based on its age
fill(120 + 80 * (1 - t), 200 * (1 - t), 255, alpha);
Sets the fill color: red channel brightens as the clone ages, green fades to 0, blue stays bright, and the fourth argument (alpha) makes it transparent
circle(c.x, c.y, size);
Draws the clone as a circle at its stored x,y position with the calculated size and color
aliveClones.push(c);
Adds this living clone to the aliveClones array so we know to keep it next frame
clones = aliveClones;
Replaces the full clones array with only the living clones, discarding any that have expired
fill(255, 250, 200);
Sets the fill color to a soft white-yellow for the bright dot at your current mouse position
circle(mouseX, mouseY, 10);
Draws a small bright circle at your current mouse position—this is the 'source' of the trail and marks the present moment

windowResized()

p5.js calls windowResized() automatically whenever the browser window changes size. By calling resizeCanvas(), we keep the sketch full-screen and responsive to user actions like dragging the window edge.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}
Line-by-line explanation (1 lines)
resizeCanvas(windowWidth, windowHeight);
Automatically adjusts the canvas size to match the new window dimensions whenever the browser window is resized

📦 Key Variables

clones array

Stores all active clone objects, where each clone is an object containing x, y, and born (timestamp) properties. The array grows and shrinks as clones are born and die

let clones = [];
CLONE_LIFESPAN number

A constant that defines how many milliseconds each clone lives before disappearing—set to 2000 (2 seconds) by default

const CLONE_LIFESPAN = 2000;
now number

Stores the current timestamp in milliseconds (returned by millis()), used to calculate the age of each clone

const now = millis();
age number

Calculated by subtracting a clone's birth time from the current time, representing how old that clone is in milliseconds

const age = now - c.born;
t number

A normalized age fraction from 0 (newly born) to 1 (about to die), used as input to lerp() to animate size and transparency

const t = age / CLONE_LIFESPAN;
size number

The diameter of the current clone, calculated by lerping from 40 to 5 based on the clone's age fraction t

const size = lerp(40, 5, t);
alpha number

The transparency value of the current clone, calculated by lerping from 255 (fully opaque) to 0 (fully transparent)

const alpha = lerp(255, 0, t);
aliveClones array

A temporary array built during each frame that collects only the clones that are still alive, replacing the main clones array at the end of draw()

let aliveClones = [];

🔧 Potential Improvements (4)

Here are some ways this code could be enhanced:

PERFORMANCE draw() - clone array management

Every frame creates a new aliveClones array even if all clones are alive, adding unnecessary memory allocation overhead

💡 Use array.filter() or manage the array more efficiently: clones = clones.filter(c => now - c.born < CLONE_LIFESPAN);

BUG draw() - color calculation

The red channel calculation (120 + 80 * (1 - t)) can exceed 255 when t is very small, potentially causing unexpected color values or clamping

💡 Ensure RGB values stay within 0-255: fill(constrain(120 + 80 * (1 - t), 0, 255), 200 * (1 - t), 255, alpha);

STYLE sketch.js - variable naming

The variable 'c' in the for loop is cryptic; a more descriptive name would improve readability

💡 Rename to 'clone': for (let clone of clones) { const age = now - clone.born; ... }

FEATURE sketch.js - interaction

The sketch only responds to mouse movement; adding mousePressed() or other interactions could enhance interactivity

💡 Add mousePressed() to spawn a burst of extra clones when clicked, or keyPressed() to toggle the effect on/off

🔄 Code Flow

Code flow showing setup, draw, windowresized

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

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> clonecreation[Clone Creation] draw --> cloneloop[Clone Animation Loop] draw --> clonefiltering[Clone Cleanup] cloneloop --> agecalc[Age Calculation] cloneloop --> animate[Animate Size and Transparency] cloneloop --> drawclones[Draw Clones] clonefiltering --> removeclones[Remove Dead Clones] click setup href "#fn-setup" click draw href "#fn-draw" click clonecreation href "#sub-clone-creation" click cloneloop href "#sub-clone-loop" click clonefiltering href "#sub-clone-filtering" click agecalc href "#sub-age-calculation" click animate href "#sub-animate-size-and-transparency" click drawclones href "#sub-draw-clones" click removeclones href "#sub-remove-dead-clones"

❓ Frequently Asked Questions

What visual effect does the fading colors sketch produce?

The sketch creates a mesmerizing display of glowing, shrinking circles that follow the mouse movement, leaving a colorful trail that gently fades into the dark background.

How can users interact with the fading colors sketch?

Users can interact by moving their mouse across the canvas, which generates a trail of fading circles that mimic the path they've taken.

What creative coding concept is showcased in the fading colors sketch?

This sketch demonstrates the concept of time-based animations through the use of fading and shrinking clones that represent the passage of time.

Preview

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