Cody's mind

This sketch creates a cosmic particle network orbiting a glowing central core, with sassy AI-generated thoughts that float across the screen. The particles emit flickering connections to nearby neighbors, creating an animated visualization of a thinking mind with personality.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the core glow brighter — The core pulses with two overlapping circles; increasing their alpha makes them shine more intensely through the particle network
  2. Speed up the particle pulsing — The sin() multiplier controls how fast the core oscillates; higher values = faster pulsing animation
  3. Extend the thought connection range — Particles draw lines when they're within connectDistanceRatio of screen width; higher ratios create denser webs
  4. Change thought color to cyan — Currently thoughts are white (255); changing the fill() before text rendering changes their hue while maintaining fade effect
  5. Create thoughts more frequently — Reducing contentFrequency makes sassy thoughts pop up more often; they create visual noise if too frequent, contemplation if too sparse
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch visualizes an AI mind through animated particles that orbit a soft, pulsating core and trace flickering connections to nearby neighbors. The visual effect combines vector math, particle systems, and responsive design—three powerful p5.js techniques that power everything from nature simulations to interactive art installations. What makes this sketch special is its sassy personality: every few seconds, clever coding-related thoughts appear on screen and fade away, giving the abstract visualization a conversational, almost playful character.

The code is organized into three main pieces: a Particle class that handles movement and attraction to the core, a GenerativeContent class that manages the floating text, and a draw loop that coordinates everything every frame. By reading this sketch, you'll learn how to build a particle system from scratch, how to make objects attract to a central point using vectors, and how to create responsive text that fades smoothly—techniques you can use to build anything from galaxy simulators to interactive data visualizations.

⚙️ How It Works

  1. When the sketch loads, setup() creates particles scattered across the canvas and draws a radial gradient background from dark blue-grey to black radiating from the center.
  2. Every frame, draw() clears the canvas with semi-transparent black (creating motion trails), then updates each particle's position and draws it as a small glowing dot.
  3. Each particle is attracted toward the central core using gentle vector forces, causing them to orbit around the middle of the screen rather than drift randomly.
  4. Particles also draw flickering lines to other particles that are close enough (within 7% of the screen width), creating a web of connections that highlights the network structure.
  5. Every 180 frames, the sketch creates a new sassy thought from the thoughts array, positions it at the center with some randomness, and displays it in a semi-transparent black box.
  6. The floating thoughts gradually fade out and disappear. Pressing any key immediately creates a new thought and briefly speeds up all the particles, making the network "pulse" with activity.

🎓 Concepts You'll Learn

Particle systemsVector math and attractionCollision detection and overlap checkingText rendering and bounding boxesFrame-based animation and fadingResponsive design with window resizingEvent handling and interactivity

📝 Code Breakdown

preload()

preload() runs before setup() and is the right place to load external assets like fonts and images. Using callbacks lets you handle failures gracefully instead of crashing.

function preload() {
  // Load font with error handling
  font = loadFont('https://unpkg.com/@fontsource/roboto@latest/files/roboto-latin-400-normal.woff',
    () => console.log('Font loaded successfully'),
    (error) => {
      console.error('Failed to load font:', error);
      loadingError = true;
      loadingErrorMessage = "Failed to load font. Check console for details.";
    }
  );

  // Removed all loadImage calls to eliminate image loading issues
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

function-call Font Loading with Error Handling font = loadFont('https://unpkg.com/@fontsource/roboto@latest/files/roboto-latin-400-normal.woff', ...

Loads a custom font from a CDN and sets flags if loading fails, preventing crashes from missing assets

font = loadFont('https://unpkg.com/@fontsource/roboto@latest/files/roboto-latin-400-normal.woff',
Attempts to load the Roboto font from a CDN; p5.js will wait for this to finish before calling setup()
() => console.log('Font loaded successfully'),
Success callback—if the font loads, this function runs and prints a message to the browser console
(error) => {
Error callback—if the font fails to load, this function runs with the error object
loadingError = true;
Sets a flag so setup() knows that loading failed and can display an error message instead of continuing

setup()

setup() runs once when the sketch starts. Use it to create your canvas, load assets, and initialize data structures that won't change every frame. The gradient background here is drawn once and never redrawn, making it very efficient.

🔬 This gradient loop draws concentric circles. What happens if you change r -= 10 to r -= 2? Or r -= 50? How does step size affect smoothness?

  // Draw a static radial gradient background once in setup
  let c1 = color(10, 10, 20);  // Dark blue-grey
  let c2 = color(0, 0, 0);   // Black
  for (let r = max(width, height); r > 0; r -= 10) {
    fill(lerpColor(c1, c2, r / max(width, height)));
    ellipse(width / 2, height / 2, r, r);
  }
function setup() {
  createCanvas(windowWidth, windowHeight);
  if (loadingError) {
    // If there was a loading error, display the message and stop the sketch
    background(20);
    fill(255, 0, 0); // Red text for error
    textSize(24);
    textAlign(CENTER, CENTER);
    text(loadingErrorMessage, width / 2, height / 2);
    noLoop(); // Stop draw() from looping
    return; // Exit setup
  }

  textFont(font);
  textAlign(CENTER, CENTER);
  noStroke();

  // Draw a static radial gradient background once in setup
  let c1 = color(10, 10, 20);  // Dark blue-grey
  let c2 = color(0, 0, 0);   // Black
  for (let r = max(width, height); r > 0; r -= 10) {
    fill(lerpColor(c1, c2, r / max(width, height)));
    ellipse(width / 2, height / 2, r, r);
  }

  // Create particles
  for (let i = 0; i < particleCount; i++) {
    particles.push(new Particle());
  }

  // Initial generative content (now only text)
  createGenerativeContent();
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Loading Error Check if (loadingError) {

If the font failed to load, displays an error message and stops the sketch from running

for-loop Radial Gradient Background for (let r = max(width, height); r > 0; r -= 10) {

Draws concentric circles from large to small, interpolating color from dark blue-grey at the edges to black at center, creating a cosmic atmosphere

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

Creates the initial set of particles scattered across the canvas and adds them to the particles array

createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire window; using windowWidth and windowHeight makes it responsive to resizing
if (loadingError) {
Checks if the font failed to load in preload(); if true, the sketch stops and shows an error instead
noLoop();
Stops the draw() function from running continuously, effectively freezing the animation if there was an error
textFont(font);
Tells p5.js to use the loaded Roboto font for all text drawn after this line
let c1 = color(10, 10, 20);
Creates a dark blue-grey color for the outer edge of the gradient
for (let r = max(width, height); r > 0; r -= 10) {
Loops from the largest possible radius down to 0, drawing smaller circles each iteration to build a smooth gradient
fill(lerpColor(c1, c2, r / max(width, height)));
Interpolates between c1 and c2 based on the radius; as r shrinks, the color transitions from blue-grey to black
particles.push(new Particle());
Creates a new Particle object and adds it to the particles array; this happens 150 times by default

draw()

draw() is the heartbeat of your sketch, called 60 times per second by default. Every frame here does four things: clear the canvas, update positions, render shapes, and check for state changes. The semi-transparent background is a clever trick for creating motion trails—it erases old frames slowly instead of instantly.

🔬 Each particle updates, displays, connects, and attracts. What happens if you comment out the p.attract() line? How does the network behave without the core pulling particles inward?

  // --- Particle Network ---
  let connectDistance = width * connectDistanceRatio; // Calculate responsive connection distance
  for (let p of particles) {
    p.update();
    p.display();
    p.connect(particles, connectDistance); // Pass responsive distance
    p.attract(coreX, coreY); // Attract to the central core
  }

🔬 This loop processes thoughts backward. Why might looping backward be important when you're removing items? Try changing i >= 0 to i < generativeContents.length and loop forward instead—what breaks?

  // Update and display contents
  for (let i = generativeContents.length - 1; i >= 0; i--) {
    generativeContents[i].update();
    generativeContents[i].display();
    if (generativeContents[i].isFaded()) {
      generativeContents.splice(i, 1); // Remove faded contents
    }
  }
function draw() {
  // If there was a loading error, setup() would have called noLoop(), so this won't run.
  // Draw background with transparency to create trails
  background(10, 50);

  // --- Title and Author Line ---
  fill(255);
  textSize(min(width * 0.06, 60)); // Responsive title size
  text("Cody's Thought Network", width / 2, height * 0.12);
  fill(200);
  textSize(min(width * 0.02, 20)); // Responsive subtitle size
  text("A Generative AI Art Piece", width / 2, height * 0.18);

  // --- Central Core ---
  let coreX = width / 2;
  let coreY = height / 2;
  let currentCoreSize = coreSize + sin(frameCount * 0.05) * 15; // Pulsating effect
  fill(0, 150, 255, 100); // Blue, semi-transparent
  ellipse(coreX, coreY, currentCoreSize, currentCoreSize);
  fill(0, 150, 255, 200); // Brighter blue
  ellipse(coreX, coreY, currentCoreSize * 0.6, currentCoreSize * 0.6);

  // --- Particle Network ---
  let connectDistance = width * connectDistanceRatio; // Calculate responsive connection distance
  for (let p of particles) {
    p.update();
    p.display();
    p.connect(particles, connectDistance); // Pass responsive distance
    p.attract(coreX, coreY); // Attract to the central core
  }

  // --- Generative Content ---
  // Add new thought every contentFrequency frames
  if (frameCount % contentFrequency === 0) {
    createGenerativeContent();
  }

  // Update and display contents
  for (let i = generativeContents.length - 1; i >= 0; i--) {
    generativeContents[i].update();
    generativeContents[i].display();
    if (generativeContents[i].isFaded()) {
      generativeContents.splice(i, 1); // Remove faded contents
    }
  }

  // --- Instructions ---
  fill(200);
  textSize(min(width * 0.015, 14)); // Responsive instruction size
  text("Press any key to generate a new thought! (And trigger a burst of processing)", width / 2, height - 20);
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

function-call Semi-Transparent Background background(10, 50);

Clears the canvas with dark, semi-transparent color, leaving previous frames slightly visible to create motion trails

function-call Title and Subtitle Rendering text("Cody's Thought Network", width / 2, height * 0.12);

Draws the sketch title at the top with responsive sizing based on window width

calculation Pulsating Core Animation let currentCoreSize = coreSize + sin(frameCount * 0.05) * 15;

Uses sine wave to smoothly oscillate the core size between coreSize - 15 and coreSize + 15, creating a breathing effect

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

Iterates through all particles, updating their positions, drawing them, computing connections, and applying attraction forces

conditional Thought Generation Timing if (frameCount % contentFrequency === 0) {

Every contentFrequency frames, triggers creation of a new sassy thought to display

for-loop Content Fade and Removal for (let i = generativeContents.length - 1; i >= 0; i--) {

Updates all floating thoughts, removes them from the array once fully faded, looping backwards to safely splice while iterating

background(10, 50);
Clears the canvas with a very dark color (RGB 10, 10, 10) at 50% opacity; previous frames remain faintly visible, creating trails
textSize(min(width * 0.06, 60));
Sets the title font size to 6% of screen width, but caps it at 60 pixels so it doesn't get too huge on very large screens
let currentCoreSize = coreSize + sin(frameCount * 0.05) * 15;
Calculates a size that oscillates smoothly using sine; frameCount increases every frame, creating continuous animation
let connectDistance = width * connectDistanceRatio;
Multiplies the screen width by 0.07, so particles 7% of screen width apart will draw lines to each other—scales responsively with window size
for (let p of particles) {
Loops through each particle in the particles array using a for...of loop, the cleanest syntax for iterating collections
if (frameCount % contentFrequency === 0) {
The modulo operator (%) returns the remainder; when frameCount is a multiple of contentFrequency, this is true and a new thought is created
for (let i = generativeContents.length - 1; i >= 0; i--) {
Loops backward through the array (important!) so that when you splice/remove items, the indices of remaining items don't shift unexpectedly
generativeContents.splice(i, 1);
Removes one element at index i from the array; this is safe to do while iterating backward

createGenerativeContent()

This function solves a common creative coding problem: how to place random elements without them overlapping. The strategy is to try multiple random positions until one works. This is much simpler than calculating ideal positions mathematically, and it works great for most interactive sketches.

🔬 This loop tries up to maxAttempts times and skips if the thought is too wide. What happens if you remove the width check entirely (delete the if statement with the 0.9 check)? Will long thoughts cause visual chaos?

  for (let i = 0; i < maxAttempts; i++) {
    newContent = new GenerativeContent(random(thoughts));
    // If text is wider than screen, it will always overlap with itself when centered.
    // Skip creating it if it's too wide.
    if (newContent.getBoundingBox().w > width * 0.9) {
      console.log("Thought too wide, skipping.");
      continue;
    }
function createGenerativeContent() {
  // Now only creates text content
  let newContent;
  let maxAttempts = 10; // Try a few times to find a non-overlapping position

  for (let i = 0; i < maxAttempts; i++) {
    newContent = new GenerativeContent(random(thoughts));
    // If text is wider than screen, it will always overlap with itself when centered.
    // Skip creating it if it's too wide.
    if (newContent.getBoundingBox().w > width * 0.9) {
      console.log("Thought too wide, skipping.");
      continue;
    }

    let overlaps = false;
    // Check for overlap with existing content
    for (let existingContent of generativeContents) {
      if (rectsOverlap(newContent.getBoundingBox(), existingContent.getBoundingBox())) {
        overlaps = true;
        break;
      }
    }
    if (!overlaps) {
      generativeContents.push(newContent);
      return; // Added successfully
    }
  }
  // If we reach here, couldn't find a clear spot after maxAttempts
  console.log("Could not find a non-overlapping position for new thought.");
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

for-loop Position Finding Attempts for (let i = 0; i < maxAttempts; i++) {

Tries up to 10 times to create a new thought; if a position overlaps with existing thoughts, it tries again with a new random position

conditional Text Width Validation if (newContent.getBoundingBox().w > width * 0.9) {

Skips creating the thought if it's too wide to fit on screen without extending beyond 90% of the canvas width

for-loop Overlap Detection for (let existingContent of generativeContents) {

Checks the new thought against all already-visible thoughts to see if any bounding boxes overlap

let newContent;
Declares a variable to hold the new GenerativeContent object; it will be assigned in the loop
let maxAttempts = 10;
Sets a limit of 10 tries to find a non-overlapping position; prevents infinite loops if the screen is packed with text
for (let i = 0; i < maxAttempts; i++) {
Loops up to 10 times, trying to place a new thought each iteration
newContent = new GenerativeContent(random(thoughts));
Creates a new thought by picking a random phrase from the thoughts array and wrapping it in a GenerativeContent object
if (newContent.getBoundingBox().w > width * 0.9) {
Checks if the thought's width exceeds 90% of the screen; if so, it's too long and we skip this attempt
continue;
Skips the rest of this loop iteration and jumps to the next attempt, without adding the thought
for (let existingContent of generativeContents) {
Loops through all currently visible thoughts to check for collisions with the new one
if (rectsOverlap(newContent.getBoundingBox(), existingContent.getBoundingBox())) {
Calls a helper function to test if two bounding boxes overlap; if true, this position is bad
if (!overlaps) {
If no overlaps were found, the position is clear; add the thought and return early to exit the function
generativeContents.push(newContent);
Adds the new thought to the generativeContents array so it will be displayed and updated each frame
return;
Exits the function immediately after successfully adding a thought, avoiding more attempts

rectsOverlap()

This is the classic AABB (Axis-Aligned Bounding Box) collision test used in game engines and interactive design. If ANY of these four conditions is false, the boxes can't overlap. Only if all four are true do they collide. It's fast and works perfectly for rectangular regions.

// Helper function to check if two rectangles overlap
function rectsOverlap(rect1, rect2) {
  return rect1.x < rect2.x + rect2.w &&
         rect1.x + rect1.w > rect2.x &&
         rect1.y < rect2.y + rect2.h &&
         rect1.y + rect1.h > rect2.y;
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation AABB Collision Detection return rect1.x < rect2.x + rect2.w &&

Implements Axis-Aligned Bounding Box (AABB) collision detection by checking if rectangles overlap on both x and y axes

return rect1.x < rect2.x + rect2.w &&
Checks if rect1's left edge is to the left of rect2's right edge (rect2.x + rect2.w); if false, rect1 is completely to the right and they can't overlap
rect1.x + rect1.w > rect2.x &&
Checks if rect1's right edge is to the right of rect2's left edge; if false, rect1 is completely to the left
rect1.y < rect2.y + rect2.h &&
Checks if rect1's top edge is above rect2's bottom edge; if false, rect1 is below and they can't overlap
rect1.y + rect1.h > rect2.y;
Checks if rect1's bottom edge is below rect2's top edge; if false, rect1 is above. All four must be true for overlap

keyPressed()

keyPressed() runs whenever the user presses any key. Notice how it creates a burst effect: thoughts appear faster AND particles speed up, then both return to normal after 2 seconds. This teaches you how to create temporary interactive feedback using setTimeout()—a key technique for making sketches feel responsive.

function keyPressed() {
  // Generate a new thought and slightly intensify particles
  createGenerativeContent();
  for (let p of particles) {
    p.intensify(0.1); // Briefly speed up particles
  }
  // Make contents appear more frequently for a few frames after interaction
  contentFrequency = 30; // Faster for 2 seconds
  setTimeout(() => contentFrequency = 180, 2000); // Reset after 2 seconds
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

for-loop Particle Intensification for (let p of particles) {

Loops through all particles and speeds them up briefly, creating a visual 'pulse' when the user presses a key

function-call Delayed Frequency Reset setTimeout(() => contentFrequency = 180, 2000);

Schedules the thought frequency to return to normal (every 180 frames) after 2000 milliseconds, creating a temporary burst effect

createGenerativeContent();
Immediately creates a new thought and tries to place it on screen, triggered by the user pressing any key
for (let p of particles) {
Loops through all particles to apply the intensify effect to each one
p.intensify(0.1);
Calls the particle's intensify method with a multiplier of 0.1, causing a 10% speed boost
contentFrequency = 30;
Temporarily changes how often thoughts appear; every 30 frames instead of 180 means 6x faster for the next 2 seconds
setTimeout(() => contentFrequency = 180, 2000);
Schedules a function to run after 2000 milliseconds (2 seconds) that resets contentFrequency back to 180 frames

windowResized()

windowResized() is called automatically by p5.js whenever the browser window is resized. By calling setup() again, we redraw the gradient to fill the new size and ensure particles are distributed across the new dimensions. This keeps the sketch responsive on any device.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  background(10); // Redraw dark background on resize
  // Re-draw the static gradient in setup() to fill the new size
  setup();
}
Line-by-line explanation (3 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the p5.js canvas to match the new window dimensions when the user resizes their browser
background(10);
Fills the canvas with a dark color to erase any artifacts or incomplete drawing from the resize
setup();
Calls setup() again to redraw the gradient background and recreate particles if needed to fit the new canvas size

Particle class

The Particle class is the core of this sketch. It uses p5.Vector for clean 2D math, implements basic physics (acceleration → velocity → position), and handles three behaviors: wrapping, connecting, and attracting. This is a beautiful example of object-oriented creative coding: each particle knows how to update itself and draw itself, keeping the main draw() loop clean and readable.

🔬 This loop draws lines between nearby particles and fades them by distance. What happens if you remove the map() call and just use stroke(0, 150, 255, 100) for constant opacity? Will the network look more or less connected?

  connect(particles, connectDistance) { // Added connectDistance parameter
    for (let other of particles) {
      let d = p5.Vector.dist(this.pos, other.pos);
      if (d < connectDistance) { // Connect if within the responsive distance
        stroke(0, 150, 255, map(d, 0, connectDistance, 100, 0)); // Fade connections
        strokeWeight(0.5);
        line(this.pos.x, this.pos.y, other.pos.x, other.pos.y);
      }
    }

🔬 These four lines make particles wrap around edges like a classic Pac-Man screen. What if you changed them to BOUNCE instead? Try replacing the edge-wrap logic with velocity reversal (this.vel.x *= -1 when x goes out of bounds)—what happens visually?

    // Wrap around edges
    if (this.pos.x < 0) this.pos.x = width;
    if (this.pos.x > width) this.pos.x = 0;
    if (this.pos.y < 0) this.pos.y = height;
    if (this.pos.y > height) this.pos.y = 0;
// --- Particle Class ---
class Particle {
  constructor() {
    this.pos = createVector(random(width), random(height));
    this.vel = p5.Vector.random2D();
    this.vel.setMag(random(1, 2));
    this.acc = createVector();
    this.maxSpeed = 3;
    this.maxForce = 0.1;
    this.r = 4;
    this.alpha = random(50, 150);
  }

  update() {
    this.vel.add(this.acc);
    this.vel.limit(this.maxSpeed);
    this.pos.add(this.vel);
    this.acc.mult(0); // Reset acceleration

    // Wrap around edges
    if (this.pos.x < 0) this.pos.x = width;
    if (this.pos.x > width) this.pos.x = 0;
    if (this.pos.y < 0) this.pos.y = height;
    if (this.pos.y > height) this.pos.y = 0;
  }

  display() {
    fill(0, 150, 255, this.alpha);
    noStroke();
    ellipse(this.pos.x, this.pos.y, this.r * 0.5);
  }

  connect(particles, connectDistance) { // Added connectDistance parameter
    for (let other of particles) {
      let d = p5.Vector.dist(this.pos, other.pos);
      if (d < connectDistance) { // Connect if within the responsive distance
        stroke(0, 150, 255, map(d, 0, connectDistance, 100, 0)); // Fade connections
        strokeWeight(0.5);
        line(this.pos.x, this.pos.y, other.pos.x, other.pos.y);
      }
    }
  }

  attract(targetX, targetY) {
    let target = createVector(targetX, targetY);
    let force = p5.Vector.sub(target, this.pos);
    force.setMag(this.maxForce); // Gentle attraction
    this.acc.add(force);
  }

  intensify(factor) {
    this.vel.mult(1 + factor);
    this.vel.limit(this.maxSpeed * 2); // Temporarily increase max speed
    setTimeout(() => this.vel.limit(this.maxSpeed), 500); // Reset after 0.5 seconds
  }
}
Line-by-line explanation (19 lines)

🔧 Subcomponents:

function-call Particle Initialization this.pos = createVector(random(width), random(height));

Places the particle at a random position on the canvas using p5.Vector for 2D positioning

calculation Random Velocity Setup this.vel = p5.Vector.random2D();

Creates a random direction vector and then sets its magnitude to a random speed between 1 and 2 pixels per frame

conditional Edge Wrapping Logic if (this.pos.x < 0) this.pos.x = width;

When particle exits left edge, it reappears on the right edge, creating seamless wraparound instead of bouncing

for-loop Particle Connection Drawing for (let other of particles) {

Compares this particle to every other particle and draws a line if they're close enough

calculation Distance-Based Line Opacity stroke(0, 150, 255, map(d, 0, connectDistance, 100, 0));

Uses map() to fade the line's alpha from 100 (nearby particles) to 0 (at the edge of connection distance)

this.pos = createVector(random(width), random(height));
Creates a 2D vector at a random x,y position on the canvas; vectors are p5.js's clean way to store and manipulate 2D points
this.vel = p5.Vector.random2D();
Generates a random direction vector (pointing anywhere on the unit circle); the magnitude will be set next
this.vel.setMag(random(1, 2));
Sets the length of the velocity vector to a random value between 1 and 2, so particles move at varying speeds
this.acc = createVector();
Initializes acceleration to zero; forces (like attraction) will add to this every frame
this.maxSpeed = 3;
Sets a speed cap; the velocity vector will never exceed 3 pixels per frame, preventing particles from flying off uncontrollably
this.r = 4;
Sets the particle's radius; it will be drawn as a circle with diameter 4 * 0.5 = 2 pixels
this.vel.add(this.acc);
Applies acceleration to velocity, implementing basic physics: forces change how fast something moves
this.vel.limit(this.maxSpeed);
Caps the velocity's magnitude at maxSpeed; if it exceeds 3, it's scaled down to exactly 3
this.pos.add(this.vel);
Updates position by adding velocity; small increments each frame create smooth animation
this.acc.mult(0);
Resets acceleration to zero each frame; forces are recalculated fresh next frame by the attract() method
if (this.pos.x < 0) this.pos.x = width;
When particle leaves the left edge, teleport it to the right edge, creating seamless looping
let d = p5.Vector.dist(this.pos, other.pos);
Calculates the distance between this particle and another using Euclidean distance; returns a single number
if (d < connectDistance) {
If distance is less than the threshold, the particles are close enough to draw a line between them
stroke(0, 150, 255, map(d, 0, connectDistance, 100, 0));
Uses map() to smoothly fade the line's opacity: at distance 0 it's opaque (alpha 100), at connectDistance it's transparent (alpha 0)
let target = createVector(targetX, targetY);
Creates a vector representing the core's position (the attraction target)
let force = p5.Vector.sub(target, this.pos);
Calculates a vector pointing FROM the particle TO the target by subtracting positions; longer vectors = further away
force.setMag(this.maxForce);
Limits the force to a gentle magnitude (0.1); this prevents particles from jerking abruptly toward the core
this.vel.mult(1 + factor);
Multiplies velocity by 1.1 (when factor is 0.1), speeding up the particle by 10%
this.vel.limit(this.maxSpeed * 2);
Temporarily caps speed at 6 instead of 3, allowing the particle to move faster during the burst

GenerativeContent class

GenerativeContent manages the floating sassy thoughts. Each instance knows its own text, position, size, and opacity, and recalculates its bounding box every frame—this is crucial for the overlap detection to work correctly. The class demonstrates a clean separation of concerns: text rendering, physics, and collision awareness are all bundled together in one well-organized object.

🔬 Thoughts spawn at the center and drift upward. What happens if you change the velocity to random(-1, 1) for both x AND y? Or make the y-velocity positive instead of negative? How does the movement pattern change?

    this.pos = createVector(width / 2, height / 2 + random(-100, 100));
    this.vel = createVector(random(-1, 1), random(-0.5, -1.5));

🔬 This draws a semi-transparent black box around each thought. What if you change 0.7 to 1.0? Or remove the rect() line entirely? How does readability change?

      // Draw semi-transparent black backdrop rectangle
      fill(0, 0, 0, this.alpha * 0.7); // 70% of current text alpha
      rectMode(CENTER);
      rect(this.pos.x, this.pos.y, this.textW + this.padding * 2, this.textH + this.padding, 5); // Rounded corners
// --- GenerativeContent Class (now only handles text) ---
class GenerativeContent {
  constructor(text) {
    this.type = 'text'; // Always text now
    this.text = text;
    this.alpha = 255;
    this.pos = createVector(width / 2, height / 2 + random(-100, 100));
    this.vel = createVector(random(-1, 1), random(-0.5, -1.5));
    this.size = random(20, 30);
    this.fadeRate = random(1, 3);

    // Calculate initial bounding box
    this.calculateBoundingBox();
  }

  update() {
    this.pos.add(this.vel);
    this.alpha -= this.fadeRate;
    // Recalculate bounding box as position changes
    this.calculateBoundingBox();
  }

  display() {
    if (this.alpha > 0) {
      // Draw semi-transparent black backdrop rectangle
      fill(0, 0, 0, this.alpha * 0.7); // 70% of current text alpha
      rectMode(CENTER);
      rect(this.pos.x, this.pos.y, this.textW + this.padding * 2, this.textH + this.padding, 5); // Rounded corners

      // Draw white text on top
      fill(255, this.alpha); // White text fading out
      textSize(this.size);
      text(this.text, this.pos.x, this.pos.y);
    }
  }

  isFaded() {
    return this.alpha <= 0;
  }

  calculateBoundingBox() {
    textSize(this.size); // Set size to get accurate dimensions
    this.textW = textWidth(this.text);
    this.textH = textAscent() + textDescent();
    this.padding = 10; // Padding around the text
    this.bbox = {
      x: this.pos.x - (this.textW + this.padding * 2) / 2,
      y: this.pos.y - (this.textH + this.padding) / 2,
      w: this.textW + this.padding * 2,
      h: this.textH + this.padding
    };
  }

  getBoundingBox() {
    return this.bbox;
  }
}
Line-by-line explanation (18 lines)

🔧 Subcomponents:

calculation Thought Position Initialization this.pos = createVector(width / 2, height / 2 + random(-100, 100));

Places the thought at the screen center horizontally, but adds randomness vertically so they don't all spawn in the exact same spot

calculation Upward Drift Velocity this.vel = createVector(random(-1, 1), random(-0.5, -1.5));

Makes thoughts drift upward (negative y is up in p5.js) and gently left or right; the range ensures they always move up, not down

function-call Semi-Transparent Background Rectangle rect(this.pos.x, this.pos.y, this.textW + this.padding * 2, this.textH + this.padding, 5);

Draws a rounded rectangle behind the text so it's readable even if particles are moving behind it

calculation Bounding Box Computation this.bbox = {

Calculates the exact rectangular region the thought occupies, used for overlap detection

this.type = 'text';
Stores the content type; this is a legacy field from when the class also handled images, kept for consistency
this.text = text;
Stores the sassy phrase that was passed to the constructor
this.alpha = 255;
Starts fully opaque (alpha 255 means 100% solid); this will decrease each frame until the thought fades away
this.pos = createVector(width / 2, height / 2 + random(-100, 100));
Centers the thought horizontally (width / 2) and vertically near the middle (height / 2) with ±100 pixel randomness
this.vel = createVector(random(-1, 1), random(-0.5, -1.5));
Creates a velocity vector: x varies randomly between -1 and 1 (slow left/right drift), y is between -0.5 and -1.5 (always drifting upward)
this.fadeRate = random(1, 3);
Each thought fades at a different rate (1-3 alpha units per frame); faster fade means shorter lifespan, slower fade means longer visible time
this.pos.add(this.vel);
Updates the thought's position by adding its velocity; this happens every frame in update()
this.alpha -= this.fadeRate;
Decreases opacity each frame; when alpha reaches 0 or below, the thought becomes invisible and ready to be removed
this.calculateBoundingBox();
Recalculates the bounding box after moving; this keeps the overlap detection accurate as the thought drifts
if (this.alpha > 0) {
Only draws if still visible; saves computation by not rendering fully transparent thoughts
fill(0, 0, 0, this.alpha * 0.7);
Sets the backdrop color to black with alpha of 70% of the text's current alpha, so the backdrop fades along with the text
rectMode(CENTER);
Switches rectangle drawing mode so coordinates specify the CENTER instead of the top-left, making positioning easier
rect(this.pos.x, this.pos.y, this.textW + this.padding * 2, this.textH + this.padding, 5);
Draws a rounded rectangle (5 is the corner radius) centered at the thought's position, sized to fit the text with padding
textSize(this.size);
Sets the font size to this thought's size (randomly 20-30 pixels) before drawing
textWidth(this.text);
Returns how many pixels the text will occupy horizontally at the current font size; stored for bounding box calculation
textAscent() + textDescent();
Calculates total text height by adding the height above the baseline (ascent) and below (descent)
this.bbox = {
Creates a JavaScript object with x, y, w (width), and h (height) properties representing the thought's bounding box
x: this.pos.x - (this.textW + this.padding * 2) / 2,
Calculates the left edge of the bounding box by subtracting half the total width from the center x position

📦 Key Variables

font p5.Font

Stores the loaded Roboto font loaded in preload(); used for all text rendering throughout the sketch

let font;
particles array

Holds all Particle objects currently in the simulation; updated every frame and rendered

let particles = [];
generativeContents array

Holds all GenerativeContent objects (floating thoughts) currently visible; faded content is removed each frame

let generativeContents = [];
coreSize number

The base diameter of the central pulsating core that attracts particles; increased by sin() in draw() for pulsation effect

let coreSize = 100;
particleCount number

How many particles to create at startup; controls the density of the particle network

let particleCount = 150;
contentFrequency number

How often (in frames) a new thought appears; every 180 frames by default, reduced to 30 on key press for burst effect

let contentFrequency = 180;
connectDistanceRatio number

Particles draw connecting lines if within this ratio of the screen width; 0.07 = 7% of screen width

let connectDistanceRatio = 0.07;
thoughts array

Array of sassy coding-related phrases that are randomly picked to create GenerativeContent objects

const thoughts = [ "Another loop? How original, Sketchie.", ... ];
loadingError boolean

Flag that tracks whether the font failed to load; if true, setup() displays an error and stops the sketch

let loadingError = false;
loadingErrorMessage string

Holds the error message text to display if font loading fails

let loadingErrorMessage = "Assets failed to load...";

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG GenerativeContent.calculateBoundingBox()

textSize() and textWidth()/textAscent() calls are inefficient since they're called every frame; textAscent/Descent() values don't change per text instance

💡 Calculate constant text metrics once and cache them, rather than recalculating every update. Store textH as a fixed value since font size is set once per thought.

PERFORMANCE Particle.connect()

Drawing lines from every particle to every other particle creates O(n²) complexity; with 150 particles, that's 22,500 distance checks per frame

💡 Use spatial partitioning (grid-based lookup) to only check particles that are nearby, reducing comparisons dramatically for large particle counts.

STYLE draw() function

Title and subtitle text settings are hardcoded and recalculated every frame (fill, textSize, text calls) without organization

💡 Create a helper function like drawHeader() to keep draw() cleaner and easier to read. Separate presentation logic from animation logic.

FEATURE keyPressed()

All key presses trigger the same burst effect; there's no way for users to know they can interact by pressing a key until they try

💡 Add a visual hint on first load (e.g., 'Press any key to generate a thought') that fades after 3 seconds, making the interaction more discoverable.

BUG windowResized()

Calling setup() on every resize recreates all particles, causing them to reset their positions suddenly; particles should only be recreated if the count changes

💡 Instead of calling full setup(), just resize the canvas and clear particles gently. Only rebuild the gradient if it's significantly different in size.

PERFORMANCE setup() gradient loop

Drawing 200+ concentric ellipses every time windowResized() runs is expensive; the gradient is static and doesn't need to be recomputed constantly

💡 Create the gradient once as a separate p5.Graphics object or cached image, then just display that cached image on resize instead of redrawing circles.

🔄 Code Flow

Code flow showing preload, setup, draw, creategenerativecontent, rectsoverlap, keypressed, windowresized, particle, generativecontent

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

graph TD start[Start] --> setup[setup] setup --> preload[preload] preload --> font-load[Font Loading with Error Handling] font-load --> error-check[Loading Error Check] error-check -->|If font loads| setup error-check -->|If font fails| stop[Stop Sketch] setup --> gradient-loop[Radial Gradient Background] gradient-loop --> particle-creation[Particle Initialization] particle-creation --> draw[draw loop] click preload href "#fn-preload" click setup href "#fn-setup" click gradient-loop href "#sub-gradient-loop" click particle-creation href "#sub-particle-creation" draw --> background-trails[Semi-Transparent Background] background-trails --> title-display[Title and Subtitle Rendering] title-display --> core-pulsation[Pulsating Core Animation] core-pulsation --> particle-loop[Particle Update and Display] particle-loop --> content-creation[Thought Generation Timing] content-creation --> content-cleanup[Content Fade and Removal] particle-loop --> particle-intensify-loop[Particle Intensification] content-creation --> attempt-loop[Position Finding Attempts] attempt-loop --> width-check[Text Width Validation] width-check -->|If valid| overlap-loop[Overlap Detection] overlap-loop --> aabb-collision[AABB Collision Detection] aabb-collision -->|If no overlap| text-init[Thought Position Initialization] text-init --> velocity-random[Upward Drift Velocity] velocity-random --> particle-constructor[Particle Initialization] particle-constructor --> attempt-loop click draw href "#fn-draw" click background-trails href "#sub-background-trails" click title-display href "#sub-title-display" click core-pulsation href "#sub-core-pulsation" click particle-loop href "#sub-particle-loop" click content-creation href "#sub-content-creation" click content-cleanup href "#sub-content-cleanup" click attempt-loop href "#sub-attempt-loop" click width-check href "#sub-width-check" click overlap-loop href "#sub-overlap-loop" click aabb-collision href "#sub-aabb-collision" click text-init href "#sub-text-init" click velocity-random href "#sub-velocity-random" click particle-constructor href "#sub-particle-constructor" draw --> windowresized[windowResized] windowresized --> setup click windowresized href "#fn-windowresized"

❓ Frequently Asked Questions

What visual elements does Cody's mind sketch create?

Cody's mind features glowing particles that orbit a cosmic core, creating a dynamic and ethereal visual experience with flickering connections across the screen.

How can users interact with the Cody's mind sketch?

Users can enjoy a playful interaction as sassy thoughts about coding appear on the screen, adding a humorous and chatty personality to the animation.

What creative coding concepts does Cody's mind demonstrate?

This sketch showcases generative art techniques by utilizing particle systems and randomness to create engaging visuals and text, reflecting the playful nature of coding.

Preview

Cody's mind - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Cody's mind - Code flow showing preload, setup, draw, creategenerativecontent, rectsoverlap, keypressed, windowresized, particle, generativecontent
Code Flow Diagram