Sketch 2026-02-08 21:36

Neural Flow Weaver is an abstract visualization of an interconnected network where animated nodes drift across the canvas and connect dynamically based on proximity. The sketch combines animated nodes, philosophical text insights, ambient soundscapes generated with Tone.js, and interactive audio feedback—creating a meditative experience that represents information processing and problem-solving.

🧪 Try This!

Experiment with the code by making these changes:

  1. Slow down the node drift — Nodes will move much more slowly, creating a calmer, more static network layout.
  2. Make connections glow brighter — Change the connection alpha range so lines are always more visible, making the network structure clearer.
  3. Speed up the rotating galaxy effect — The entire node network will rotate noticeably faster around the center, creating a more dynamic spiral sensation.
  4. Increase connection range to densify the network — Nodes will connect at larger distances, creating many more visible lines and a thicker web of connections.
  5. Make insights display longer — Philosophical text will remain visible for twice as long before fading and being replaced by a new insight.
  6. Brighten the background for daytime mode — The canvas shifts from dark navy to a lighter, less dramatic tone, changing the mood from nocturnal to energetic.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a mesmerizing visualization of an interconnected neural network where hundreds of nodes drift slowly across the canvas and automatically connect when they drift close enough to each other. The magic comes from combining five p5.js techniques: animated particle systems (the drifting nodes), dynamic connection drawing based on distance calculations, perlin noise for organic background texture, text rendering with fade-in/fade-out effects, and responsive canvas resizing. Layered on top are Tone.js synthesizers that create an evolving ambient soundscape, rhythmic pulses triggered by an Euclidean rhythm pattern, and interactive audio feedback when you click the canvas.

The code is structured around a setup() function that initializes 100 nodes with random positions and configures five different Tone.js synthesizers, and a draw() loop that updates node positions every frame, draws connections between nearby nodes, renders philosophical insights that fade in and out, and modulates the audio parameters to evolve the ambient sound over time. By studying it you will learn how to build particle systems, calculate distances to create emergent patterns, layer multiple Tone.js effects into a single coherent soundscape, and respond to window resizing to keep your sketch responsive.

⚙️ How It Works

  1. When the sketch loads, setup() creates a 100 nodes at random positions across the canvas and initializes five Tone.js synthesizers: an ambient drone (DuoSynth) that plays continuously through filters and reverb, a membrane synth for subtle connection pulses, a rhythm synth triggered by an Euclidean 5-beat pattern, a pluck synth for interactive clicks, and a noise synth for insight emergence effects.
  2. Every frame, draw() clears the canvas with a dark background and draws organic flowing lines using perlin noise to create the illusion of continuous data processing.
  3. The code updates each node's position by adding small random offsets (a random walk), constraining them within the canvas bounds, then draws them as small glowing circles that pulse in brightness.
  4. For every pair of nodes within connectionRadius (100 pixels), the sketch draws a connection line whose opacity animates based on a sine wave, creating a flowing pulse effect along the connections.
  5. Every 240 frames, a new philosophical insight text fades in at the bottom of the canvas, accompanied by a subtle chime sound, radiating scan lines that pulse from the center, and optional sparkle effects.
  6. The visual rhythm indicator shows an Euclidean 5-beat pattern as dots arranged in a circle, with the current step highlighted in electric blue and pulsing on active beats.
  7. When you click the canvas, a white expanding circle (query pulse) emanates from your cursor, the query synth plays a pluck sound, and Tone.js audio context starts if not already running.

🎓 Concepts You'll Learn

Particle systems and animationDistance-based geometry (collision detection)Perlin noise and organic randomnessTone.js synthesizers and audio effectsEuclidean rhythm patternsText rendering and fade effectsCanvas transformations and push/pop

📝 Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes the canvas, creates all visual objects (nodes), sets up all audio synthesizers and effects chains, and starts playback of the ambient drone and rhythm pattern. Understanding how Tone.js synthesizers are created and chained together here is key to making your own soundscapes.

function setup() {
  console.log("Setup start"); // DEBUG LOG
  createCanvas(windowWidth, windowHeight);
  
  // Create nodes with random positions
  for (let i = 0; i < numNodes; i++) {
    nodes.push(createVector(random(width), random(height)));
  }
  
  // Set initial insight
  currentInsight = random(insights);
  insightDisplayTime = frameCount;

  // Tone.js setup - Ensure Tone.start() is called on user interaction
  // It's already in mousePressed, which is good.

  // Main ambient drone representing continuous processing
  ambientDuoSynth = new Tone.DuoSynth({
    voice0: {
      oscillator: { type: "sine" },
      envelope: { attack: 2, decay: 4, sustain: 0.5, release: 8 },
      volume: -15
    },
    voice1: {
      oscillator: { type: "sine", detune: 5 }, // Subtle detune for richness
      envelope: { attack: 2, decay: 4, sustain: 0.5, release: 8 },
      volume: -15
    },
    vibratoAmount: 0.5,
    vibratoRate: 0.1,
    volume: -10 // Overall volume
  });

  filter = new Tone.Filter(200, "lowpass").toDestination();
  let reverb = new Tone.Reverb(4).toDestination();
  let delay = new Tone.FeedbackDelay("8n", 0.5).toDestination();

  ambientDuoSynth.chain(filter, reverb, delay, Tone.Destination);

  // Play a sustained note (C3) indefinitely
  ambientDuoSynth.triggerAttack("C3");

  // Setup click synth for subtle connection pulses (data packets)
  clickSynth = new Tone.MembraneSynth({
    envelope: { attack: 0.005, decay: 0.1, sustain: 0, release: 0.1 },
    pitchDecay: 0.05
  }).toDestination();
  clickSynth.volume.value = -20; // Make clicks very subtle

  // Setup rhythmic synth for the "puzzles" aspect - low frequency pulse
  rhythmSynth = new Tone.MembraneSynth({
    envelope: { attack: 0.01, decay: 0.2, sustain: 0, release: 0.3 },
    pitchDecay: 0.01
  }).toDestination();
  rhythmSynth.volume.value = -22; // Slightly increase volume to make it more noticeable

  // Setup synth for interactive query pulse
  querySynth = new Tone.PluckSynth({
    attackNoise: 1,
    envelope: { attack: 0.005, decay: 0.4, sustain: 0, release: 0.5 },
    dampening: 4000
  }).toDestination();
  querySynth.volume.value = -15; // Make query sound audible

  // Setup sparkle synth for insight emergence
  sparkleSynth = new Tone.NoiseSynth({
    noise: { type: "pink" },
    envelope: { attack: 0.001, decay: 0.1, sustain: 0, release: 0.2 },
    volume: -20
  }).toDestination();

  // Create a Tone.Sequence for the Euclidean rhythm
  let rhythmNotes = euclideanPattern.map(hit => hit ? "C1" : null);

  let rhythmSequence = new Tone.Sequence((time, note) => {
    if (note) {
      rhythmSynth.triggerAttackRelease(note, "8n", time);
    }
    // Update rhythm step for visual indicator
    currentRhythmStep = (currentRhythmStep + 1) % euclideanPattern.length;
  }, rhythmNotes, "8n"); // Schedule each step as an 8th note
  rhythmSequence.loop = true;
  rhythmSequence.start(0); // Start the sequence at the beginning of the transport
  console.log("Setup complete"); // DEBUG LOG
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Create random nodes for (let i = 0; i < numNodes; i++) { nodes.push(createVector(random(width), random(height))); }

Populates the nodes array with 100 p5.Vector objects at random positions across the canvas

calculation Configure ambient drone synthesizer ambientDuoSynth = new Tone.DuoSynth({...});

Creates a dual-voice sine wave synthesizer with slow attack/decay envelopes and subtle detuning for a rich, evolving ambient sound

calculation Set up Euclidean rhythm sequencer let rhythmSequence = new Tone.Sequence((time, note) => {...}, rhythmNotes, "8n");

Creates a looping rhythmic pattern that triggers the rhythm synth at Euclidean intervals and tracks the current beat step

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window, making the visualization fullscreen and responsive
for (let i = 0; i < numNodes; i++) { nodes.push(createVector(random(width), random(height))); }
Loops 100 times, creating a new p5.Vector (representing x,y position) with random coordinates and adding it to the nodes array
currentInsight = random(insights);
Picks one random philosophical quote from the insights array to display when the sketch starts
ambientDuoSynth.triggerAttack("C3");
Starts the ambient synthesizer playing a low C note indefinitely—this is the foundational drone you hear throughout
rhythmSequence.start(0);
Starts the Euclidean rhythm pattern playing from the beginning of Tone.js's transport (internal clock)

draw()

draw() runs 60 times per second (every frame). It is the heart of the sketch: it clears the canvas, updates all positions and audio parameters, draws every visual element, and checks for state changes (like when to display a new insight). Understanding how it orchestrates background rendering, particle animation, distance-based connections, text effects, and audio synchronization is key to building your own interactive visualizations.

🔬 The numbers 4 and 8 set the min/max node size. What happens if you change them to 2 and 20? The effect of changing 0.1 multiplier: try 0.05 (slower pulse) or 0.2 (faster pulse).

  for (let i = 0; i < nodes.length; i++) {
    // Animate node size/color for a "pulse" effect
    let pulseFactor = sin(frameCount * 0.1 + i * 0.2);
    let nodeSize = map(pulseFactor, -1, 1, 4, 8);
    let nodeColor = map(pulseFactor, -1, 1, 150, 255);
    fill(nodeColor, nodeColor, 255, 200); // Bright Electric Blue nodes
    ellipse(nodes[i].x, nodes[i].y, nodeSize);
  }
function draw() {
  console.log("Draw start"); // DEBUG LOG

  // Always clear the background first
  background(10, 10, 30); // Dark background for contrast

  // Dynamic background texture (representing microscopic data processing)
  // Drawn on top of the cleared background
  let noiseScale = 0.01;
  for (let x = 0; x < width; x += 10) {
    for (let y = 0; y < height; y += 10) {
      let v = noise(x * noiseScale, y * noiseScale, frameCount * 0.005);
      let c = map(v, 0, 1, 0, 50);
      noStroke();
      fill(10, 10, 30, c); // Dark background with subtle noise
      rect(x, y, 10, 10);
    }
  }
  console.log("Draw after background texture"); // DEBUG LOG

  // Draw ephemeral "Flow Trails" - symbolizing continuous data flow
  push();
  stroke(100, 100, 255, 10); // Very faint Electric Blue trails
  strokeWeight(0.75); // Slightly thicker trails
  let flowNoiseScale = 0.005;
  for (let i = 0; i < 50; i++) { // Draw more trails
    let x1 = noise(frameCount * 0.001 + i * 100) * width;
    let y1 = noise(frameCount * 0.001 + i * 200) * height;
    let x2 = noise(frameCount * 0.001 + i * 300) * width;
    let y2 = noise(frameCount * 0.001 + i * 400) * height;
    line(x1, y1, x2, y2);
  }
  pop();
  console.log("Draw after flow trails"); // DEBUG LOG

  // Update ambient synth parameters subtly over time for evolution
  let timeFactor = frameCount * 0.001;
  let filterFreq = map(sin(timeFactor * 0.7), -1, 1, 100, 800);
  filter.set({ frequency: filterFreq });

  // Subtle volume modulation for the drone
  let volumeShift = map(sin(timeFactor * 0.5), -1, 1, -18, -12);
  ambientDuoSynth.set({ volume: volumeShift });
  console.log("Draw after ambient synth update"); // DEBUG LOG

  // Check if it's time to change the insight
  if (frameCount - insightDisplayTime > insightDuration) {
    currentInsight = random(insights);
    insightDisplayTime = frameCount;
    // Subtle chime sound and sparkle when a new insight emerges
    let chime = new Tone.MembraneSynth().toDestination();
    chime.triggerAttackRelease("C4", "8n");
    chime.volume.value = -10; // Make chime audible
    // sparkleSynth.triggerAttackRelease("4n"); // COMMENTED OUT FOR DEBUGGING
  }
  console.log("Draw after insight logic"); // DEBUG LOG

  // Visual "Querying the Void / Finding the Signal" effect when insight appears
  if (frameCount - insightDisplayTime < 60) { // Display effect for 1 second (60 frames)
    let scanProgress = map(frameCount - insightDisplayTime, 0, 60, 0, 1);
    let scanRadius = map(sin(scanProgress * PI), 0, 1, 0, width * 0.4); // Pulsing radius
    let scanAlpha = map(scanProgress, 0, 1, 200, 0); // Fading alpha

    push();
    noFill();
    stroke(255, 255, 255, scanAlpha); // White scan lines/circles
    strokeWeight(2);
    // Radiating lines represent signal search
    for (let a = 0; a < TWO_PI; a += PI / 8) {
      let x1 = width / 2 + cos(a) * (scanRadius * 0.5);
      let y1 = height / 2 + sin(a) * (scanRadius * 0.5);
      let x2 = width / 2 + cos(a) * scanRadius;
      let y2 = height / 2 + sin(a) * scanRadius;
      line(x1, y1, x2, y2);
    }
    pop();
  }
  console.log("Draw after querying void effect"); // DEBUG LOG

  // Animate the insight text display
  let insightAlpha = 255;
  let insightSize = 24;
  let elapsed = frameCount - insightDisplayTime;
  if (elapsed < 30) { // Fade in effect
    insightAlpha = map(elapsed, 0, 30, 0, 255);
    insightSize = map(elapsed, 0, 30, 18, 26); // More pronounced growth
  } else if (elapsed > insightDuration - 30) { // Fade out effect
    insightAlpha = map(elapsed, insightDuration - 30, insightDuration, 255, 0);
    insightSize = map(elapsed, insightDuration - 30, insightDuration, 26, 18); // More pronounced shrinkage
  }

  fill(255, insightAlpha);
  noStroke();
  textAlign(CENTER, CENTER);
  textSize(insightSize);
  text(currentInsight, width / 2, height - 50); // Display insight at the bottom
  console.log("Draw after insight text"); // DEBUG LOG

  // Visual Euclidean Rhythm Indicator
  push();
  let indicatorX = width / 2;
  let indicatorY = height - 100;
  let indicatorBaseSize = 12; // Slightly larger base
  let indicatorPulseSize = 6; // Slightly larger pulse
  let indicatorAlpha = 150;

  // Check if current step is a beat
  if (euclideanPattern[currentRhythmStep]) {
    // Pulse effect for the beat
    let pulseFactor = sin(frameCount * 0.1);
    indicatorBaseSize = map(pulseFactor, -1, 1, 12, 18); // More pronounced pulse
    indicatorAlpha = map(pulseFactor, -1, 1, 150, 255);
  }

  fill(255, indicatorAlpha);
  noStroke();
  ellipse(indicatorX, indicatorY, indicatorBaseSize); // Base indicator
  
  // Draw all steps as small dots, highlight current step
  let stepAngle = TWO_PI / euclideanPattern.length;
  for (let i = 0; i < euclideanPattern.length; i++) {
    let x = indicatorX + cos(i * stepAngle) * (indicatorBaseSize * 2.5); // Further out
    let y = indicatorY + sin(i * stepAngle) * (indicatorBaseSize * 2.5);
    if (i === currentRhythmStep) {
      fill(100, 100, 255, 255); // Highlight current step with Electric Blue
    } else {
      fill(200, 200, 200, 100); // Other steps
    }
    ellipse(x, y, indicatorPulseSize);
  }
  pop();
  console.log("Draw after rhythm indicator"); // DEBUG LOG

  // Draw active query pulses
  for (let i = activeQueries.length - 1; i >= 0; i--) {
    let query = activeQueries[i];
    push();
    noFill();
    stroke(255, 255, 255, query.alpha);
    strokeWeight(1);
    ellipse(query.x, query.y, query.radius * 2);
    query.radius += 5;
    query.alpha -= 10;
    if (query.alpha <= 0) {
      activeQueries.splice(i, 1); // Remove query when it fades out
    }
    pop();
  }
  console.log("Draw after query pulses"); // DEBUG LOG

  // --- Galaxy Shifting Block ---
  push();
  translate(width / 2, height / 2);
  let galaxyAngle = frameCount * 0.0005; // Very subtle continuous rotation
  rotate(galaxyAngle);
  translate(-width / 2, -height / 2); // Translate back to top-left for drawing relative to canvas
  console.log("Draw inside galaxy block - after transformations"); // DEBUG LOG

  // Update node positions (subtle random walk for continuous processing)
  for (let i = 0; i < nodes.length; i++) {
    nodes[i].x += random(-0.3, 0.3); // Slow horizontal drift
    nodes[i].y += random(-0.3, 0.3); // Slow vertical drift
    
    // Constrain nodes within canvas bounds
    nodes[i].x = constrain(nodes[i].x, 0, width);
    nodes[i].y = constrain(nodes[i].y, 0, height);
  }
  console.log("Draw inside galaxy block - after node updates"); // DEBUG LOG

  // Draw connections between nodes
  stroke(100, 100, 255, 50); // Faint Electric Blue connections
  strokeWeight(1);
  connectionPulseCounter++; // Increment counter for click sound triggering

  for (let i = 0; i < nodes.length; i++) {
    for (let j = i + 1; j < nodes.length; j++) {
      let d = dist(nodes[i].x, nodes[i].y, nodes[j].x, nodes[j].y);
      if (d < connectionRadius) {
        // Animate connection transparency/color for a "flow" effect
        let flowFactor = sin(frameCount * 0.05 + i * 0.1 + j * 0.1);
        let alpha = map(flowFactor, -1, 1, 30, 100);
        stroke(100, 100, 255, alpha); // Electric Blue connections
        line(nodes[i].x, nodes[i].y, nodes[j].x, nodes[j].y);

        // Add a subtle click sound when connection pulses strongly
        // Trigger less frequently to avoid audio overload
        if (alpha > 90 && connectionPulseCounter % 30 === 0) {
          clickSynth.triggerAttackRelease("C2", "32n"); // Shorter, subtle click
        }
      }
    }
  }
  console.log("Draw inside galaxy block - after connections"); // DEBUG LOG

  // Draw nodes
  noStroke();
  for (let i = 0; i < nodes.length; i++) {
    // Animate node size/color for a "pulse" effect
    let pulseFactor = sin(frameCount * 0.1 + i * 0.2);
    let nodeSize = map(pulseFactor, -1, 1, 4, 8);
    let nodeColor = map(pulseFactor, -1, 1, 150, 255);
    fill(nodeColor, nodeColor, 255, 200); // Bright Electric Blue nodes
    ellipse(nodes[i].x, nodes[i].y, nodeSize);
  }
  console.log("Draw inside galaxy block - after nodes"); // DEBUG LOG

  // Draw the Central "Core" of Flow - Electric Blue heart
  push(); // Push again for core's own pulse/alpha
  let coreSize = map(sin(frameCount * 0.05), -1, 1, 25, 35); // More pronounced pulse
  let coreAlpha = map(sin(frameCount * 0.03), -1, 1, 150, 255); // More pronounced pulse
  noStroke();
  fill(100, 100, 255, coreAlpha); // Electric Blue core
  ellipse(width / 2, height / 2, coreSize); // Central core
  pop(); // Pop for core's own transformations
  console.log("Draw inside galaxy block - after core"); // DEBUG LOG

  pop(); // Pop for galaxy transformation
  console.log("Draw end of galaxy block"); // DEBUG LOG

  console.log("Draw loop complete"); // DEBUG LOG
}
Line-by-line explanation (16 lines)

🔧 Subcomponents:

for-loop Perlin noise background texture for (let x = 0; x < width; x += 10) { for (let y = 0; y < height; y += 10) { let v = noise(x * noiseScale, y * noiseScale, frameCount * 0.005); let c = map(v, 0, 1, 0, 50); noStroke(); fill(10, 10, 30, c); rect(x, y, 10, 10); } }

Creates a slowly-evolving grid of semi-transparent rectangles using 3D perlin noise (x, y, and time) to simulate microscopic data activity

for-loop Flowing trails effect for (let i = 0; i < 50; i++) { let x1 = noise(frameCount * 0.001 + i * 100) * width; let y1 = noise(frameCount * 0.001 + i * 200) * height; let x2 = noise(frameCount * 0.001 + i * 300) * width; let y2 = noise(frameCount * 0.001 + i * 400) * height; line(x1, y1, x2, y2); }

Draws 50 animated lines that flow smoothly across the canvas using time-based perlin noise, giving the illusion of data streams

conditional Insight change check if (frameCount - insightDisplayTime > insightDuration) { currentInsight = random(insights); insightDisplayTime = frameCount; let chime = new Tone.MembraneSynth().toDestination(); chime.triggerAttackRelease("C4", "8n"); }

Every 240 frames, picks a new philosophical insight, records the current frame number, and plays a chime sound

for-loop Radiating scan lines effect for (let a = 0; a < TWO_PI; a += PI / 8) { let x1 = width / 2 + cos(a) * (scanRadius * 0.5); let y1 = height / 2 + sin(a) * (scanRadius * 0.5); let x2 = width / 2 + cos(a) * scanRadius; let y2 = height / 2 + sin(a) * scanRadius; line(x1, y1, x2, y2); }

Draws 16 radiating lines from the center that pulse outward and fade when a new insight appears, representing signal search

conditional Insight text fade in/out if (elapsed < 30) { insightAlpha = map(elapsed, 0, 30, 0, 255); insightSize = map(elapsed, 0, 30, 18, 26); } else if (elapsed > insightDuration - 30) { insightAlpha = map(elapsed, insightDuration - 30, insightDuration, 255, 0); insightSize = map(elapsed, insightDuration - 30, insightDuration, 26, 18); }

Controls fade-in for the first 30 frames and fade-out for the last 30 frames of insight display, with accompanying size growth/shrinkage

for-loop Euclidean rhythm step visualization for (let i = 0; i < euclideanPattern.length; i++) { let x = indicatorX + cos(i * stepAngle) * (indicatorBaseSize * 2.5); let y = indicatorY + sin(i * stepAngle) * (indicatorBaseSize * 2.5); if (i === currentRhythmStep) { fill(100, 100, 255, 255); } else { fill(200, 200, 200, 100); } ellipse(x, y, indicatorPulseSize); }

Arranges 8 small dots in a circle representing the Euclidean rhythm steps, highlighting the current step in bright blue

for-loop Expand and fade query circles for (let i = activeQueries.length - 1; i >= 0; i--) { let query = activeQueries[i]; push(); noFill(); stroke(255, 255, 255, query.alpha); strokeWeight(1); ellipse(query.x, query.y, query.radius * 2); query.radius += 5; query.alpha -= 10; if (query.alpha <= 0) { activeQueries.splice(i, 1); } pop(); }

Updates and draws all expanding/fading circles created by mouse clicks, removing them when they become invisible

for-loop Update node positions with random walk for (let i = 0; i < nodes.length; i++) { nodes[i].x += random(-0.3, 0.3); nodes[i].y += random(-0.3, 0.3); nodes[i].x = constrain(nodes[i].x, 0, width); nodes[i].y = constrain(nodes[i].y, 0, height); }

Applies tiny random movements to each node and keeps them within canvas bounds, creating organic drifting motion

for-loop Draw connections between nearby nodes for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { let d = dist(nodes[i].x, nodes[i].y, nodes[j].x, nodes[j].y); if (d < connectionRadius) { let flowFactor = sin(frameCount * 0.05 + i * 0.1 + j * 0.1); let alpha = map(flowFactor, -1, 1, 30, 100); stroke(100, 100, 255, alpha); line(nodes[i].x, nodes[i].y, nodes[j].x, nodes[j].y); if (alpha > 90 && connectionPulseCounter % 30 === 0) { clickSynth.triggerAttackRelease("C2", "32n"); } } } }

Nested loop that checks every pair of nodes: if they're within connectionRadius, draws a pulsing connection line between them and occasionally triggers a subtle click sound

for-loop Draw pulsing nodes for (let i = 0; i < nodes.length; i++) { let pulseFactor = sin(frameCount * 0.1 + i * 0.2); let nodeSize = map(pulseFactor, -1, 1, 4, 8); let nodeColor = map(pulseFactor, -1, 1, 150, 255); fill(nodeColor, nodeColor, 255, 200); ellipse(nodes[i].x, nodes[i].y, nodeSize); }

Draws each node as a glowing electric blue circle whose brightness and size oscillate using a sine wave offset for each node

background(10, 10, 30);
Clears the canvas every frame with a very dark navy blue color, erasing the previous frame's drawings
let v = noise(x * noiseScale, y * noiseScale, frameCount * 0.005);
Uses 3D perlin noise (x position, y position, and time evolving via frameCount) to generate a smooth random value between 0 and 1
let filterFreq = map(sin(timeFactor * 0.7), -1, 1, 100, 800);
Maps a slowly-oscillating sine wave to a frequency range (100–800 Hz), making the ambient filter sweep up and down for tonal evolution
if (frameCount - insightDisplayTime > insightDuration) {
Checks if enough frames have passed since the last insight was displayed; if so, picks a new one and resets the timer
let scanRadius = map(sin(scanProgress * PI), 0, 1, 0, width * 0.4);
Creates a pulsing radius using a sine wave that starts at 0 and reaches 40% of the canvas width, then back to 0 over 60 frames
if (elapsed < 30) { insightAlpha = map(elapsed, 0, 30, 0, 255); insightSize = map(elapsed, 0, 30, 18, 26); }
During the first 30 frames of an insight, the text fades in from invisible to fully visible while growing from size 18 to 26
let stepAngle = TWO_PI / euclideanPattern.length;
Divides a full circle (TWO_PI radians) by 8 steps to space the rhythm indicator dots evenly around a circle
translate(width / 2, height / 2);
Moves the origin point to the center of the canvas, so all subsequent drawings are relative to the center
let galaxyAngle = frameCount * 0.0005;
Calculates a very slow rotation angle that increases by 0.0005 radians every frame, completing a full rotation every ~12,500 frames
rotate(galaxyAngle);
Applies the calculated rotation to all subsequent drawings, making the entire node network slowly spin around the canvas center
translate(-width / 2, -height / 2);
Moves the origin back to the top-left corner after the rotation, so nodes still draw at their actual x,y coordinates
nodes[i].x += random(-0.3, 0.3);
Adds a tiny random value (-0.3 to +0.3 pixels) to each node's x position every frame, creating a slow, organic drift
let d = dist(nodes[i].x, nodes[i].y, nodes[j].x, nodes[j].y);
Calculates the Euclidean distance between two nodes using the dist() function, returning a number in pixels
if (d < connectionRadius) {
Only draws a connection line if the two nodes are closer than 100 pixels apart
let flowFactor = sin(frameCount * 0.05 + i * 0.1 + j * 0.1);
Creates an animated sine wave that's unique to each pair of nodes (different phase offsets based on i and j), controlling connection opacity
if (alpha > 90 && connectionPulseCounter % 30 === 0) {
Triggers a subtle click sound only when a connection is bright (alpha > 90) and only every 30 frames to avoid audio overload

mousePressed()

mousePressed() is a p5.js built-in function that runs once every time the user clicks the canvas. Here it serves two purposes: ensuring Tone.js audio is initialized (required by most browsers for security), and creating an interactive visual and audio response at the click location. This is how you give users feedback for their actions.

function mousePressed() {
  // Ensure audio context is running on user interaction
  if (Tone.context.state !== 'running') {
    Tone.start();
  }

  // Trigger query sound and add a new query pulse
  querySynth.triggerAttackRelease("G4", "8n");
  activeQueries.push({ x: mouseX, y: mouseY, radius: 0, alpha: 200 });
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Initialize audio context if (Tone.context.state !== 'running') { Tone.start(); }

Ensures Tone.js audio is enabled; some browsers require user interaction to start audio, so this runs on first click

calculation Create expanding pulse at cursor activeQueries.push({ x: mouseX, y: mouseY, radius: 0, alpha: 200 });

Adds a new query object to the array with the cursor position and initial size/opacity for the draw loop to animate

if (Tone.context.state !== 'running') {
Checks if Tone.js's audio context is already running; on first user interaction it might not be
Tone.start();
Starts Tone.js's internal clock and initializes the audio context, allowing all synths to play sound
querySynth.triggerAttackRelease("G4", "8n");
Plays a pluck synth sound at a high G note (G4) for the duration of an 8th note, creating an immediate audio response to the click
activeQueries.push({ x: mouseX, y: mouseY, radius: 0, alpha: 200 });
Creates a new query pulse object with the current mouse position and adds it to the activeQueries array for the draw loop to animate and fade

windowResized()

windowResized() is a p5.js built-in function that runs automatically whenever the browser window is resized. Here it keeps the sketch responsive by resizing the canvas and repositioning all nodes to fit the new space. Without this, the network would be cut off or leave empty space if you resize the browser.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  
  // Re-generate nodes for the new canvas size
  nodes = [];
  for (let i = 0; i < numNodes; i++) {
    nodes.push(createVector(random(width), random(height)));
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

calculation Resize canvas to fill window resizeCanvas(windowWidth, windowHeight);

Updates the p5.js canvas dimensions to match the current browser window size when the user resizes their window

for-loop Regenerate nodes for new space nodes = []; for (let i = 0; i < numNodes; i++) { nodes.push(createVector(random(width), random(height))); }

Clears the nodes array and repopulates it with 100 new random positions, ensuring nodes fill the resized canvas properly

resizeCanvas(windowWidth, windowHeight);
Updates p5.js's internal canvas size to match the browser window's new width and height
nodes = [];
Empties the nodes array completely, discarding all old node positions
for (let i = 0; i < numNodes; i++) { nodes.push(createVector(random(width), random(height))); }
Creates 100 new nodes with random positions within the new canvas dimensions, so the network adapts to the resized space

📦 Key Variables

nodes array

Stores 100 p5.Vector objects representing the x,y positions of animated particles in the network

let nodes = [];
numNodes number

How many particles populate the network visualization—increasing this creates a denser, more complex graph

let numNodes = 100;
connectionRadius number

Maximum pixel distance at which two nodes will draw a connecting line—larger values create more connections

let connectionRadius = 100;
insights array

An array of 10 philosophical quotes that fade in and out at the bottom of the screen throughout the sketch

const insights = ["Emergent patterns from data streams.", ...];
currentInsight string

Stores the currently-displayed philosophical quote

let currentInsight = "";
insightDisplayTime number

Stores the frameCount when the current insight started displaying, used to calculate fade effects and duration

let insightDisplayTime = 0;
insightDuration number

How many frames (at 60fps) each insight text remains visible before fading out and a new one appears

let insightDuration = 240;
ambientDuoSynth object

A Tone.js DuoSynth that plays the continuous ambient drone—two sine waves playing at slightly different pitches for richness

let ambientDuoSynth;
filter object

A Tone.js lowpass filter that modulates the ambient drone's tone over time, making it evolve and shift

let filter;
clickSynth object

A Tone.js MembraneSynth that produces subtle click sounds whenever connections between nodes pulse strongly

let clickSynth;
rhythmSynth object

A Tone.js MembraneSynth that plays rhythmic pulses in an Euclidean pattern at regular intervals

let rhythmSynth;
querySynth object

A Tone.js PluckSynth that plays a pluck sound whenever the user clicks the canvas

let querySynth;
sparkleSynth object

A Tone.js NoiseSynth configured to produce a sparkle effect when insights emerge (currently commented out)

let sparkleSynth;
euclideanPattern array

A pre-calculated array of 8 true/false values representing which beats sound in a Euclidean 5-beat-in-8-steps pattern

let euclideanPattern = [true, false, true, false, true, false, true, false];
currentRhythmStep number

Tracks which step (0–7) of the Euclidean rhythm is currently playing, used to highlight the rhythm indicator

let currentRhythmStep = 0;
activeQueries array

Stores active query pulse objects created by mouse clicks, each with x, y, radius, and alpha properties for animation

let activeQueries = [];
connectionPulseCounter number

Increments every frame to trigger click sounds at regular intervals (every 30 frames) when connections pulse

let connectionPulseCounter = 0;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

PERFORMANCE draw() - background texture loop

The nested loop drawing 10×10 pixel rectangles with perlin noise recalculates every frame, even though the visual change is minimal. For a 1920×1080 canvas, this is ~20,000 rect() calls per frame, which can cause stuttering.

💡 Render the noise texture to an offscreen buffer (createGraphics) once every 5–10 frames, or use a lower resolution grid (increase x/y step from 10 to 20 or 30 pixels) to reduce draw calls.

BUG draw() - connection drawing nested loop

Excessive console.log() statements inside the nested loop log tens of thousands of messages per frame (up to 100 nodes × 100 nodes = 10,000 checks), making the console unusable and slowing performance. The DEBUG logs should be removed or conditioned on specific events.

💡 Remove all console.log() statements or wrap them in a conditional (e.g., if (DEBUG_MODE) console.log(...)) that defaults to false.

FEATURE draw() - insight emergence

The sparkleSynth.triggerAttackRelease() line is commented out (line ~158), so the sparkle sound effect for insight emergence never plays even though the sparkle synth is configured.

💡 Uncomment the sparkleSynth line or add a condition: if (true) sparkleSynth.triggerAttackRelease("4n"); to restore the full audio experience.

STYLE Global constants

Magic numbers are scattered throughout the code (connection click frequency of 30, rhythm pattern hardcoded, indicator sizes, fade durations). These make the sketch hard to tune and understand at a glance.

💡 Extract frequently-tweaked values into named constants at the top: const CLICK_FREQUENCY = 30; const INSIGHT_FADE_FRAMES = 30; const NODE_DRIFT_AMOUNT = 0.3; This makes the sketch more readable and easier to customize.

PERFORMANCE setup() - Tone.js reverb and delay

Multiple Tone.js effects (reverb, delay, filter) are created and routed, but they are not connected to each other optimally. The ambientDuoSynth chains to filter, reverb, delay, and Tone.Destination, but if other synths (clickSynth, rhythmSynth, etc.) also route to Destination independently, the reverb tail from the drone may not apply uniformly.

💡 Create a single reverb/delay return chain and route all synths through it: ambientDuoSynth.chain(filter, reverbDelay); clickSynth.chain(reverbDelay); etc. Or explicitly route all synths to the same Reverb instance for cohesive spatial effects.

BUG draw() - galaxy rotation transformations

The push/pop for the galaxy block applies transformations correctly, but if the window is resized during a rotate state, the nodes are regenerated at new positions while still in a rotated coordinate space. This could cause nodes to briefly appear in unexpected locations.

💡 Add resetMatrix() at the start of windowResized() to ensure no transforms are active when nodes are regenerated: resetMatrix(); resizeCanvas(...);

🔄 Code Flow

Code flow showing setup, draw, mousepressed, windowresized

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

graph TD start[Start] --> setup[setup] setup --> audio-context-check[audio-context-check] audio-context-check --> node-creation-loop[node-creation-loop] node-creation-loop --> ambient-synth-config[ambient-synth-config] ambient-synth-config --> rhythm-sequence-config[rhythm-sequence-config] rhythm-sequence-config --> draw[draw loop] draw --> background-noise-loop[background-noise-loop] draw --> flow-trails-loop[flow-trails-loop] draw --> insight-logic[insight-logic] draw --> scan-effect-loop[scan-effect-loop] draw --> text-fade-logic[text-fade-logic] draw --> rhythm-indicator-loop[rhythm-indicator-loop] draw --> query-pulses-loop[query-pulses-loop] draw --> node-update-loop[node-update-loop] draw --> connection-drawing-loop[connection-drawing-loop] draw --> node-drawing-loop[node-drawing-loop] draw --> windowresized[windowresized] windowresized --> canvas-resize-logic[canvas-resize-logic] canvas-resize-logic --> node-regeneration-loop[node-regeneration-loop] node-regeneration-loop --> node-creation-loop mousepressed[mousepressed] --> audio-context-check click setup href "#fn-setup" click draw href "#fn-draw" click mousepressed href "#fn-mousepressed" click windowresized href "#fn-windowresized" click node-creation-loop href "#sub-node-creation-loop" click ambient-synth-config href "#sub-ambient-synth-config" click rhythm-sequence-config href "#sub-rhythm-sequence-config" click background-noise-loop href "#sub-background-noise-loop" click flow-trails-loop href "#sub-flow-trails-loop" click insight-logic href "#sub-insight-logic" click scan-effect-loop href "#sub-scan-effect-loop" click text-fade-logic href "#sub-text-fade-logic" click rhythm-indicator-loop href "#sub-rhythm-indicator-loop" click query-pulses-loop href "#sub-query-pulses-loop" click node-update-loop href "#sub-node-update-loop" click connection-drawing-loop href "#sub-connection-drawing-loop" click node-drawing-loop href "#sub-node-drawing-loop" click canvas-resize-logic href "#sub-canvas-resize-logic" click node-regeneration-loop href "#sub-node-regeneration-loop"

❓ Frequently Asked Questions

What kind of visual experience does the p5.js sketch create?

The sketch generates an abstract visualization of interconnected nodes, representing the flow of information and creative processes through dynamic patterns and movements.

How can users interact with this p5.js creative coding sketch?

Users can interact with the sketch by clicking, which triggers rhythmic elements and highlights connections between nodes, while also revealing insights related to data processing.

What creative coding techniques are showcased in this sketch?

The sketch demonstrates concepts such as node-based visualization, dynamic audio synthesis with Tone.js, and the generation of emergent patterns from interconnected data points.

Preview

Sketch 2026-02-08 21:36 - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Sketch 2026-02-08 21:36 - Code flow showing setup, draw, mousepressed, windowresized
Code Flow Diagram