Mycelium Network — Bioluminescent Growth Simulation

This sketch simulates a bioluminescent mycelium network that organically grows across the canvas, with glowing branches that split and fuse. Clicking plants food sources that attract the network, while nutrient packets flow through branches, spores drift overhead, and ripples respond to your interactions.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make branches steer aggressively toward food — Change food attraction strength to 1.0 so branches nearly point at food sources instead of gently curving toward them.
  2. Disable fusing between separate networks — Comment out the anastomosis() call so branches never recognize and connect with each other—they'll grow independently forever.
  3. Triple the nutrient packet count — More green glowing packets flow through branches, creating denser light trails.
  4. Make spores drift downward instead of up — Change the spore speed to positive so they sink slowly instead of rising—opposite of the natural drift.
  5. Allow branches to fuse from farther away — Increase anastomosis threshold so separate branch tips connect from greater distances, creating a looser web.
  6. Grow much deeper, fractaling further — Extend the maximum branching depth so the network grows more complex and intricate.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a mesmerizing simulation of a fungal mycelium network spreading across your screen in real time. The network grows organically with branching fractal-like patterns, glowing in teal and green as it explores the canvas. When you click, a food source appears and the mycelium branches are attracted toward it—a behavior driven by vector math, distance calculations, and angle interpolation. Nutrient packets glow as they flow through the network, spores drift like falling snow, and ripples pulse outward from your clicks, all creating a living, reactive ecosystem.

The code is organized into a central Mycelium class that manages the branching structure, plus helper classes for Branch, FoodSource, NutrientPacket, Spore, Flash, and Ripple. The setup() function initializes the network from the center of the screen, and draw() runs 60 times per second to grow new branches, detect fusions between separate tips, display all visual elements, and update particles. By reading this sketch, you'll learn how to build recursive tree structures, implement attraction-based steering toward targets, detect collisions between network endpoints, and orchestrate multiple animated systems in a single visualization.

⚙️ How It Works

  1. When the sketch loads, setup() creates a full-screen canvas and initializes a Mycelium object at the center with five root branches radiating outward like spokes.
  2. Every frame, draw() calls mycelium.grow() to expand a few random branch tips—each tip reads the foodSources array and steers toward nearby food if within FOOD_ATTRACTION_RADIUS pixels.
  3. After growing, mycelium.anastomosis() checks whether any two separate branch tips have grown close enough to touch (within ANASTOMOSIS_THRESHOLD pixels), and if so, fuses them by creating new fusion branches that connect the two tips.
  4. Nutrient packets spawn from random branches and travel upward toward branch tips or downward toward the root, bouncing between branches at junctions and creating a glowing trail as they move.
  5. When you click, a new FoodSource appears at the mouse position and a Ripple expands outward, while nutrient packets are attracted to the food and consumed when a branch tip reaches it.
  6. Spores drift upward across the screen, flashes pulse white where branches fuse, and all elements are drawn with glow effects using dual-layer line strokes and semi-transparent fills.

🎓 Concepts You'll Learn

Recursive tree structures and branching algorithmsVector math and angle interpolation for attractionCollision detection and endpoint fusionParticle systems and animated trailsColor gradients and lerp for visual effectsState machines for nutrient packet pathfindingGlow effects through layered strokes

📝 Code Breakdown

setup()

setup() runs once when the sketch launches. It initializes the canvas size, creates the central mycelium network, and populates the spores array. This is where you set up all the global objects that draw() will animate each frame.

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
  
  console.log("Setup called - Canvas size:", width, "x", height);

  // Initialize the mycelium network from the center
  mycelium = new Mycelium(width / 2, height / 2);
  
  console.log("Mycelium created with", mycelium.branches.length, "initial branches");
  console.log("Active tips:", mycelium.activeTips.length);

  // Initialize spores
  for (let i = 0; i < SPORE_COUNT; i++) {
    spores.push(new Spore(random(width), random(height)));
  }
  
  console.log("Setup complete");
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

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

Creates a full-screen canvas that scales to the window size

object-instantiation Mycelium Initialization mycelium = new Mycelium(width / 2, height / 2);

Creates the root mycelium network at the center of the screen

for-loop Spore Initialization for (let i = 0; i < SPORE_COUNT; i++) { spores.push(new Spore(random(width), random(height))); }

Populates the spores array with 150 Spore objects at random positions

createCanvas(windowWidth, windowHeight);
Creates a p5.js canvas that fills the entire browser window using the windowWidth and windowHeight variables.
background(0);
Fills the canvas with black (RGB value 0), setting the initial dark background.
mycelium = new Mycelium(width / 2, height / 2);
Instantiates a new Mycelium object at the center of the canvas (width/2, height/2). The Mycelium constructor creates five root branches radiating outward.
for (let i = 0; i < SPORE_COUNT; i++) {
Loops 150 times (SPORE_COUNT is 150), creating a new Spore for each iteration.
spores.push(new Spore(random(width), random(height)));
Creates a Spore at a random x and y position on the canvas and adds it to the spores array.

draw()

draw() runs 60 times per second (the animation loop). Each call clears the background, grows the mycelium, updates all animated particles, and renders everything to the screen. This is where the 'liveness' of the sketch comes from—every system animates here.

🔬 This loop moves and draws every spore every frame. What happens if you add spores.push(new Spore(random(width), random(height))); inside this loop, right before spore.update()? (Hint: spores array will grow infinitely—watch for a slowdown.)

  // Update and display spores
  for (let spore of spores) {
    spore.update();
    spore.display();
  }

🔬 This code keeps exactly PACKET_COUNT nutrient packets flowing. What happens if you change PACKET_COUNT to 0 in the check? (Will packets still spawn or will they disappear?)

  // Refill nutrient packets
  if (nutrientPackets.length < PACKET_COUNT && mycelium.branches.length > 0) {
    let randomBranch = random(mycelium.branches);
    nutrientPackets.push(new NutrientPacket(randomBranch));
  }
function draw() {
  // Clear background
  background(0);

  // Grow and display mycelium
  mycelium.grow();
  mycelium.anastomosis();
  mycelium.display();
  
  // Debug: Draw a test circle at center to verify draw is running
  if (frameCount < 60) {
    fill(255, 0, 0, 100);
    noStroke();
    ellipse(width/2, height/2, 20);
  }
  
  // Log branch count every 60 frames
  if (frameCount % 60 === 0) {
    console.log("Frame", frameCount, "- Branches:", mycelium.branches.length, "Active tips:", mycelium.activeTips.length);
  }

  // Display food sources
  for (let food of foodSources) {
    food.display();
  }

  // Update and display fusion flashes
  for (let i = flashes.length - 1; i >= 0; i--) {
    flashes[i].update();
    flashes[i].display();
    if (!flashes[i].isAlive()) {
      flashes.splice(i, 1);
    }
  }

  // Update and display spores
  for (let spore of spores) {
    spore.update();
    spore.display();
  }

  // Update and display ripples
  for (let i = ripples.length - 1; i >= 0; i--) {
    ripples[i].update();
    ripples[i].display();
    if (!ripples[i].isAlive()) {
      ripples.splice(i, 1);
    }
  }

  // Update and display nutrient packets
  for (let i = nutrientPackets.length - 1; i >= 0; i--) {
    let packet = nutrientPackets[i];
    packet.update();
    packet.display();
    if (!packet.alive) {
      nutrientPackets.splice(i, 1);
    }
  }

  // Refill nutrient packets
  if (nutrientPackets.length < PACKET_COUNT && mycelium.branches.length > 0) {
    let randomBranch = random(mycelium.branches);
    nutrientPackets.push(new NutrientPacket(randomBranch));
  }
}
Line-by-line explanation (26 lines)

🔧 Subcomponents:

function-call Mycelium Growth Step mycelium.grow(); mycelium.anastomosis(); mycelium.display();

Grows a few new branches from random tips, detects and fuses nearby endpoints, and renders the entire network

for-loop Food Source Display for (let food of foodSources) { food.display(); }

Iterates through all food sources and draws each one with its pulsing radius

for-loop Flash Update and Display for (let i = flashes.length - 1; i >= 0; i--) { flashes[i].update(); flashes[i].display(); if (!flashes[i].isAlive()) { flashes.splice(i, 1); } }

Updates each flash animation, draws it, and removes dead flashes from the array

for-loop Spore Animation for (let spore of spores) { spore.update(); spore.display(); }

Moves each spore upward and horizontally, then draws it

for-loop Ripple Animation for (let i = ripples.length - 1; i >= 0; i--) { ripples[i].update(); ripples[i].display(); if (!ripples[i].isAlive()) { ripples.splice(i, 1); } }

Expands each ripple outward, fades it, and removes dead ripples

for-loop Nutrient Packet Animation for (let i = nutrientPackets.length - 1; i >= 0; i--) { let packet = nutrientPackets[i]; packet.update(); packet.display(); if (!packet.alive) { nutrientPackets.splice(i, 1); } }

Moves each packet along its branch path, draws it with a trail, and removes dead packets

conditional Nutrient Packet Spawning if (nutrientPackets.length < PACKET_COUNT && mycelium.branches.length > 0) { let randomBranch = random(mycelium.branches); nutrientPackets.push(new NutrientPacket(randomBranch)); }

Keeps the nutrient packet count at PACKET_COUNT by spawning new ones from random branches

background(0);
Fills the entire canvas with black, erasing the previous frame and preventing motion trails.
mycelium.grow();
Selects a few random branch tips and grows new children from them, considering food attraction.
mycelium.anastomosis();
Checks whether any two separate branch tips have grown close enough to fuse, and creates connections where they touch.
mycelium.display();
Draws every branch in the mycelium.branches array with its glow effect and core stroke.
for (let food of foodSources) {
Loops through all food sources in the foodSources array.
food.display();
Draws the pulsing food source at its position with attraction radius visualization.
for (let i = flashes.length - 1; i >= 0; i--) {
Loops backward through the flashes array so removing dead ones doesn't skip elements.
flashes[i].update();
Updates the flash's lifespan, fading its alpha value over time.
flashes[i].display();
Draws the white pulsing circle at the flash's position.
if (!flashes[i].isAlive()) {
Checks if the flash has completely faded (alpha <= 0).
flashes.splice(i, 1);
Removes the dead flash from the array by removing 1 element at index i.
for (let spore of spores) {
Loops through each spore in the spores array.
spore.update();
Moves the spore upward and drifts it horizontally; wraps it to the bottom when it exits the top.
spore.display();
Draws the small white semi-transparent circle representing the spore.
for (let i = ripples.length - 1; i >= 0; i--) {
Loops backward through ripples so removal doesn't skip any.
ripples[i].update();
Expands the ripple's radius and decreases its alpha for fading effect.
ripples[i].display();
Draws a white circle outline that grows outward from the click position.
if (!ripples[i].isAlive()) {
Checks if the ripple has faded and reached its max radius.
ripples.splice(i, 1);
Removes the dead ripple from the array.
let packet = nutrientPackets[i];
Stores a reference to the current packet for cleaner code.
packet.update();
Moves the packet along its current branch toward a child branch or parent, updating its position.
packet.display();
Draws the packet and its glowing trail of previous positions.
if (!packet.alive) {
Checks if the packet is flagged as dead (usually because it reached a root).
if (nutrientPackets.length < PACKET_COUNT && mycelium.branches.length > 0) {
Spawns a new packet only if we have fewer than PACKET_COUNT packets and the mycelium has branches.
let randomBranch = random(mycelium.branches);
Selects a random branch from the mycelium's branches array as the starting point for a new packet.
nutrientPackets.push(new NutrientPacket(randomBranch));
Creates a new NutrientPacket starting from the random branch and adds it to the nutrientPackets array.

mousePressed()

mousePressed() is a p5.js built-in function that runs once whenever the mouse button is clicked. It's the primary interaction mechanism in this sketch—every click plants a new food source that steers the growing network.

function mousePressed() {
  foodSources.push(new FoodSource(mouseX, mouseY));
  ripples.push(new Ripple(mouseX, mouseY));
  console.log("Food source added at", mouseX, mouseY);
}
Line-by-line explanation (3 lines)
foodSources.push(new FoodSource(mouseX, mouseY));
Creates a new FoodSource object at the current mouse position and adds it to the foodSources array. The mycelium will now be attracted to this location.
ripples.push(new Ripple(mouseX, mouseY));
Creates a new Ripple object at the mouse position and adds it to the ripples array, creating a visual feedback animation that expands outward.
console.log("Food source added at", mouseX, mouseY);
Prints a message to the browser console showing the coordinates where the food source was placed (useful for debugging).

windowResized()

windowResized() is a p5.js built-in function called automatically whenever the browser window changes size. It keeps the canvas fullscreen and responsive.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  background(0);
}
Line-by-line explanation (2 lines)
resizeCanvas(windowWidth, windowHeight);
Updates the p5.js canvas to match the new window dimensions when the browser window is resized.
background(0);
Clears the canvas with black to prevent visual artifacts from the old canvas size being visible.

Branch class

The Branch class represents a single segment in the mycelium network. Each branch knows its angle, length, thickness, depth level, and parent/children relationships. When grow() is called, it creates child branches with decay in size and optional food attraction. The display() method uses a two-layer drawing technique (glow + core) to create the bioluminescent appearance.

🔬 This loop steers branches toward food. The lerp at 0.2 (FOOD_ATTRACTION_STRENGTH) blends the angle gently. What happens if you change 0.2 to 1? (Will branches turn sharply or stay the same?)

    for (let food of foodSources) {
      if (!food.alive) continue;
      let d = p5.Vector.dist(this.end, food.position);
      if (d < FOOD_ATTRACTION_RADIUS) {
        let foodAngle = atan2(food.position.y - this.end.y, food.position.x - this.end.x);
        baseAngle = lerp(baseAngle, foodAngle, FOOD_ATTRACTION_STRENGTH);
        attracted = true;
      }
    }
class Branch {
  constructor(start, angle, length, thickness, level, parent = null, isFusion = false, oppositeFusion = null, wasAttractedByFood = false) {
    this.start = start.copy();
    this.angle = angle;
    this.length = length;
    this.thickness = thickness;
    this.level = level;
    this.parent = parent;
    this.end = p5.Vector.fromAngle(this.angle).mult(this.length).add(this.start);
    this.children = [];
    this.grown = false;
    this.isFusionBranch = isFusion;
    this.oppositeFusionBranch = oppositeFusion;
    this.wasAttractedByFood = wasAttractedByFood;
  }

  display() {
    let gradientColor;

    if (this.isFusionBranch) {
      gradientColor = color(0, 255, 255);
    } else {
      let colorAmount = map(this.level, 0, BRANCH_MAX_LEVEL, 0, 1, true);
      gradientColor = lerpColor(color(...TEAL_BLUE), color(...BRIGHT_GREEN), colorAmount);

      if (this.wasAttractedByFood) {
        gradientColor = lerpColor(gradientColor, color(255, 255, 255), 0.3);
      }
    }

    // Draw glow
    noFill();
    strokeWeight(this.thickness * 2.5);
    stroke(red(gradientColor), green(gradientColor), blue(gradientColor), 100);
    line(this.start.x, this.start.y, this.end.x, this.end.y);

    // Draw core
    strokeWeight(this.thickness);
    stroke(red(gradientColor), green(gradientColor), blue(gradientColor), 255);
    line(this.start.x, this.start.y, this.end.x, this.end.y);
  }

  grow(foodSources) {
    if (this.level >= BRANCH_MAX_LEVEL || this.grown) {
      return;
    }

    let numChildren = 1;
    if (random() < BRANCH_SPLIT_PROBABILITY && this.level < BRANCH_MAX_LEVEL - 1) {
      numChildren = floor(random(2, 4));
    }

    let baseAngle = this.angle;
    let attracted = false;

    for (let food of foodSources) {
      if (!food.alive) continue;
      let d = p5.Vector.dist(this.end, food.position);
      if (d < FOOD_ATTRACTION_RADIUS) {
        let foodAngle = atan2(food.position.y - this.end.y, food.position.x - this.end.x);
        baseAngle = lerp(baseAngle, foodAngle, FOOD_ATTRACTION_STRENGTH);
        attracted = true;
      }
    }

    for (let i = 0; i < numChildren; i++) {
      let newAngle = baseAngle + random(-BRANCH_ANGLE_VARIATION, BRANCH_ANGLE_VARIATION);
      let newLength = this.length * BRANCH_DECAY * random(0.8, 1.2);
      let newThickness = this.thickness * THICKNESS_DECAY;

      if (attracted) {
        newThickness *= 1.2;
      }

      let newBranch = new Branch(this.end, newAngle, newLength, newThickness, this.level + 1, this, false, null, attracted);
      this.children.push(newBranch);
    }
    this.grown = true;
  }
}
Line-by-line explanation (33 lines)

🔧 Subcomponents:

constructor Branch Constructor constructor(start, angle, length, thickness, level, parent = null, isFusion = false, oppositeFusion = null, wasAttractedByFood = false)

Initializes a new branch with position, angle, size, hierarchical level, and properties for fusion and food attraction

calculation Endpoint Calculation this.end = p5.Vector.fromAngle(this.angle).mult(this.length).add(this.start);

Computes the endpoint of the branch using vector math: creates a unit vector in the direction of angle, scales it by length, and adds it to the start position

conditional Gradient Color Selection if (this.isFusionBranch) { gradientColor = color(0, 255, 255); } else { let colorAmount = map(this.level, 0, BRANCH_MAX_LEVEL, 0, 1, true); gradientColor = lerpColor(color(...TEAL_BLUE), color(...BRIGHT_GREEN), colorAmount); if (this.wasAttractedByFood) { gradientColor = lerpColor(gradientColor, color(255, 255, 255), 0.3); } }

Fusion branches are cyan; normal branches transition from teal to green based on depth level; branches attracted to food are lightened with white

calculation Glow Layer Drawing strokeWeight(this.thickness * 2.5); stroke(red(gradientColor), green(gradientColor), blue(gradientColor), 100); line(this.start.x, this.start.y, this.end.x, this.end.y);

Draws a thick, semi-transparent line behind the core branch to create a bioluminescent glow effect

calculation Core Branch Drawing strokeWeight(this.thickness); stroke(red(gradientColor), green(gradientColor), blue(gradientColor), 255); line(this.start.x, this.start.y, this.end.x, this.end.y);

Draws the solid core of the branch with full opacity over the glow layer

conditional Branching Split Decision if (random() < BRANCH_SPLIT_PROBABILITY && this.level < BRANCH_MAX_LEVEL - 1) { numChildren = floor(random(2, 4)); }

Randomly decides whether this branch splits into 2-3 children based on probability, respecting max depth

for-loop Food Attraction Loop for (let food of foodSources) { if (!food.alive) continue; let d = p5.Vector.dist(this.end, food.position); if (d < FOOD_ATTRACTION_RADIUS) { let foodAngle = atan2(food.position.y - this.end.y, food.position.x - this.end.x); baseAngle = lerp(baseAngle, foodAngle, FOOD_ATTRACTION_STRENGTH); attracted = true; } }

Loops through all food sources and steers the branch's angle toward any food within attraction radius using angle interpolation

for-loop Child Branch Creation for (let i = 0; i < numChildren; i++) { let newAngle = baseAngle + random(-BRANCH_ANGLE_VARIATION, BRANCH_ANGLE_VARIATION); let newLength = this.length * BRANCH_DECAY * random(0.8, 1.2); let newThickness = this.thickness * THICKNESS_DECAY; if (attracted) { newThickness *= 1.2; } let newBranch = new Branch(this.end, newAngle, newLength, newThickness, this.level + 1, this, false, null, attracted); this.children.push(newBranch); }

Creates child branches with decay in length and thickness, adds angle variation, boosts thickness if attracted to food, and links them as children

this.start = start.copy();
Stores the branch's starting point as a copy of the input vector (copying prevents accidental changes to the original).
this.end = p5.Vector.fromAngle(this.angle).mult(this.length).add(this.start);
Calculates the branch's endpoint by creating a unit vector pointing in the angle direction, scaling it by length pixels, and adding it to the start position.
this.children = [];
Initializes an empty array to hold any child branches created when this branch grows.
this.grown = false;
Sets a flag to false, tracking whether this branch has already created children (prevents re-growth).
let colorAmount = map(this.level, 0, BRANCH_MAX_LEVEL, 0, 1, true);
Maps the branch's level (depth in the tree) from the range 0 to BRANCH_MAX_LEVEL to a 0-1 value for interpolating colors.
gradientColor = lerpColor(color(...TEAL_BLUE), color(...BRIGHT_GREEN), colorAmount);
Interpolates between teal and green based on the branch's depth, creating a gradient effect where deeper branches are greener.
if (this.wasAttractedByFood) {
Checks if this branch grew while being attracted to food.
gradientColor = lerpColor(gradientColor, color(255, 255, 255), 0.3);
Lightens the branch's color by blending it 30% toward white if it was food-attracted, making it glow brighter.
strokeWeight(this.thickness * 2.5);
Sets the line thickness to 2.5 times the branch thickness for drawing the glow layer (thicker than the core).
stroke(red(gradientColor), green(gradientColor), blue(gradientColor), 100);
Extracts the RGB components of the gradient color and sets the stroke with 100 alpha (semi-transparent) for the glow.
line(this.start.x, this.start.y, this.end.x, this.end.y);
Draws a line from the branch's start to end point.
strokeWeight(this.thickness);
Sets the line thickness back to normal for drawing the solid core.
stroke(red(gradientColor), green(gradientColor), blue(gradientColor), 255);
Sets the stroke to the same color but with full alpha (255) for the opaque core layer.
if (this.level >= BRANCH_MAX_LEVEL || this.grown) {
Exits the grow() method if the branch has reached max depth or already grown children (prevents infinite re-growth).
let numChildren = 1;
Defaults to creating one child branch.
if (random() < BRANCH_SPLIT_PROBABILITY && this.level < BRANCH_MAX_LEVEL - 1) {
Randomly decides to split into multiple children (2-3) if under the split probability and not too close to max depth.
numChildren = floor(random(2, 4));
Randomly selects 2 or 3 as the number of children.
let baseAngle = this.angle;
Initializes the base angle for new children to inherit the current branch's direction.
for (let food of foodSources) {
Loops through every food source to check for attraction.
if (!food.alive) continue;
Skips dead food sources (those that have been consumed).
let d = p5.Vector.dist(this.end, food.position);
Calculates the distance from this branch's endpoint to the food source.
if (d < FOOD_ATTRACTION_RADIUS) {
Checks if the food is within the attraction radius (100 pixels by default).
let foodAngle = atan2(food.position.y - this.end.y, food.position.x - this.end.x);
Calculates the angle from this branch's endpoint toward the food using atan2, returning a value between -PI and PI.
baseAngle = lerp(baseAngle, foodAngle, FOOD_ATTRACTION_STRENGTH);
Smoothly blends the base angle 20% (FOOD_ATTRACTION_STRENGTH) toward the food angle, steering the branch toward food without turning too sharply.
attracted = true;
Sets a flag indicating that this branch is being attracted to food, used later to boost its thickness.
let newAngle = baseAngle + random(-BRANCH_ANGLE_VARIATION, BRANCH_ANGLE_VARIATION);
Adds random noise to the base angle to create natural variation in branch directions (±0.5 radians).
let newLength = this.length * BRANCH_DECAY * random(0.8, 1.2);
Multiplies the current branch length by decay factor (0.85) and a random factor (0.8-1.2) so child branches shrink and vary naturally.
let newThickness = this.thickness * THICKNESS_DECAY;
Multiplies the thickness by 0.7 so child branches are thinner than their parent.
if (attracted) {
Checks if this branch was attracted to food.
newThickness *= 1.2;
Boosts the child branch thickness by 20% if the parent was attracted, making food-seeking branches more visually prominent.
let newBranch = new Branch(this.end, newAngle, newLength, newThickness, this.level + 1, this, false, null, attracted);
Creates a new Branch starting from this branch's end point, with the calculated angle, length, thickness, and level+1. Sets isFusion to false and passes the attracted flag.
this.children.push(newBranch);
Adds the new branch to this branch's children array, maintaining the parent-child relationship.
this.grown = true;
Sets the grown flag to true so this branch won't create children again.

Mycelium class

The Mycelium class manages the entire network lifecycle. It grows() by selecting random tips to expand each frame (creating natural variation), detects anastomosis() when tips fuse together, manages food consumption, and displays() all branches. It's the orchestrator—organizing the thousands of individual branch-rendering calls and collision detections into a coherent system.

🔬 This loop checks if a branch tip touches food. What happens if you remove the break; line inside the food loop? Will a single tip be able to consume multiple food sources in one frame?

    for (let i = 0; i < currentActiveTipsForFusion.length; i++) {
      let tipA = currentActiveTipsForFusion[i];
      if (tipsToRemove.has(tipA)) continue;

      let consumedFood = false;
      for (let food of foodSources) {
        if (!food.alive) continue;
        let dSqFood = p5.Vector.sub(tipA.end, food.position).magSq();
        if (dSqFood < (food.radius * food.radius)) {
          foodSourcesToConsume.set(food, (foodSourcesToConsume.get(food) || 0) + CONSUMPTION_RATE);
          tipA.grown = true;
          tipsToRemove.add(tipA);
          consumedFood = true;
          break;
        }
      }
      if (consumedFood) continue;
class Mycelium {
  constructor(x, y) {
    this.branches = [];
    this.activeTips = [];

    const numRoots = 5;
    const angleStep = TWO_PI / numRoots;

    for (let i = 0; i < numRoots; i++) {
      let angle = -HALF_PI + i * angleStep;
      let rootBranch = new Branch(createVector(x, y), angle, BRANCH_LENGTH, BRANCH_THICKNESS, 0);
      this.branches.push(rootBranch);
      this.activeTips.push(rootBranch);
    }
  }

  grow() {
    let numTipsToGrowThisFrame = floor(random(1, 4));
    numTipsToGrowThisFrame = min(numTipsToGrowThisFrame, this.activeTips.length);

    let tipsToGrow = [];
    let availableTipsForNextFrame = [...this.activeTips];

    for (let i = 0; i < numTipsToGrowThisFrame; i++) {
      if (availableTipsForNextFrame.length === 0) break;
      let randomIndex = floor(random(availableTipsForNextFrame.length));
      tipsToGrow.push(availableTipsForNextFrame.splice(randomIndex, 1)[0]);
    }

    let newChildren = [];

    for (let tip of tipsToGrow) {
      if (!tip.grown) {
        let childrenCountBefore = tip.children.length;
        tip.grow(foodSources);
        if (tip.children.length > childrenCountBefore) {
          tip.grown = true;
          for (let child of tip.children) {
            this.branches.push(child);
            newChildren.push(child);
          }
        } else {
          availableTipsForNextFrame.push(tip);
        }
      }
    }

    this.activeTips = availableTipsForNextFrame.concat(newChildren);
  }

  anastomosis() {
    let fusionThresholdSq = ANASTOMOSIS_THRESHOLD * ANASTOMOSIS_THRESHOLD;
    let tipsToRemove = new Set();
    let foodSourcesToConsume = new Map();

    let currentActiveTipsForFusion = this.activeTips.filter(tip => !tip.grown);

    for (let i = 0; i < currentActiveTipsForFusion.length; i++) {
      let tipA = currentActiveTipsForFusion[i];
      if (tipsToRemove.has(tipA)) continue;

      let consumedFood = false;
      for (let food of foodSources) {
        if (!food.alive) continue;
        let dSqFood = p5.Vector.sub(tipA.end, food.position).magSq();
        if (dSqFood < (food.radius * food.radius)) {
          foodSourcesToConsume.set(food, (foodSourcesToConsume.get(food) || 0) + CONSUMPTION_RATE);
          tipA.grown = true;
          tipsToRemove.add(tipA);
          consumedFood = true;
          break;
        }
      }
      if (consumedFood) continue;

      for (let j = i + 1; j < currentActiveTipsForFusion.length; j++) {
        let tipB = currentActiveTipsForFusion[j];
        if (tipsToRemove.has(tipB)) continue;

        let related = false;
        let current = tipB;
        while (current) {
          if (current === tipA) {
            related = true;
            break;
          }
          current = current.parent;
        }        if (related) continue;

        let dSq = p5.Vector.sub(tipA.end, tipB.end).magSq();

        if (dSq < fusionThresholdSq) {
          let fusionBranch_AB = new Branch(tipA.end, atan2(tipB.end.y - tipA.end.y, tipB.end.x - tipA.end.x),
            sqrt(dSq), min(tipA.thickness, tipB.thickness) * 0.8, max(tipA.level, tipB.level) + 1, tipA, true);
          let fusionBranch_BA = new Branch(tipB.end, atan2(tipA.end.y - tipB.end.y, tipA.end.x - tipB.end.x),
            sqrt(dSq), min(tipA.thickness, tipB.thickness) * 0.8, max(tipA.level, tipB.level) + 1, tipB, true);

          fusionBranch_AB.oppositeFusionBranch = fusionBranch_BA;
          fusionBranch_BA.oppositeFusionBranch = fusionBranch_AB;

          this.branches.push(fusionBranch_AB);
          this.branches.push(fusionBranch_BA);

          tipA.children.push(fusionBranch_AB);
          tipB.children.push(fusionBranch_BA);

          tipA.grown = true;
          tipB.grown = true;
          tipsToRemove.add(tipA);
          tipsToRemove.add(tipB);

          let flashX = (tipA.end.x + tipB.end.x) / 2;
          let flashY = (tipA.end.y + tipB.end.y) / 2;
          flashes.push(new Flash(flashX, flashY));

          break;
        }
      }
    }

    for (let [food, amount] of foodSourcesToConsume.entries()) {
      food.consume(amount);
    }
    foodSources = foodSources.filter(food => food.alive);

    this.activeTips = this.activeTips.filter(tip => !tipsToRemove.has(tip));
  }

  display() {
    for (let branch of this.branches) {
      branch.display();
    }
  }
}
Line-by-line explanation (75 lines)

🔧 Subcomponents:

constructor Mycelium Initialization constructor(x, y)

Creates 5 root branches radiating outward from a center point, each added to branches and activeTips arrays

for-loop Root Branch Radial Spread for (let i = 0; i < numRoots; i++) { let angle = -HALF_PI + i * angleStep; let rootBranch = new Branch(createVector(x, y), angle, BRANCH_LENGTH, BRANCH_THICKNESS, 0); this.branches.push(rootBranch); this.activeTips.push(rootBranch); }

Creates 5 branches evenly spaced around a circle using TWO_PI divided into 5 parts, starting at -HALF_PI (upward)

calculation Frame Growth Selection let numTipsToGrowThisFrame = floor(random(1, 4)); numTipsToGrowThisFrame = min(numTipsToGrowThisFrame, this.activeTips.length);

Randomly selects 1-3 active tips to grow this frame, capped by the actual number of available tips

for-loop Random Tip Selection for (let i = 0; i < numTipsToGrowThisFrame; i++) { if (availableTipsForNextFrame.length === 0) break; let randomIndex = floor(random(availableTipsForNextFrame.length)); tipsToGrow.push(availableTipsForNextFrame.splice(randomIndex, 1)[0]); }

Randomly removes numTipsToGrowThisFrame tips from availableTipsForNextFrame and adds them to tipsToGrow (without replacement)

for-loop Tip Growth Application for (let tip of tipsToGrow) { if (!tip.grown) { let childrenCountBefore = tip.children.length; tip.grow(foodSources); if (tip.children.length > childrenCountBefore) { tip.grown = true; for (let child of tip.children) { this.branches.push(child); newChildren.push(child); } } else { availableTipsForNextFrame.push(tip); } } }

Calls grow() on each selected tip, adds newly created child branches to the branches array, and tracks them as new active tips for the next frame

for-loop Food Contact Detection for (let food of foodSources) { if (!food.alive) continue; let dSqFood = p5.Vector.sub(tipA.end, food.position).magSq(); if (dSqFood < (food.radius * food.radius)) { foodSourcesToConsume.set(food, (foodSourcesToConsume.get(food) || 0) + CONSUMPTION_RATE); tipA.grown = true; tipsToRemove.add(tipA); consumedFood = true; break; } }

Checks if any branch tip has reached a food source and marks it for consumption

for-loop Branch Fusion Detection for (let j = i + 1; j < currentActiveTipsForFusion.length; j++) { let tipB = currentActiveTipsForFusion[j]; if (tipsToRemove.has(tipB)) continue; let related = false; let current = tipB; while (current) { if (current === tipA) { related = true; break; } current = current.parent; } if (related) continue; let dSq = p5.Vector.sub(tipA.end, tipB.end).magSq(); if (dSq < fusionThresholdSq) {

Compares all pairs of tips to find two separate (non-related) tips closer than ANASTOMOSIS_THRESHOLD pixels, triggering fusion

calculation Fusion Branch Creation let fusionBranch_AB = new Branch(tipA.end, atan2(tipB.end.y - tipA.end.y, tipB.end.x - tipA.end.x), sqrt(dSq), min(tipA.thickness, tipB.thickness) * 0.8, max(tipA.level, tipB.level) + 1, tipA, true); let fusionBranch_BA = new Branch(tipB.end, atan2(tipA.end.y - tipB.end.y, tipA.end.x - tipB.end.x), sqrt(dSq), min(tipA.thickness, tipB.thickness) * 0.8, max(tipA.level, tipB.level) + 1, tipB, true);

Creates two fusion branches (cyan colored) connecting the two tips; they point toward each other and know about each other via oppositeFusionBranch

this.branches = [];
Initializes an empty array to store all branches in the mycelium network.
this.activeTips = [];
Initializes an empty array to store branch tips that can still grow (not yet max depth or fused).
const numRoots = 5;
Sets the number of initial root branches to 5.
const angleStep = TWO_PI / numRoots;
Divides the full circle (TWO_PI radians) by 5 to get the angle between each root (approximately 1.26 radians or 72 degrees).
let angle = -HALF_PI + i * angleStep;
Calculates the angle for root i, starting at -HALF_PI (pointing up) and stepping around the circle.
let rootBranch = new Branch(createVector(x, y), angle, BRANCH_LENGTH, BRANCH_THICKNESS, 0);
Creates a new Branch at position (x, y) with the calculated angle, starting length, thickness, and level 0.
this.branches.push(rootBranch);
Adds the root branch to the branches array.
this.activeTips.push(rootBranch);
Adds the root branch to activeTips so it can grow children on the first frame.
let numTipsToGrowThisFrame = floor(random(1, 4));
Randomly selects 1, 2, or 3 as the number of tips to grow this frame, creating variability in growth speed.
numTipsToGrowThisFrame = min(numTipsToGrowThisFrame, this.activeTips.length);
Ensures we don't try to grow more tips than exist; caps the growth count to the actual number of available tips.
let tipsToGrow = [];
Creates an empty array to store the tips that will grow this frame.
let availableTipsForNextFrame = [...this.activeTips];
Creates a shallow copy of the activeTips array using spread syntax, which will be modified as tips are selected.
for (let i = 0; i < numTipsToGrowThisFrame; i++) {
Loops the number of times specified by numTipsToGrowThisFrame.
if (availableTipsForNextFrame.length === 0) break;
Exits the loop early if there are no more tips available (shouldn't happen if min() works correctly, but defensive coding).
let randomIndex = floor(random(availableTipsForNextFrame.length));
Picks a random index from the availableTipsForNextFrame array.
tipsToGrow.push(availableTipsForNextFrame.splice(randomIndex, 1)[0]);
Removes the tip at randomIndex from availableTipsForNextFrame (splice returns an array, [0] gets the removed tip) and adds it to tipsToGrow. This ensures each tip is selected at most once per frame.
let newChildren = [];
Creates an empty array to collect all newly created child branches.
for (let tip of tipsToGrow) {
Loops through each tip selected to grow.
if (!tip.grown) {
Checks if the tip hasn't already grown children.
let childrenCountBefore = tip.children.length;
Stores the current number of children so we can detect if new ones were created.
tip.grow(foodSources);
Calls the tip's grow() method, passing foodSources for attraction calculation. This creates new children in tip.children.
if (tip.children.length > childrenCountBefore) {
Checks if new children were actually created (children count increased).
tip.grown = true;
Marks the tip as grown so it won't try to grow again.
for (let child of tip.children) {
Loops through all children of this tip.
this.branches.push(child);
Adds each child branch to the global branches array so it can be displayed.
newChildren.push(child);
Also adds the child to newChildren so it becomes an active tip for the next frame.
} else {
Handles the case where grow() didn't create new children (can happen if at max level).
availableTipsForNextFrame.push(tip);
Puts the tip back into availableTipsForNextFrame so it can try again next frame.
this.activeTips = availableTipsForNextFrame.concat(newChildren);
Sets activeTips to the remaining tips (that haven't grown yet) plus the newly created children, ready for the next frame.
let fusionThresholdSq = ANASTOMOSIS_THRESHOLD * ANASTOMOSIS_THRESHOLD;
Pre-calculates the squared threshold distance to avoid doing sqrt() in the comparison loop (faster).
let tipsToRemove = new Set();
Creates a Set to efficiently track which tips have fused or consumed food (faster lookup than arrays).
let foodSourcesToConsume = new Map();
Creates a Map to track how much nutrition each food source should lose (in case multiple tips touch it same frame).
let currentActiveTipsForFusion = this.activeTips.filter(tip => !tip.grown);
Creates an array of only ungrown tips (still growing), since grown tips can't fuse again.
for (let i = 0; i < currentActiveTipsForFusion.length; i++) {
Loops through each ungrown tip to check for food or fusion.
let tipA = currentActiveTipsForFusion[i];
Stores the current tip for cleaner code.
if (tipsToRemove.has(tipA)) continue;
Skips this tip if it's already been fused or consumed food this frame.
let consumedFood = false;
Initializes a flag to track if this tip touched food (prevents checking for fusion if food was consumed).
let dSqFood = p5.Vector.sub(tipA.end, food.position).magSq();
Calculates the squared distance from the tip to the food (magSq is faster than dist because it skips sqrt).
if (dSqFood < (food.radius * food.radius)) {
Checks if the tip is within the food's radius (comparing squared distances).
foodSourcesToConsume.set(food, (foodSourcesToConsume.get(food) || 0) + CONSUMPTION_RATE);
Adds CONSUMPTION_RATE (5) to the total consumption amount for this food (using || 0 to handle first time).
tipA.grown = true;
Marks the tip as grown since it found food and stops growing.
tipsToRemove.add(tipA);
Adds the tip to the removal set.
consumedFood = true;
Sets the flag so we skip fusion checking for this tip.
break;
Exits the food loop since this tip already found food (it won't consume multiple food sources in one frame).
if (consumedFood) continue;
Skips fusion checking if this tip already touched food.
for (let j = i + 1; j < currentActiveTipsForFusion.length; j++) {
Inner loop comparing tipA to all tips after it in the array (avoids comparing same pair twice).
let tipB = currentActiveTipsForFusion[j];
The second tip to compare.
if (tipsToRemove.has(tipB)) continue;
Skips tipB if it's already been processed.
let related = false;
Initializes a flag to track if the two tips share a common ancestor.
let current = tipB;
Starts traversal from tipB.
while (current) {
Walks up the parent chain from tipB toward the root.
if (current === tipA) {
Checks if we've encountered tipA while walking up from tipB, meaning they're in the same tree.
related = true;
Sets the related flag.
break;
Exits the while loop.
current = current.parent;
Moves up one level in the tree.
if (related) continue;
Skips fusion if the two tips are from the same network (they can't fuse with themselves).
let dSq = p5.Vector.sub(tipA.end, tipB.end).magSq();
Calculates the squared distance between the two tips' endpoints.
if (dSq < fusionThresholdSq) {
Checks if the tips are closer than ANASTOMOSIS_THRESHOLD (25 pixels by default).
let fusionBranch_AB = new Branch(tipA.end, atan2(tipB.end.y - tipA.end.y, tipB.end.x - tipA.end.x), sqrt(dSq), min(tipA.thickness, tipB.thickness) * 0.8, max(tipA.level, tipB.level) + 1, tipA, true);
Creates a fusion branch from tipA to tipB: it starts at tipA.end, points toward tipB.end (calculated with atan2), has length equal to the distance between tips, thickness is 80% of the thinner tip, level is max(levels)+1, parent is tipA, and isFusion is true.
let fusionBranch_BA = new Branch(tipB.end, atan2(tipA.end.y - tipB.end.y, tipA.end.x - tipB.end.x), sqrt(dSq), min(tipA.thickness, tipB.thickness) * 0.8, max(tipA.level, tipB.level) + 1, tipB, true);
Creates the reciprocal fusion branch from tipB to tipA with the same logic but opposite direction.
fusionBranch_AB.oppositeFusionBranch = fusionBranch_BA;
Links each fusion branch to its opposite so nutrient packets can cross between them.
fusionBranch_BA.oppositeFusionBranch = fusionBranch_AB;
Sets the reverse link.
this.branches.push(fusionBranch_AB); this.branches.push(fusionBranch_BA);
Adds both fusion branches to the global branches array so they'll be displayed.
tipA.children.push(fusionBranch_AB); tipB.children.push(fusionBranch_BA);
Adds fusion branches as children of their parent tips so nutrient packets can traverse them.
tipA.grown = true; tipB.grown = true;
Marks both tips as grown since they've fused.
tipsToRemove.add(tipA); tipsToRemove.add(tipB);
Adds both tips to the removal set.
let flashX = (tipA.end.x + tipB.end.x) / 2; let flashY = (tipA.end.y + tipB.end.y) / 2;
Calculates the midpoint between the two tips for the flash effect.
flashes.push(new Flash(flashX, flashY));
Creates a white flash animation at the fusion point.
break;
Exits the inner j loop (each tipA only fuses with the first nearby tipB it finds).
for (let [food, amount] of foodSourcesToConsume.entries()) {
Loops through the Map of food sources to consume, destructuring each entry into [food, amount].
food.consume(amount);
Calls consume() on each food source, reducing its nutrients by the total amount collected this frame.
foodSources = foodSources.filter(food => food.alive);
Removes dead food sources from the global foodSources array so they won't be drawn or checked anymore.
this.activeTips = this.activeTips.filter(tip => !tipsToRemove.has(tip));
Removes fused and consumed tips from activeTips, leaving only tips that are still growing.
for (let branch of this.branches) {
Loops through every branch in the mycelium.
branch.display();
Calls the display method on each branch, drawing it with its glow effect.

FoodSource class

FoodSource represents an interactive target planted by mouse clicks. It shrinks as it's consumed and pulses to draw visual attention. The three-layer display (faint radius, semi-transparent halo, bright core) guides the viewer's eye and makes the attraction radius obvious without being intrusive.

class FoodSource {
  constructor(x, y) {
    this.position = createVector(x, y);
    this.initialRadius = 10;
    this.radius = this.initialRadius;
    this.nutrients = MAX_NUTRIENTS;
    this.alive = true;
    this.pulseOffset = random(TWO_PI);
  }

  display() {
    if (!this.alive) return;

    let baseRadius = map(this.nutrients, 0, MAX_NUTRIENTS, 0, this.initialRadius, true);
    let currentRadius = baseRadius * (1 + sin(frameCount * FOOD_PULSE_SPEED + this.pulseOffset) * FOOD_PULSE_AMPLITUDE);

    noStroke();
    fill(TEAL_BLUE[0], TEAL_BLUE[1], TEAL_BLUE[2], 30);
    ellipse(this.position.x, this.position.y, FOOD_ATTRACTION_RADIUS * 2);

    fill(TEAL_BLUE[0], TEAL_BLUE[1], TEAL_BLUE[2], 100);
    ellipse(this.position.x, this.position.y, currentRadius * 3);

    fill(TEAL_BLUE[0], TEAL_BLUE[1], TEAL_BLUE[2]);
    ellipse(this.position.x, this.position.y, currentRadius * 2);
  }

  consume(amount) {
    this.nutrients -= amount;
    if (this.nutrients <= 0) {
      this.nutrients = 0;
      this.alive = false;
    }
    this.radius = map(this.nutrients, 0, MAX_NUTRIENTS, 0, this.initialRadius, true);
  }
}
Line-by-line explanation (20 lines)

🔧 Subcomponents:

calculation Pulsing Animation let currentRadius = baseRadius * (1 + sin(frameCount * FOOD_PULSE_SPEED + this.pulseOffset) * FOOD_PULSE_AMPLITUDE);

Uses a sine wave to smoothly oscillate the food's size, creating a living pulsing effect

calculation Layered Visual Hierarchy ellipse(this.position.x, this.position.y, FOOD_ATTRACTION_RADIUS * 2); fill(TEAL_BLUE[0], TEAL_BLUE[1], TEAL_BLUE[2], 100); ellipse(this.position.x, this.position.y, currentRadius * 3); fill(TEAL_BLUE[0], TEAL_BLUE[1], TEAL_BLUE[2]); ellipse(this.position.x, this.position.y, currentRadius * 2);

Draws three circles: a faint attraction radius, a semi-transparent halo, and a bright core, creating depth

this.position = createVector(x, y);
Stores the food source's position as a p5.Vector at coordinates (x, y).
this.initialRadius = 10;
Sets the base radius of the food source to 10 pixels.
this.radius = this.initialRadius;
Initializes the current radius to the initial radius (changes as the food is consumed).
this.nutrients = MAX_NUTRIENTS;
Sets the nutrient count to 100, the maximum.
this.alive = true;
Marks the food source as alive (won't be removed from the array).
this.pulseOffset = random(TWO_PI);
Randomizes the starting phase of the pulse animation so multiple food sources pulse at different times.
if (!this.alive) return;
Exits the display() function early if the food is dead, preventing it from being drawn.
let baseRadius = map(this.nutrients, 0, MAX_NUTRIENTS, 0, this.initialRadius, true);
Maps the current nutrient level (0-100) to a radius (0-10 pixels), so as nutrients decrease, the food shrinks.
let currentRadius = baseRadius * (1 + sin(frameCount * FOOD_PULSE_SPEED + this.pulseOffset) * FOOD_PULSE_AMPLITUDE);
Creates a pulsing effect: adds a sine wave (oscillating -1 to 1) multiplied by amplitude (0.3) and offset (unique for each food), scaled by baseRadius. The result is constant pulsing between baseRadius * 0.7 and baseRadius * 1.3.
fill(TEAL_BLUE[0], TEAL_BLUE[1], TEAL_BLUE[2], 30);
Sets fill color to teal with very low alpha (30) for a faint overlay.
ellipse(this.position.x, this.position.y, FOOD_ATTRACTION_RADIUS * 2);
Draws a large faint circle with diameter 200 pixels (100 radius * 2), showing the area where the mycelium will be attracted.
fill(TEAL_BLUE[0], TEAL_BLUE[1], TEAL_BLUE[2], 100);
Sets fill color to teal with medium alpha (100).
ellipse(this.position.x, this.position.y, currentRadius * 3);
Draws a medium-sized semi-transparent halo around the food, using the pulsing radius.
fill(TEAL_BLUE[0], TEAL_BLUE[1], TEAL_BLUE[2]);
Sets fill to opaque teal (default alpha 255).
ellipse(this.position.x, this.position.y, currentRadius * 2);
Draws the bright core circle with full opacity, pulsing in size.
this.nutrients -= amount;
Decreases the nutrient count by the amount consumed (5 points per frame if a branch tip is touching).
if (this.nutrients <= 0) {
Checks if nutrients have been fully depleted.
this.nutrients = 0;
Clamps nutrients to 0 (prevents negative values).
this.alive = false;
Marks the food source as dead, triggering its removal from the array in the next anastomosis() call.
this.radius = map(this.nutrients, 0, MAX_NUTRIENTS, 0, this.initialRadius, true);
Updates the radius based on remaining nutrients so the food shrinks as it's consumed.

NutrientPacket class

NutrientPacket implements a sophisticated pathfinding system that flows through the mycelium's branching structure. It intelligently navigates branches, randomly explores siblings, uses fusion bridges to cross between separate networks, and maintains a glowing trail. This class demonstrates complex state management and recursive tree traversal, all running 30+ times per frame.

🔬 This code keeps a 4-position tail history. What happens if you change 4 to 10 or 20? Will the packets have longer glowing trails?

    this.trail.push(this.position.copy());
    if (this.trail.length > 4) {
      this.trail.shift();
    }
class NutrientPacket {
  constructor(startBranch) {
    this.currentBranch = startBranch;
    this.progressOnBranch = 0;
    this.speed = PACKET_SPEED;
    this.position = startBranch.start.copy();
    this.alive = true;
    this.direction = 1;
    this.trail = [];
    this.previousBranch = null;
  }

  update() {
    if (!this.alive) return;

    this.progressOnBranch += this.speed * this.direction;

    let branchChanged = false;

    if (this.direction === 1) {
      if (this.progressOnBranch >= 1) {
        this.progressOnBranch = 1;
        branchChanged = true;
      }
    } else {
      if (this.progressOnBranch <= 0) {
        this.progressOnBranch = 0;
        branchChanged = true;
      }
    }

    if (branchChanged) {
      let oldBranch = this.currentBranch;

      if (this.direction === 1) {
        let availableChildren = this.currentBranch.children.filter(child => child !== this.previousBranch);

        if (availableChildren.length > 0) {
          this.currentBranch = random(availableChildren);
          this.progressOnBranch = 0;
          this.direction = 1;
        } else {
          if (this.currentBranch.isFusionBranch) {
            this.currentBranch = this.currentBranch.oppositeFusionBranch;
            this.progressOnBranch = 0;
            this.direction = 1;
          } else if (this.currentBranch.parent) {
            this.currentBranch = this.currentBranch.parent;
            this.progressOnBranch = 1;
            this.direction = -1;
          } else {
            this.progressOnBranch = 1;
            this.direction = -1;
          }
        }
      } else {
        if (this.currentBranch.isFusionBranch) {
          this.currentBranch = this.currentBranch.oppositeFusionBranch;
          this.progressOnBranch = 1;
          this.direction = -1;
        } else if (this.currentBranch.parent) {
          let parentBranch = this.currentBranch.parent;
          let siblings = parentBranch.children.filter(child => child !== this.currentBranch);

          if (siblings.length > 0 && random() < PACKET_SIBLING_CHANCE) {
            this.currentBranch = random(siblings);
            this.progressOnBranch = 0;
            this.direction = 1;
          } else {
            this.currentBranch = parentBranch;
            this.progressOnBranch = 1;
            this.direction = -1;
          }
        } else {
          this.progressOnBranch = 0;
          this.direction = 1;
        }
      }
      this.previousBranch = oldBranch;
    }

    this.position.x = lerp(this.currentBranch.start.x, this.currentBranch.end.x, this.progressOnBranch);
    this.position.y = lerp(this.currentBranch.start.y, this.currentBranch.end.y, this.progressOnBranch);

    this.trail.push(this.position.copy());
    if (this.trail.length > 4) {
      this.trail.shift();
    }
  }

  display() {
    if (!this.alive) return;

    let trailAlphaStep = 30;

    for (let i = 0; i < this.trail.length; i++) {
      let trailPos = this.trail[i];
      let trailAlpha = (i + 1) * trailAlphaStep;
      fill(BRIGHT_GREEN[0], BRIGHT_GREEN[1], BRIGHT_GREEN[2], trailAlpha);
      noStroke();
      ellipse(trailPos.x, trailPos.y, PACKET_SIZE);
    }

    let currentPacketSize = PACKET_SIZE * (this.currentBranch.isFusionBranch ? 1.5 : 1);
    let currentPacketAlpha = this.currentBranch.isFusionBranch ? 255 : 200;

    noStroke();
    fill(BRIGHT_GREEN[0], BRIGHT_GREEN[1], BRIGHT_GREEN[2], currentPacketAlpha * 0.5);
    ellipse(this.position.x, this.position.y, currentPacketSize * 3);

    fill(BRIGHT_GREEN[0], BRIGHT_GREEN[1], BRIGHT_GREEN[2], currentPacketAlpha);
    ellipse(this.position.x, this.position.y, currentPacketSize * 2);
  }
}
Line-by-line explanation (60 lines)

🔧 Subcomponents:

calculation Branch Path Following this.position.x = lerp(this.currentBranch.start.x, this.currentBranch.end.x, this.progressOnBranch); this.position.y = lerp(this.currentBranch.start.y, this.currentBranch.end.y, this.progressOnBranch);

Interpolates the packet's position along the current branch from start to end based on progress (0 to 1)

conditional Forward Pathfinding Logic if (this.direction === 1) { let availableChildren = this.currentBranch.children.filter(child => child !== this.previousBranch); if (availableChildren.length > 0) { this.currentBranch = random(availableChildren); this.progressOnBranch = 0; this.direction = 1; } else { if (this.currentBranch.isFusionBranch) { this.currentBranch = this.currentBranch.oppositeFusionBranch; this.progressOnBranch = 0; this.direction = 1; } else if (this.currentBranch.parent) { this.currentBranch = this.currentBranch.parent; this.progressOnBranch = 1; this.direction = -1; } else { this.progressOnBranch = 1; this.direction = -1; } }

Decides which branch to traverse next when moving forward (toward tips): picks a random child, uses fusion to cross to another network, or backtracks to parent

conditional Backward Pathfinding Logic } else { if (this.currentBranch.isFusionBranch) { this.currentBranch = this.currentBranch.oppositeFusionBranch; this.progressOnBranch = 1; this.direction = -1; } else if (this.currentBranch.parent) { let parentBranch = this.currentBranch.parent; let siblings = parentBranch.children.filter(child => child !== this.currentBranch); if (siblings.length > 0 && random() < PACKET_SIBLING_CHANCE) { this.currentBranch = random(siblings); this.progressOnBranch = 0; this.direction = 1; } else { this.currentBranch = parentBranch; this.progressOnBranch = 1; this.direction = -1; } } else { this.progressOnBranch = 0; this.direction = 1; }

Decides which branch to traverse when moving backward (toward root): uses fusion to cross, continues to parent, or randomly explores siblings

calculation Motion Trail this.trail.push(this.position.copy()); if (this.trail.length > 4) { this.trail.shift(); }

Maintains a short buffer of the last 4 positions, creating a glowing tail effect when drawn

this.currentBranch = startBranch;
Sets the packet's starting branch (passed in from draw()).
this.progressOnBranch = 0;
Initializes progress to 0 (the packet starts at the beginning of its branch).
this.speed = PACKET_SPEED;
Sets the movement speed to 0.02, meaning the packet moves 2% along the current branch each frame.
this.position = startBranch.start.copy();
Sets the packet's visual position to the start of the branch.
this.alive = true;
Marks the packet as alive (won't be culled from the array).
this.direction = 1;
Initializes direction to 1 (moving forward toward branch tips); -1 means moving backward toward roots.
this.trail = [];
Initializes an empty array to hold previous positions for the tail effect.
this.previousBranch = null;
Tracks the branch the packet just came from, preventing it from immediately backtracking.
if (!this.alive) return;
Exits early if the packet is dead.
this.progressOnBranch += this.speed * this.direction;
Moves the progress along the branch by 0.02 in the current direction (positive or negative).
let branchChanged = false;
Initializes a flag to track whether the packet needs to move to a new branch.
if (this.direction === 1) {
Checks if moving forward.
if (this.progressOnBranch >= 1) {
Checks if the packet has reached the end of the branch (progress >= 1).
this.progressOnBranch = 1;
Clamps progress to 1 (the endpoint).
branchChanged = true;
Sets the flag so we'll find the next branch.
} else {
Moving backward.
if (this.progressOnBranch <= 0) {
Checks if the packet has reached the start of the branch (progress <= 0).
this.progressOnBranch = 0;
Clamps progress to 0 (the start point).
let oldBranch = this.currentBranch;
Stores the current branch before switching.
let availableChildren = this.currentBranch.children.filter(child => child !== this.previousBranch);
Gets all child branches except the one the packet just came from (prevents immediate backtracking).
if (availableChildren.length > 0) {
Checks if there are any children to move to.
this.currentBranch = random(availableChildren);
Randomly picks one of the available children.
this.progressOnBranch = 0;
Resets progress to 0 at the start of the new branch.
this.direction = 1;
Continues forward on the new branch.
if (this.currentBranch.isFusionBranch) {
Checks if the current branch is a fusion bridge connecting two networks.
this.currentBranch = this.currentBranch.oppositeFusionBranch;
Crosses to the opposite fusion branch to move into the other network.
this.progressOnBranch = 0;
Resets to the start of the opposite fusion branch.
} else if (this.currentBranch.parent) {
Checks if this branch has a parent (not a root).
this.currentBranch = this.currentBranch.parent;
Moves to the parent branch.
this.progressOnBranch = 1;
Sets progress to 1 (at the end of the parent, where this branch started).
this.direction = -1;
Switches to moving backward toward the root.
} else {
The packet is at a root with no children—can't go forward.
this.progressOnBranch = 1;
Stays at the end.
let parentBranch = this.currentBranch.parent;
Stores the parent branch.
let siblings = parentBranch.children.filter(child => child !== this.currentBranch);
Gets all sibling branches (children of the parent, except this one).
if (siblings.length > 0 && random() < PACKET_SIBLING_CHANCE) {
Randomly decides (60% chance) whether to explore a sibling branch instead of continuing backward.
this.currentBranch = random(siblings);
Picks a random sibling.
this.progressOnBranch = 0;
Starts from the beginning of the sibling.
this.direction = 1;
Moves forward on the sibling.
this.currentBranch = parentBranch;
Continues moving to the parent.
this.progressOnBranch = 1;
Ends at the parent branch's endpoint (where this branch originated).
this.progressOnBranch = 0;
At the root with nowhere to go—stay at start.
this.previousBranch = oldBranch;
Records the old branch so we won't immediately backtrack to it.
this.position.x = lerp(this.currentBranch.start.x, this.currentBranch.end.x, this.progressOnBranch);
Interpolates the x position between the branch's start and end based on progress (0 = start, 1 = end).
this.position.y = lerp(this.currentBranch.start.y, this.currentBranch.end.y, this.progressOnBranch);
Interpolates the y position the same way.
this.trail.push(this.position.copy());
Adds a copy of the current position to the trail array.
if (this.trail.length > 4) {
Checks if the trail is too long.
this.trail.shift();
Removes the oldest position from the trail, keeping it at 4 positions (a short tail).
let trailAlphaStep = 30;
Sets the alpha increment per trail segment (so segment 1 is 30 alpha, segment 2 is 60, etc.).
for (let i = 0; i < this.trail.length; i++) {
Loops through each position in the trail.
let trailPos = this.trail[i];
Gets the current trail position.
let trailAlpha = (i + 1) * trailAlphaStep;
Calculates the alpha: older positions (lower i) are fainter, newer positions are brighter.
fill(BRIGHT_GREEN[0], BRIGHT_GREEN[1], BRIGHT_GREEN[2], trailAlpha);
Sets the fill to green with the calculated alpha.
ellipse(trailPos.x, trailPos.y, PACKET_SIZE);
Draws a green circle at the trail position.
let currentPacketSize = PACKET_SIZE * (this.currentBranch.isFusionBranch ? 1.5 : 1);
Makes the packet 1.5x larger if on a fusion branch, otherwise normal size.
let currentPacketAlpha = this.currentBranch.isFusionBranch ? 255 : 200;
Makes fusion branch packets fully opaque, regular packets slightly transparent.
fill(BRIGHT_GREEN[0], BRIGHT_GREEN[1], BRIGHT_GREEN[2], currentPacketAlpha * 0.5);
Sets a semi-transparent fill for the glow layer.
ellipse(this.position.x, this.position.y, currentPacketSize * 3);
Draws a large semi-transparent glow circle.
fill(BRIGHT_GREEN[0], BRIGHT_GREEN[1], BRIGHT_GREEN[2], currentPacketAlpha);
Sets full opacity for the core.
ellipse(this.position.x, this.position.y, currentPacketSize * 2);
Draws the bright green core circle, creating a glowing effect combined with the glow layer.

Flash class

Flash is a simple particle effect spawned whenever two branch tips fuse. It provides visual feedback of the fusion event by flashing bright white and fading over half a second. It's a minimal but effective example of an ephemeral animation—one that exists briefly and self-removes when complete.

class Flash {
  constructor(x, y) {
    this.position = createVector(x, y);
    this.radius = 20;
    this.alpha = 255;
    this.duration = 30;
    this.frameCount = 0;
  }

  update() {
    this.frameCount++;
    if (this.frameCount <= this.duration) {
      this.alpha = map(this.frameCount, 0, this.duration, 255, 0);
    } else {
      this.alpha = 0;
    }
  }

  display() {
    if (this.alpha <= 0) return;

    noStroke();
    fill(255, this.alpha);
    ellipse(this.position.x, this.position.y, this.radius * 2);
  }

  isAlive() {
    return this.alpha > 0;
  }
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

calculation Fade-Out Animation if (this.frameCount <= this.duration) { this.alpha = map(this.frameCount, 0, this.duration, 255, 0); } else { this.alpha = 0; }

Smoothly maps frame count to alpha, creating a 30-frame fade-out effect

this.position = createVector(x, y);
Stores the position where the flash will appear (typically a branch fusion midpoint).
this.radius = 20;
Sets the flash's radius to 20 pixels (diameter 40).
this.alpha = 255;
Initializes the flash at full opacity (255).
this.duration = 30;
Sets the flash to fade out over 30 frames (about 0.5 seconds at 60 FPS).
this.frameCount = 0;
Initializes a counter to track how many frames have elapsed.
this.frameCount++;
Increments the frame counter each update.
if (this.frameCount <= this.duration) {
Checks if the fade duration hasn't been exceeded.
this.alpha = map(this.frameCount, 0, this.duration, 255, 0);
Maps frameCount from 0-30 to alpha from 255-0, creating a smooth fade-out.
if (this.alpha <= 0) return;
Exits display() early if fully transparent (no need to draw invisible objects).
fill(255, this.alpha);
Sets the fill color to white with the current alpha (fading from bright to invisible).
ellipse(this.position.x, this.position.y, this.radius * 2);
Draws a white circle at the flash position, diameter 40 pixels.
return this.alpha > 0;
Returns true if the flash still has any opacity, false if fully faded (used for removal from the array).

Spore class

Spore provides ambient visual interest—they're the background particle system that drifts upward continuously. Their semi-transparency and small size make them atmospheric rather than distracting. Combined with the glowing mycelium and pulsing food, they contribute to the biological ecosystem feeling.

class Spore {
  constructor(x, y) {
    this.position = createVector(x, y);
    this.xOffset = random(-1, 1);
  }

  update() {
    this.position.y -= SPORE_SPEED;
    this.position.x += this.xOffset * 0.1;

    if (this.position.y < -SPORE_SIZE) {
      this.position.y = height + SPORE_SIZE;
      this.position.x = random(width);
      this.xOffset = random(-1, 1);
    }
  }

  display() {
    noStroke();
    fill(255, SPORE_ALPHA);
    ellipse(this.position.x, this.position.y, SPORE_SIZE);
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Drifting Movement this.position.y -= SPORE_SPEED; this.position.x += this.xOffset * 0.1;

Moves spores upward while drifting horizontally, creating a floating effect

conditional Screen Edge Wrapping if (this.position.y < -SPORE_SIZE) { this.position.y = height + SPORE_SIZE; this.position.x = random(width); this.xOffset = random(-1, 1); }

Recycles spores to the bottom of the screen when they exit the top, giving them a new random horizontal offset

this.position = createVector(x, y);
Stores the spore's starting position.
this.xOffset = random(-1, 1);
Randomly sets a horizontal drift direction between -1 and 1, determining which way the spore will drift.
this.position.y -= SPORE_SPEED;
Moves the spore upward by 0.5 pixels per frame.
this.position.x += this.xOffset * 0.1;
Adds a small horizontal drift (xOffset scaled by 0.1), creating a zigzag upward motion.
if (this.position.y < -SPORE_SIZE) {
Checks if the spore has exited the top of the screen.
this.position.y = height + SPORE_SIZE;
Recycles the spore to the bottom of the screen.
this.position.x = random(width);
Gives it a random new horizontal position.
this.xOffset = random(-1, 1);
Assigns a new random drift direction.
noStroke();
Disables outline drawing.
fill(255, SPORE_ALPHA);
Sets the fill color to white with alpha 80 (semi-transparent).
ellipse(this.position.x, this.position.y, SPORE_SIZE);
Draws a small white circle with diameter 2 pixels.

Ripple class

Ripple provides immediate visual feedback to clicks. Spawned at the cursor position, it expands and fades simultaneously, creating a shimmering wave effect that communicates the interaction without being intrusive. It's a satisfying detail that makes the sketch feel responsive.

class Ripple {
  constructor(x, y) {
    this.position = createVector(x, y);
    this.radius = 0;
    this.alpha = 255;
  }

  update() {
    this.radius += RIPPLE_SPEED;
    this.alpha -= RIPPLE_FADE_SPEED;
  }

  display() {
    if (this.alpha <= 0) return;

    noFill();
    stroke(255, this.alpha);
    strokeWeight(2);
    ellipse(this.position.x, this.position.y, this.radius * 2);
  }

  isAlive() {
    return this.alpha > 0 && this.radius < RIPPLE_MAX_RADIUS;
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

calculation Expanding Ripple this.radius += RIPPLE_SPEED; this.alpha -= RIPPLE_FADE_SPEED;

Expands the ripple 3 pixels per frame while fading it 10 alpha points per frame

this.position = createVector(x, y);
Stores the click position where the ripple originates.
this.radius = 0;
Initializes the ripple at zero radius (starts at a point).
this.alpha = 255;
Initializes the ripple at full opacity.
this.radius += RIPPLE_SPEED;
Expands the ripple by 3 pixels every frame.
this.alpha -= RIPPLE_FADE_SPEED;
Decreases alpha by 10 every frame, causing the ripple to fade as it expands.
if (this.alpha <= 0) return;
Exits display() early if the ripple is fully transparent.
noFill();
Disables fill so only the outline (stroke) is drawn.
stroke(255, this.alpha);
Sets the stroke color to white with the current alpha.
strokeWeight(2);
Sets the line thickness to 2 pixels.
ellipse(this.position.x, this.position.y, this.radius * 2);
Draws a circle outline at the click position with diameter radius*2.
return this.alpha > 0 && this.radius < RIPPLE_MAX_RADIUS;
Returns true if the ripple is still visible and within max radius; false when it's faded or too large.

📦 Key Variables

mycelium object

The root Mycelium object managing the entire branching network; stores all branches and active tips

let mycelium = new Mycelium(width / 2, height / 2);
foodSources array

Global array storing all active FoodSource objects placed by clicks; attraction centers for the mycelium

let foodSources = [];
nutrientPackets array

Global array of NutrientPacket objects flowing through branches, providing visual interest and showing network connectivity

let nutrientPackets = [];
flashes array

Global array of Flash objects that appear briefly when branches fuse

let flashes = [];
spores array

Global array of Spore objects that drift upward, providing ambient background particles

let spores = [];
ripples array

Global array of Ripple objects created on clicks, expanding outward for interaction feedback

let ripples = [];
BRANCH_LENGTH number

Initial length of root branches in pixels; controls how quickly the network spreads

const BRANCH_LENGTH = 40;
BRANCH_DECAY number

Multiplier reducing branch length each generation (0.85 = 85% of parent length); controls taper

const BRANCH_DECAY = 0.85;
BRANCH_THICKNESS number

Initial line thickness of root branches in pixels; controls visual thickness

const BRANCH_THICKNESS = 6;
BRANCH_MAX_LEVEL number

Maximum branching depth; prevents infinite recursion and defines network size limits

const BRANCH_MAX_LEVEL = 11;
BRANCH_SPLIT_PROBABILITY number

Probability (0-1) that a branch splits into multiple children; 0.7 means 70% chance

const BRANCH_SPLIT_PROBABILITY = 0.7;
FOOD_ATTRACTION_RADIUS number

Distance in pixels within which branches are attracted to food sources

const FOOD_ATTRACTION_RADIUS = 100;
FOOD_ATTRACTION_STRENGTH number

Blend amount (0-1) controlling how strongly branches steer toward food; 0.2 = gentle steering

const FOOD_ATTRACTION_STRENGTH = 0.2;
ANASTOMOSIS_THRESHOLD number

Distance in pixels at which two separate branch tips fuse together

const ANASTOMOSIS_THRESHOLD = 25;
SPORE_SPEED number

Pixels per frame that spores move upward

const SPORE_SPEED = 0.5;
PACKET_COUNT number

Target number of nutrient packets flowing through the network at any time

const PACKET_COUNT = 30;
PACKET_SPEED number

Progress speed along branches (0-1) per frame; 0.02 = slow deliberate movement

const PACKET_SPEED = 0.02;

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

PERFORMANCE draw() - nutrient packet spawning

Calls random(mycelium.branches) every frame, which becomes expensive with thousands of branches as the network grows large.

💡 Cache a 'recentBranches' array and pick randomly from that subset instead of the entire branches array, or spawn packets from activeTips instead.

PERFORMANCE Mycelium.anastomosis() - tip comparison loop

Compares every tip pair (O(n²) complexity); with 100+ active tips, this becomes slow. The related-branch check walks up the tree for every comparison, compounding the cost.

💡 Use spatial hashing (grid cells) to only compare tips that are close in space, or maintain a spatial index of branch endpoints.

BUG Branch.grow() - food attraction

If multiple food sources exist within attraction radius, baseAngle is overwritten each loop, so only the last food in the array influences the direction instead of blending all nearby food sources.

💡 Accumulate attraction vectors instead of overwriting baseAngle, then calculate a weighted average direction.

STYLE Global variable organization

Constants are scattered throughout the top of the file with inconsistent grouping (some by system, some by property); hard to find and modify related parameters.

💡 Group constants into objects: e.g., const BRANCH_CONFIG = { LENGTH: 40, DECAY: 0.85, ... }, const FOOD_CONFIG = { ... }, making the code more modular and self-documenting.

FEATURE NutrientPacket pathfinding

Packets always spawn from random branches and flow randomly; no mechanism to detect which branches are most productive or to direct flow toward growing tips.

💡 Modify the spawning logic to preferentially spawn packets from active tips, or add pheromone-like tracking to concentrate flow where branches are actively growing.

BUG Mycelium.grow() - branch re-growth prevention

If a branch doesn't create children (e.g., at max level), it's returned to availableTipsForNextFrame and will keep re-attempting every frame, wasting computation.

💡 Mark branches that can't grow (level >= BRANCH_MAX_LEVEL) and never add them back to active tips; only retry branches that simply haven't grown yet.

🔄 Code Flow

Code flow showing setup, draw, mousepressed, windowresized, branch, mycelium, foodsource, nutrientpacket, flash, spore, ripple

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

graph TD start[Start] --> setup[setup] setup --> canvasinit[canvas-init] canvasinit --> myceliuminit[mycelium-init] myceliuminit --> sporeloop[spore-loop] sporeloop --> draw[draw loop] draw --> myceliumgrowth[mycelium-growth] draw --> fooddisplay[food-display] draw --> flashupdate[flash-update] draw --> sporeupdate[spore-update] draw --> rippleupdate[ripple-update] draw --> packetupdate[packet-update] draw --> mousepressed[mousepressed] draw --> windowresized[windowresized] myceliumgrowth --> fooddetection[food-detection] myceliumgrowth --> fusiondetection[fusion-detection] myceliumgrowth --> growthapplication[growth-application] myceliumgrowth --> growframeselection[grow-frame-selection] myceliumgrowth --> tipselection[tip-selection] fooddetection --> foodattraction[food-attraction] fooddetection --> fusioncreation[fusion-creation] flashupdate --> fadeanimation[fade-animation] sporeupdate --> driftmovement[drift-movement] sporeupdate --> wrapping[wrapping] rippleupdate --> expansionfade[expansion-fade] packetupdate --> packetspawn[packet-spawn] packetupdate --> branchtraversal[branch-traversal] packetupdate --> forwardpathfinding[forward-pathfinding] packetupdate --> backwardpathfinding[backward-pathfinding] packetupdate --> trailsystem[trail-system] click setup href "#fn-setup" click draw href "#fn-draw" click canvasinit href "#sub-canvas-init" click myceliuminit href "#sub-mycelium-init" click sporeloop href "#sub-spore-loop" click myceliumgrowth href "#sub-mycelium-growth" click fooddisplay href "#sub-food-display" click flashupdate href "#sub-flash-update" click sporeupdate href "#sub-spore-update" click rippleupdate href "#sub-ripple-update" click packetupdate href "#sub-packet-update" click fooddetection href "#sub-food-detection" click fusiondetection href "#sub-fusion-detection" click growthapplication href "#sub-growth-application" click growframeselection href "#sub-grow-frame-selection" click tipselection href "#sub-tip-selection" click foodattraction href "#sub-food-attraction" click fusioncreation href "#sub-fusion-creation" click fadeanimation href "#sub-fade-animation" click driftmovement href "#sub-drift-movement" click wrapping href "#sub-wrapping" click expansionfade href "#sub-expansion-fade" click packetspawn href "#sub-packet-spawn" click branchtraversal href "#sub-branch-traversal" click forwardpathfinding href "#sub-forward-pathfinding" click backwardpathfinding href "#sub-backward-pathfinding" click trailsystem href "#sub-trail-system"

❓ Frequently Asked Questions

What visual experience does the Mycelium Network sketch provide?

The sketch creates a dynamic and glowing mycelium-like network that spreads across the screen, accompanied by drifting spores and pulsing ripples that respond to user interactions.

How can users interact with the Mycelium Network simulation?

Users can click on the canvas to plant food sources, which guide the organic growth of the mycelium towards them, enhancing the reactive visualization.

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

This sketch demonstrates techniques such as recursive branching for growth patterns, particle systems for spores, and interactive animations through user clicks.

Preview

Mycelium Network — Bioluminescent Growth Simulation - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Mycelium Network — Bioluminescent Growth Simulation - Code flow showing setup, draw, mousepressed, windowresized, branch, mycelium, foodsource, nutrientpacket, flash, spore, ripple
Code Flow Diagram