top of page

Directed by Ronit Izraeli

PGraphics branchLayer;
ArrayList<FungalNode> nodes = new ArrayList<FungalNode>();
ArrayList<ArrayList<PVector>> myceliumBranches = new ArrayList<ArrayList<PVector>>();
ArrayList<PVector> treeEntryPoints = new ArrayList<PVector>();
ArrayList<TreeRoot> treeRoots = new ArrayList<TreeRoot>();
ArrayList<EstablishedPath> establishedPaths = new ArrayList<EstablishedPath>();
ArrayList<Signal> signals = new ArrayList<Signal>();

int maxNodes = 60;  // Reduced from 80 for better performance
float zoom = 1.0;
PVector panOffset = new PVector(0, 0);
boolean dragging = false;
PVector lastMouse = new PVector();

int lastPathCheck = 0;
int pathCheckInterval = 1000;
int lastSignalSpawn = 0;
int signalSpawnInterval = 2000;
int lastCheckedBranch = 0;

boolean showUI = false;

// Organic Color Palette
color bgColor = color(8, 12, 10);
color myceliumPrimary = color(220, 235, 210);
color myceliumGhost = color(200, 220, 190, 60);
color pathDormant = color(180, 170, 120, 100);
color pathActive = color(220, 255, 160, 180);
color pathPulse = color(200, 240, 140, 255);
color ancientPath = color(40, 80, 50, 200);
color signalBurst = color(255, 255, 200);
color signalTrail = color(180, 220, 160, 100);
color dataFlow = color(160, 255, 180, 150);
// Updated tree colors
color treeBrown = color(92, 67, 43);  // Rich brown for tree centers
color rootLightBrown = color(121, 85, 61);  // Light brown for roots
color rootGlow = color(140, 100, 70, 60);  // Warm brown glow
color bioGlow = color(120, 255, 180, 100);
// Food colors - purple coordinated with organic palette
color foodPrimary = color(150, 100, 180);  // Rich purple
color foodGlow = color(170, 120, 200, 80);  // Purple glow
color foodConsumed = color(120, 80, 150, 150);  // Darker when consumed

// Canvas dimensions - much larger than screen for exploration
float canvasWidth;
float canvasHeight;
class NutrientPatch {
  PVector pos;
  boolean isConsumed = false;
  boolean isActive = true;  // Add this line
  float radius = 15;  // Much smaller
  
  NutrientPatch(float x, float y) {
    pos = new PVector(x, y);
  }
  
  void update() {
    // Check if any node picks up this food
    if (!isConsumed) {
      for (FungalNode node : nodes) {
        if (PVector.dist(node.pos, pos) < radius && !node.hasFood) {
          node.hasFood = true;
          node.isFoodCarrier = true;
          isConsumed = true;
          break;
        }
      }
    }
  }
  
  void display() {
    if (!isConsumed) {
      noStroke();
      // Small purple food particle
      fill(red(foodGlow), green(foodGlow), blue(foodGlow), 100);
      ellipse(pos.x, pos.y, radius * 1.5, radius * 1.5);
      
      fill(red(foodPrimary), green(foodPrimary), blue(foodPrimary), 220);
      ellipse(pos.x, pos.y, radius, radius);
      
      // Bright center
      fill(red(foodPrimary) + 40, green(foodPrimary) + 30, blue(foodPrimary) + 40, 255);
      ellipse(pos.x, pos.y, radius * 0.5, radius * 0.5);
    }
  }
}

ArrayList<NutrientPatch> nutrientPatches = new ArrayList<NutrientPatch>();
ArrayList<ConnectionMemory> connectionMemories = new ArrayList<ConnectionMemory>();
float[][] memoryField;  // Heatmap of connection frequency
int fieldResolution = 50;  // Grid resolution for memory field

void setup() {
  fullScreen();
  background(bgColor);
  frameRate(24); // or even 24, film-style
  
  lastMouse = new PVector(0, 0);
  
  // Canvas dimensions - much larger than screen for exploration
  canvasWidth = width * 3;   // 3x screen width
  canvasHeight = height * 3;  // 3x screen height
  
  // Initialize memory field
  memoryField = new float[fieldResolution][fieldResolution];
  
  nodes.add(new FungalNode(canvasWidth/2, canvasHeight/2));
  
  // Start camera centered on node origin
  panOffset.x = -canvasWidth/2 + width/2;
  panOffset.y = -canvasHeight/2 + height/2;
  
  // Create tree entry points - 23 total trees spread across LARGE canvas
  treeEntryPoints.add(new PVector(canvasWidth * 0.05, canvasHeight * 0.1));
  treeEntryPoints.add(new PVector(canvasWidth * 0.95, canvasHeight * 0.9));
  treeEntryPoints.add(new PVector(canvasWidth * 0.8, canvasHeight * 0.05));
  treeEntryPoints.add(new PVector(canvasWidth * 0.02, canvasHeight * 0.7));
  treeEntryPoints.add(new PVector(canvasWidth * 0.98, canvasHeight * 0.3));
  treeEntryPoints.add(new PVector(canvasWidth * 0.15, canvasHeight * 0.95));
  treeEntryPoints.add(new PVector(canvasWidth * 0.85, canvasHeight * 0.02));
  treeEntryPoints.add(new PVector(canvasWidth * 0.03, canvasHeight * 0.03));
  treeEntryPoints.add(new PVector(canvasWidth * 0.97, canvasHeight * 0.97));
  treeEntryPoints.add(new PVector(canvasWidth * 0.5, canvasHeight * 0.98));
  treeEntryPoints.add(new PVector(canvasWidth * 0.01, canvasHeight * 0.85));
  treeEntryPoints.add(new PVector(canvasWidth * 0.99, canvasHeight * 0.15));
  treeEntryPoints.add(new PVector(canvasWidth * 0.45, canvasHeight * 0.01));
  treeEntryPoints.add(new PVector(canvasWidth * 0.25, canvasHeight * 0.65));
  treeEntryPoints.add(new PVector(canvasWidth * 0.75, canvasHeight * 0.35));
  treeEntryPoints.add(new PVector(canvasWidth * 0.35, canvasHeight * 0.25));
  treeEntryPoints.add(new PVector(canvasWidth * 0.65, canvasHeight * 0.75));
  treeEntryPoints.add(new PVector(canvasWidth * 0.08, canvasHeight * 0.45));
  treeEntryPoints.add(new PVector(canvasWidth * 0.92, canvasHeight * 0.55));
  treeEntryPoints.add(new PVector(canvasWidth * 0.55, canvasHeight * 0.8));
  treeEntryPoints.add(new PVector(canvasWidth * 0.4, canvasHeight * 0.2));
  treeEntryPoints.add(new PVector(canvasWidth * 0.12, canvasHeight * 0.92));
  treeEntryPoints.add(new PVector(canvasWidth * 0.88, canvasHeight * 0.12));
  
  branchLayer = createGraphics(width, height);
  branchLayer.beginDraw();
  branchLayer.background(0, 0); // Transparent
  branchLayer.endDraw();
  
  // Create root systems for each tree
  for (PVector treePoint : treeEntryPoints) {
    treeRoots.add(new TreeRoot(treePoint.x, treePoint.y));
  }
  
  // Create scattered micro food sources
  for (int i = 0; i < 50; i++) {  // More numerous, smaller food
    nutrientPatches.add(new NutrientPatch(
      random(canvasWidth * 0.1, canvasWidth * 0.9),
      random(canvasHeight * 0.1, canvasHeight * 0.9)
    ));
  }
  
  println("Setup complete - press 'h' to toggle UI");
}

void draw() {
  // Organic fade instead of hard clear
  fill(bgColor, 25);
  noStroke();
  rect(0, 0, width, height);
  
  image(branchLayer, 0, 0);
  pushMatrix();
  translate(width/2 + panOffset.x, height/2 + panOffset.y);
  scale(constrain(zoom, 0.1, 5.0));
  translate(-width/2, -height/2);

  // Comment out the memory field drawing - remove green fuzz
  // drawMemoryField();

  // Draw tree roots and mycelium
  for (TreeRoot root : treeRoots) {
    root.update();
    root.display();
  }

  drawMyceliumBranches();
  updateSignals();
  updateNodes();

  // Periodically check for connections - even less frequently
  if (millis() - lastPathCheck > 5000) {  // Every 5 seconds instead of 2
    checkForNewConnections();
  }
  
  // Periodically spawn signals - much less frequently  
  if (millis() - lastSignalSpawn > 6000) {  // Every 6 seconds instead of 3
    spawnPathSignals();
    lastSignalSpawn = millis();
  }
  
  // Update and display nutrient patches
  for (NutrientPatch patch : nutrientPatches) {
    patch.update();
    patch.display();
  }
  if (frameCount % 20 == 0) {
    updateMemoryField();
  }

  popMatrix();

  if (showUI) {
    displayInfo();
  }
}

void drawMyceliumBranches() {
  // First draw all regular mycelium
  for (int i = 0; i < myceliumBranches.size(); i++) {
    ArrayList<PVector> branch = myceliumBranches.get(i);
    
    if (branch == null || branch.size() < 2) continue;
    
    // Check if this branch is part of an established path
    boolean isEstablished = false;
    for (EstablishedPath path : establishedPaths) {
      if (path.branchIndex == i) {
        isEstablished = true;
        break;
      }
    }
    
    if (!isEstablished) {
      // Draw regular mycelium with organic variation
      drawOrganicBranch(branch, myceliumPrimary, 1.0);
    }
  }
  
  // Then draw established paths on top
  for (EstablishedPath path : establishedPaths) {
    path.display();
  }
}

void drawOrganicBranch(ArrayList<PVector> branch, color baseColor, float thickness) {
  // Level-of-Detail: thin distant trails for performance
  float camX = -panOffset.x / zoom;
  float camY = -panOffset.y / zoom;
  
  noFill();
  beginShape();
  for (int i = 0; i < branch.size(); i++) {
    PVector p = branch.get(i);
    if (p != null) {
      // Calculate distance from camera
      float distFromCam = dist(p.x, p.y, camX, camY);
      
      // Skip points based on distance (LOD)
      int skipFactor = 1;
      if (distFromCam > 800) skipFactor = 5;  // Far: every 5th point
      else if (distFromCam > 400) skipFactor = 3;  // Mid: every 3rd point
      
      if (i % skipFactor == 0) {
        float colorNoise = noise(p.x * 0.01, p.y * 0.01, frameCount * 0.0001);
        stroke(red(baseColor) + colorNoise * 10, 
               green(baseColor) + colorNoise * 8,
               blue(baseColor) + colorNoise * 5,
               alpha(baseColor));
        strokeWeight(thickness);
        vertex(p.x, p.y);
      }
    }
  }
  endShape();
  
  // Glow layer - also with LOD
  beginShape();
  for (int i = 0; i < branch.size(); i++) {
    PVector p = branch.get(i);
    if (p != null) {
      float distFromCam = dist(p.x, p.y, camX, camY);
      int skipFactor = (distFromCam > 600) ? 4 : 2;
      
      if (i % skipFactor == 0) {
        stroke(red(baseColor), green(baseColor), blue(baseColor), 30);
        strokeWeight(thickness * 3);
        vertex(p.x, p.y);
      }
    }
  }
  endShape();
}

void updateSignals() {
  for (int i = signals.size() - 1; i >= 0; i--) {
    Signal signal = signals.get(i);
    if (signal != null) {
      signal.update();
      signal.display();
      
      if (signal.isDead()) {
        signals.remove(i);
      }
    }
  }
}

void updateNodes() {
  ArrayList<FungalNode> newNodes = new ArrayList<FungalNode>();
  for (int i = nodes.size() - 1; i >= 0; i--) {
    FungalNode node = nodes.get(i);
    if (node != null) {
      node.attractToTrees(treeRoots);
      node.update();
      node.display();
      
      // Remove nodes after 3 minutes
      if (millis() - node.birthTime > 180000) {
        nodes.remove(i);
        continue;
      }
      
      if (nodes.size() < maxNodes) {
        FungalNode offspring = node.tryDuplicate();
        if (offspring != null) {
          newNodes.add(offspring);
        }
      }
    }
  }
  
  nodes.addAll(newNodes);
}

void checkForNewConnections() {
  // Only check every 2 seconds instead of every second
  if (millis() - lastPathCheck < 2000) return;
  
  // Check fewer branches per frame
  int branchesToCheck = min(2, myceliumBranches.size() - lastCheckedBranch);
  
  for (int i = 0; i < branchesToCheck; i++) {
    int branchIdx = lastCheckedBranch + i;
    if (branchIdx >= myceliumBranches.size()) break;
    
    ArrayList<PVector> branch = myceliumBranches.get(branchIdx);
    
    if (branch == null || branch.size() < 10) continue;
    
    boolean alreadyEstablished = false;
    for (EstablishedPath path : establishedPaths) {
      if (path.branchIndex == branchIdx) {
        alreadyEstablished = true;
        break;
      }
    }
    if (alreadyEstablished) continue;
    
    int connectedTree1 = -1;
    int connectedTree2 = -1;
    
    // Less frequent sampling for performance
    int step = max(1, branch.size() / 15);
    
    // FIXED: Check against treeEntryPoints instead of treeRoots
    for (int treeIdx = 0; treeIdx < treeEntryPoints.size(); treeIdx++) {
      PVector treePos = treeEntryPoints.get(treeIdx);
      boolean touchesTree = false;
      
      for (int j = 0; j < branch.size(); j += step) {
        PVector branchPoint = branch.get(j);
        if (branchPoint == null) continue;
        
        // Detection radius scaled by tree size
        if (PVector.dist(branchPoint, treePos) < 80 * treeRoots.get(treeIdx).treeSize) {
          touchesTree = true;
          break;
        }
      }
      
      if (touchesTree) {
        if (connectedTree1 == -1) {
          connectedTree1 = treeIdx;
        } else if (connectedTree2 == -1 && treeIdx != connectedTree1) {
          connectedTree2 = treeIdx;
          break;
        }
      }
    }
    
    if (connectedTree1 >= 0 && connectedTree2 >= 0) {
      EstablishedPath newPath = new EstablishedPath(connectedTree1, connectedTree2, branchIdx, branch);
      establishedPaths.add(newPath);
      
      // Add to memory system
      ConnectionMemory memory = new ConnectionMemory(
        treeEntryPoints.get(connectedTree1),
        treeEntryPoints.get(connectedTree2)
      );
      connectionMemories.add(memory);
      
      // Update memory field along the path
      for (PVector point : newPath.pathSegment) {
        addMemoryPoint(point.x, point.y, 5.0);
      }
      
      println("CONNECTION ESTABLISHED! Tree " + connectedTree1 + " <-> Tree " + connectedTree2 + " via branch " + branchIdx);
    }
  }
  
  lastCheckedBranch += branchesToCheck;
  if (lastCheckedBranch >= myceliumBranches.size()) {
    lastCheckedBranch = 0;
  }
  
  lastPathCheck = millis();
}

void spawnPathSignals() {
  for (int i = 0; i < establishedPaths.size(); i++) {
    EstablishedPath path = establishedPaths.get(i);

    if (path.pathSegment.size() > 10) {
      // Spawn info signals (yellow)
      if (random(1) < 0.15) {  // Reduced frequency
        signals.add(new PathSignal(path));
      }
      
      // Spawn food signals (purple) when food sources are consumed
      if (random(1) < 0.1) {  // Less frequent food transfer
        for (NutrientPatch patch : nutrientPatches) {
          if (patch.isConsumed && PVector.dist(patch.pos, path.pathSegment.get(0)) < 100) {
            signals.add(new FoodSignal(path));
            break;
          }
        }
      }

      // Set usageStrength based on avg memory along path
      float totalMemory = 0;
      for (PVector p : path.pathSegment) {
        int gx = int(p.x / canvasWidth * fieldResolution);
        int gy = int(p.y / canvasHeight * fieldResolution);
        if (gx >= 0 && gx < fieldResolution && gy >= 0 && gy < fieldResolution) {
          totalMemory += memoryField[gx][gy];
        }
      }
      float avgMemory = totalMemory / path.pathSegment.size();
      path.usageStrength = constrain(map(avgMemory, 0, 5.0, 1.0, 10.0), 1.0, 10.0);
    }
  }
}

void displayInfo() {
  fill(myceliumPrimary);
  textSize(12);
  text("Nodes: " + nodes.size() + "/" + maxNodes, 20, 30);
  text("Branches: " + myceliumBranches.size(), 20, 50);
  text("Trees: " + treeRoots.size(), 20, 70);
  text("Established Connections: " + establishedPaths.size(), 20, 90);
  text("Signals: " + signals.size(), 20, 110);
  text("Press 'h' to hide UI", 20, 130);
  text("Press 'r' to reset", 20, 150);
}

void keyPressed() {
  if (key == 'h' || key == 'H') {
    showUI = !showUI;
  }
  if (key == 'r' || key == 'R') {
    resetSimulation();
  }
}

void resetSimulation() {
  nodes.clear();
  myceliumBranches.clear();
  establishedPaths.clear();
  signals.clear();
  
  for (TreeRoot tree : treeRoots) {
    tree.rootBranches.clear();
  }
  
  nodes.add(new FungalNode(canvasWidth/2, canvasHeight/2));
}

void mouseWheel(MouseEvent event) {
  if (event != null) {
    float e = event.getCount();
    float newZoom = constrain(zoom - e * 0.1, 0.1, 5.0);
    
    // Calculate zoom toward mouse position
    float mouseWorldX = (mouseX - width/2 - panOffset.x) / zoom + width/2;
    float mouseWorldY = (mouseY - height/2 - panOffset.y) / zoom + height/2;
    
    panOffset.x += (mouseWorldX - width/2) * (zoom - newZoom);
    panOffset.y += (mouseWorldY - height/2) * (zoom - newZoom);
    
    zoom = newZoom;
  }
}

void mousePressed() {
  dragging = true;
  lastMouse.set(mouseX, mouseY);
}

void mouseDragged() {
  if (dragging) {
    panOffset.x += mouseX - lastMouse.x;
    panOffset.y += mouseY - lastMouse.y;
    lastMouse.set(mouseX, mouseY);
  }
}

void mouseReleased() {
  dragging = false;
}

class EstablishedPath {
  int tree1, tree2;
  int branchIndex;
  ArrayList<PVector> pathSegment;
  float pulsePhase = 0;
  float usageStrength = 1.0; // Track signal traffic

  EstablishedPath(int t1, int t2, int idx, ArrayList<PVector> fullBranch) {
    tree1 = t1;
    tree2 = t2;
    branchIndex = idx;

    pathSegment = new ArrayList<PVector>();
    PVector tree1Pos = treeEntryPoints.get(t1);
    PVector tree2Pos = treeEntryPoints.get(t2);

    int startIdx = -1;
    int endIdx = -1;

    for (int i = 0; i < fullBranch.size(); i++) {
      PVector p = fullBranch.get(i);
      if (p == null) continue;

      if (PVector.dist(p, tree1Pos) < 100 && startIdx == -1) {
        startIdx = i;
      }

      if (PVector.dist(p, tree2Pos) < 100) {
        endIdx = i;
      }
    }

    if (startIdx >= 0 && endIdx >= 0) {
      int from = min(startIdx, endIdx);
      int to = max(startIdx, endIdx);

      for (int i = from; i <= to && i < fullBranch.size(); i++) {
        if (fullBranch.get(i) != null) {
          pathSegment.add(fullBranch.get(i).copy());
        }
      }
    }
  }

  void display() {
    if (pathSegment.size() < 2) return;

    pulsePhase += 0.02;
    float pulse = sin(pulsePhase) * 0.3 + 0.7;

    usageStrength *= 0.9995; // slow decay
    drawLayeredPath(pathSegment, pulse);
    drawConnectionPoint(pathSegment.get(0));
    drawConnectionPoint(pathSegment.get(pathSegment.size() - 1));
  }

  void drawLayeredPath(ArrayList<PVector> path, float pulse) {
    float lineThickness = map(usageStrength, 1.0, 10.0, 2.0, 8.0);

    noFill();

    // Core path
    beginShape();
    for (PVector p : path) {
      stroke(red(pathPulse), green(pathPulse), blue(pathPulse), 200 * pulse);
      strokeWeight(lineThickness);
      vertex(p.x, p.y);
    }
    endShape();

    // Optional: faint outer layer
    beginShape();
    for (PVector p : path) {
      stroke(red(pathActive), green(pathActive), blue(pathActive), 40);
      strokeWeight(lineThickness * 1.8);
      vertex(p.x, p.y);
    }
    endShape();
  }

  void drawConnectionPoint(PVector pos) {
    // Faint pulsing dot only
    noStroke();
    fill(red(signalBurst), green(signalBurst), blue(signalBurst), 180);
    ellipse(pos.x, pos.y, 6, 6);
  }
}

void drawLayeredGlow(float x, float y, float size) {
  noStroke();
  // Outer aura
  fill(red(dataFlow), green(dataFlow), blue(dataFlow), 20);
  ellipse(x, y, size * 2, size * 2);
  
  // Mid glow
  fill(red(bioGlow), green(bioGlow), blue(bioGlow), 40);
  ellipse(x, y, size * 1.5, size * 1.5);
  
  // Inner bright
  fill(red(pathActive), green(pathActive), blue(pathActive), 80);
  ellipse(x, y, size, size);
  
  // Core
  fill(red(signalBurst), green(signalBurst), blue(signalBurst), 200);
  ellipse(x, y, size * 0.3, size * 0.3);
}

// Fungal Node Class
class FungalNode {
  PVector pos, velocity;
  float direction;
  int lastDuplicationTime;
  int myBranchIndex;
  int birthTime;
  boolean isExplorer = false;  // Long-distance explorer nodes
  boolean hasFood = false;     // Carrying food
  boolean isFoodCarrier = false; // Food transport node
  int treesDelivered = 0;      // Trees reached with food
  
  FungalNode(float x, float y) {
    pos = new PVector(x, y);
    velocity = new PVector(0, 0);
    direction = random(TWO_PI);
    lastDuplicationTime = millis();
    birthTime = millis();
    
    myBranchIndex = myceliumBranches.size();
    ArrayList<PVector> newBranch = new ArrayList<PVector>();
    newBranch.add(pos.copy());
    myceliumBranches.add(newBranch);
  }
  
  void attractToTrees(ArrayList<TreeRoot> roots) {
    PVector nearestRoot = null;
    float minDist = Float.MAX_VALUE;
    
    // Find nearest tree for long-distance migration - but prefer distant ones
    for (TreeRoot root : roots) {
      float d = PVector.dist(pos, root.center);
      if (d < minDist && d > 50) {  // Increased minimum distance to avoid clustering
        minDist = d;
        nearestRoot = root.center;
      }
    }
    
    // Attraction influenced by memory field
    PVector memoryAttraction = getMemoryGradient(pos.x, pos.y);
    
    // Long-distance landscape migration pull
    if (nearestRoot != null && minDist < 800) {  // Increased range even more
      PVector treeAttraction = PVector.sub(nearestRoot, pos);
      treeAttraction.normalize();
      
      // Weaker attraction when close, stronger when far - encourages exploration
      float attractionStrength = map(minDist, 50, 800, 0.02, 0.15);
      treeAttraction.mult(attractionStrength);
      
      // Blend tree attraction with memory field
      PVector combinedAttraction = PVector.add(treeAttraction, memoryAttraction);
      combinedAttraction.mult(0.2);  // Gentler influence
      
      direction = lerp(direction, combinedAttraction.heading(), 0.02);
    } else if (memoryAttraction.mag() > 0.01) {
      // Follow memory field even without nearby trees
      direction = lerp(direction, memoryAttraction.heading(), 0.015);
    }
    
    // Food carriers have special behavior
    if (isFoodCarrier && hasFood) {
      // Check if reached a tree
      for (int i = 0; i < treeRoots.size(); i++) {
        TreeRoot tree = treeRoots.get(i);
        if (PVector.dist(pos, tree.center) < tree.treeSize * 40) {
          hasFood = false;
          treesDelivered++;
          
          // If delivered to 2 trees, establish food network
          if (treesDelivered >= 2) {
            // Find or create established path for food network
            for (EstablishedPath path : establishedPaths) {
              if ((path.tree1 == i || path.tree2 == i) && random(1) < 0.3) {
                // Add food signal to this path occasionally
                signals.add(new FoodSignal(path));
              }
            }
          }
          break;
        }
      }
    }
    // Explorers have different behavior - less attracted to nearby patches/trees
    if (isExplorer) {
      // Explorers ignore nearby attractions and push toward distant targets
      direction += random(-PI/16, PI/16);  // Less random wandering
      return;  // Skip normal attraction logic
    }
    
    // Add nutrient patch attraction with colony behavior
    if (!isFoodCarrier) {  // Only non-food carriers attracted to patches
    NutrientPatch nearestPatch = null;
    float minPatchDist = Float.MAX_VALUE;
    for (NutrientPatch patch : nutrientPatches) {
      float d = PVector.dist(pos, patch.pos);
      
      // Strong attraction when in colony range
      if (d < patch.radius && patch.isActive) {
        nearestPatch = patch;
        minPatchDist = d;
        break;
      }
      // Weaker long-range attraction to active patches
      else if (d < 200 && patch.isActive && d < minPatchDist) {
        nearestPatch = patch;
        minPatchDist = d;
      }
    }
    
    if (nearestPatch != null) {
      PVector patchAttraction = PVector.sub(nearestPatch.pos, pos);
      patchAttraction.normalize();
      
      // Stronger pull when close (colony formation)
      float attractionStrength = (minPatchDist < nearestPatch.radius) ? 0.15 : 0.05;
      patchAttraction.mult(attractionStrength);
      
      direction = lerp(direction, patchAttraction.heading(), 0.04);
    }
    if (random(1) < 0.1) {  // Only check 10% of the time
      int nearbyNodes = 0;
      for (FungalNode other : nodes) {
        if (other != this && PVector.dist(pos, other.pos) < 100) {
          nearbyNodes++;
          if (nearbyNodes > 2) break;  // Early exit
        }
      }
      
      if (nearbyNodes > 2) {  // If crowded, add outward push
        PVector pushAway = PVector.sub(pos, new PVector(canvasWidth/2, canvasHeight/2));
        pushAway.normalize();
        pushAway.mult(0.05);
        direction = lerp(direction, pushAway.heading(), 0.03);
      }
    }
  }
  }
  
  void update() {
    direction += random(-PI/8, PI/8);
    velocity = PVector.fromAngle(direction);
    velocity.mult(1.5);
    
    pos.add(velocity);
    
    if (myBranchIndex >= 0 && myBranchIndex < myceliumBranches.size()) {
      ArrayList<PVector> myBranch = myceliumBranches.get(myBranchIndex);
      if (myBranch != null) {
        myBranch.add(pos.copy());
        
        // Keep infinite trails - no removal
        // Performance handled by LOD rendering below
      }
    }
  }
  
  FungalNode tryDuplicate() {
    int timeSinceLastDup = millis() - lastDuplicationTime;
    
      // Higher duplication chance near nutrient patches (colony growth)
      float nearestPatchDist = Float.MAX_VALUE;
      for (NutrientPatch patch : nutrientPatches) {
        nearestPatchDist = min(nearestPatchDist, PVector.dist(pos, patch.pos));
      }
      
      float baseChance = 0.4;
      if (nearestPatchDist < 60) baseChance = 0.7;  // Higher chance in colonies
      
      if (timeSinceLastDup > 6000 && random(1) < baseChance) {
      lastDuplicationTime = millis();
      
      FungalNode offspring = new FungalNode(pos.x, pos.y);
      offspring.direction = direction + random(-PI/2, PI/2);
      
      return offspring;
    }
    
    return null;
  }
  
  void display() {
    // Food carriers glow purple
    if (isFoodCarrier && hasFood) {
      // Purple glow for food carriers
      noStroke();
      fill(red(foodGlow), green(foodGlow), blue(foodGlow), 150);
      ellipse(pos.x, pos.y, 15, 15);
      
      fill(red(foodPrimary), green(foodPrimary), blue(foodPrimary), 200);
      ellipse(pos.x, pos.y, 8, 8);
      
      fill(255, 220, 255, 255);
      ellipse(pos.x, pos.y, 4, 4);
    } else {
      // Regular glowing node head
      drawLayeredGlow(pos.x, pos.y, 6);
    }
  }
}

// Tree Root Class
class TreeRoot {
  PVector center;
  ArrayList<ArrayList<PVector>> rootBranches;
  float treeSize;  // Variable size for each tree
  
  TreeRoot(float x, float y) {
    center = new PVector(x, y);
    rootBranches = new ArrayList<ArrayList<PVector>>();
    
    // Random tree size between 0.7 and 1.5 scale
    treeSize = random(0.7, 1.5);
  }
  
  void update() {
    // Static roots - only grow once to detection radius, then stop
    if (rootBranches.size() < 8) {  // Create 8 static root lines
      for (int i = rootBranches.size(); i < 8; i++) {
        ArrayList<PVector> staticRoot = new ArrayList<PVector>();
        float angle = TWO_PI / 8 * i;
        float maxDist = 20 * treeSize;
        
        // Create straight line from center to edge
        for (float d = 0; d <= maxDist; d += 5) {
          staticRoot.add(new PVector(
            center.x + cos(angle) * d,
            center.y + sin(angle) * d
          ));
        }
        rootBranches.add(staticRoot);
      }
    }
  }
  
  void display() {
    // Layered tree orb with multiple rings showing detection radius - scaled by treeSize
    noStroke();
    
    // Outermost detection radius ring (80px radius * treeSize)
    fill(red(treeBrown) + 40, green(treeBrown) + 30, blue(treeBrown) + 20, 20);
    ellipse(center.x, center.y, 40 * treeSize, 40 * treeSize);
    
    // Draw static root trails
    for (ArrayList<PVector> branch : rootBranches) {
      if (branch != null && branch.size() > 1) {
        drawOrganicBranch(branch, color(red(treeBrown) + 70, green(treeBrown) + 55, blue(treeBrown) + 35), 1.5);
      }
    }
  }
}

// Root Node Class
class RootNode {
  PVector pos, velocity;
  float direction;
  int lastDuplicationTime;
  int myBranchIndex;
  TreeRoot parentTree;
  float speed = 0.2;
  
  RootNode(float x, float y, TreeRoot parent) {
    pos = new PVector(x, y);
    velocity = new PVector(0, 0);
    direction = random(TWO_PI);
    lastDuplicationTime = millis();
    parentTree = parent;
    
    myBranchIndex = parentTree.rootBranches.size();
    ArrayList<PVector> newBranch = new ArrayList<PVector>();
    newBranch.add(pos.copy());
    parentTree.rootBranches.add(newBranch);
  }
  
  void update() {
    direction += random(-PI/8, PI/8);
    velocity = PVector.fromAngle(direction);
    velocity.mult(speed);
    
    float distFromTree = PVector.dist(pos, parentTree.center);
    if (distFromTree > 80 * parentTree.treeSize) {  // Stop at detection radius scaled by tree size
      // Stop the root node instead of pulling back
      velocity.mult(0);
      return;
    }
    
    pos.add(velocity);
    
    if (myBranchIndex >= 0 && myBranchIndex < parentTree.rootBranches.size()) {
      ArrayList<PVector> myBranch = parentTree.rootBranches.get(myBranchIndex);
      if (myBranch != null) {
        myBranch.add(pos.copy());
        
        if (myBranch.size() > 1000) {  // Reduced from 10000
          myBranch.remove(0);
        }
      }
    }
  }
  
  RootNode tryDuplicate() {
    int timeSinceLastDup = millis() - lastDuplicationTime;
    
    if (timeSinceLastDup > 8000 && random(1) < 0.2) {
      lastDuplicationTime = millis();
      
      RootNode offspring = new RootNode(pos.x, pos.y, parentTree);
      offspring.direction = direction + random(-PI/2, PI/2);
      
      return offspring;
    }
    
    return null;
  }
}

// Signal Base Class
class Signal {
  PVector pos, target;
  PVector velocity;
  float speed = 3.0;
  boolean dead = false;
  float life = 255;
  int birthTime;
  
  Signal(PVector start, PVector end) {
    if (start == null || end == null) {
      dead = true;
      return;
    }
    
    pos = start.copy();
    target = end.copy();
    
    velocity = PVector.sub(target, pos);
    if (velocity.mag() > 0) {
      velocity.normalize();
      velocity.mult(speed);
    } else {
      dead = true;
      return;
    }
    
    birthTime = millis();
  }
  
  void update() {
    if (dead) return;
    
    pos.add(velocity);
    life -= 0.5;
    
    if (PVector.dist(pos, target) < 5 || life <= 0 || millis() - birthTime > 10000) {
      dead = true;
    }
  }
  
  void display() {
    if (dead) return;
    
    // Multi-layer signal glow
    noStroke();
    fill(0,0,0, life * 0.3);
    ellipse(pos.x, pos.y, 16, 16);
    
    fill(red(dataFlow), green(dataFlow), blue(dataFlow), life * 0.6);
    ellipse(pos.x, pos.y, 10, 10);
    
   fill(255, 255, 150, life); // Soft yellow tone
    ellipse(pos.x, pos.y, 6, 6);
  }
  
  boolean isDead() {
    return dead;
  }
}

// Food Signal Class - travels along established paths
class FoodSignal extends Signal {
  EstablishedPath path;
  int currentIndex = 0;
  boolean forward = true;
  
  FoodSignal(EstablishedPath p) {
    super(p.pathSegment.get(0), p.pathSegment.get(p.pathSegment.size()-1));
    path = p;
    speed = 1.5;  // Slower than info signals
    
    if (random(1) > 0.5) {
      forward = false;
      currentIndex = path.pathSegment.size() - 1;
      pos = path.pathSegment.get(currentIndex).copy();
    }
  }
  
  void display() {
    if (dead) return;
    
    // Purple food signal
    noStroke();
    fill(red(foodPrimary), green(foodPrimary), blue(foodPrimary), life * 0.4);
    ellipse(pos.x, pos.y, 12, 12);
    
    fill(red(foodGlow), green(foodGlow), blue(foodGlow), life * 0.7);
    ellipse(pos.x, pos.y, 8, 8);
    
    fill(255, 200, 255, life); // Light purple core
    ellipse(pos.x, pos.y, 4, 4);
  }
  
  void update() {
    if (dead || path.pathSegment.size() < 2) return;
    
    // Same movement logic as PathSignal but slower
    if (forward) {
      if (currentIndex < path.pathSegment.size() - 1) {
        PVector next = path.pathSegment.get(currentIndex + 1);
        PVector dir = PVector.sub(next, pos);
        
        if (dir.mag() > speed) {
          dir.normalize();
          dir.mult(speed);
          pos.add(dir);
        } else {
          currentIndex++;
          pos = next.copy();
        }
      } else {
        dead = true;
      }
    } else {
      if (currentIndex > 0) {
        PVector next = path.pathSegment.get(currentIndex - 1);
        PVector dir = PVector.sub(next, pos);
        
        if (dir.mag() > speed) {
          dir.normalize();
          dir.mult(speed);
          pos.add(dir);
        } else {
          currentIndex--;
          pos = next.copy();
        }
      } else {
        dead = true;
      }
    }
    
    life -= 0.3;  // Slower decay than info signals
    if (life <= 0) dead = true;
  }
}
class PathSignal extends Signal {
  EstablishedPath path;
  int currentIndex = 0;
  boolean forward = true;
  
  PathSignal(EstablishedPath p) {
    super(p.pathSegment.get(0), p.pathSegment.get(p.pathSegment.size()-1));
    path = p;
    speed = 2.0;
    
    if (random(1) > 0.5) {
      forward = false;
      currentIndex = path.pathSegment.size() - 1;
      pos = path.pathSegment.get(currentIndex).copy();
    }
  }
  
  void update() {
    if (dead || path.pathSegment.size() < 2) return;
    
    if (forward) {
      if (currentIndex < path.pathSegment.size() - 1) {
        PVector next = path.pathSegment.get(currentIndex + 1);
        PVector dir = PVector.sub(next, pos);
        
        if (dir.mag() > speed) {
          dir.normalize();
          dir.mult(speed);
          pos.add(dir);
        } else {
          currentIndex++;
          pos = next.copy();
        }
      } else {
        dead = true;
      }
    } else {
      if (currentIndex > 0) {
        PVector next = path.pathSegment.get(currentIndex - 1);
        PVector dir = PVector.sub(next, pos);
        
        if (dir.mag() > speed) {
          dir.normalize();
          dir.mult(speed);
          pos.add(dir);
        } else {
          currentIndex--;
          pos = next.copy();
        }
      } else {
        dead = true;
      }
    }
    
    life -= 0.5;
    if (life <= 0) dead = true;
  }
}

// Memory System Classes and Functions
class ConnectionMemory {
  PVector point1, point2;
  float strength;
  int timestamp;
  
  ConnectionMemory(PVector p1, PVector p2) {
    point1 = p1.copy();
    point2 = p2.copy();
    strength = 1.0;
    timestamp = millis();
  }
  
  void decay() {
    strength *= 0.9995;  // Very slow decay
  }
}

void drawMemoryField() {
  // Draw the memory field as a subtle organic background layer
  float cellWidth = width / float(fieldResolution);
  float cellHeight = height / float(fieldResolution);
  
  // First pass: draw with gaussian blur effect
  for (int i = 0; i < fieldResolution; i++) {
    for (int j = 0; j < fieldResolution; j++) {
      float value = memoryField[i][j];
      if (value > 0.01) {
        // Sample neighboring cells for smoother rendering
        float avgValue = value;
        int samples = 1;
        
        // Average with neighbors
        for (int di = -1; di <= 1; di++) {
          for (int dj = -1; dj <= 1; dj++) {
            int ni = i + di;
            int nj = j + dj;
            if (ni >= 0 && ni < fieldResolution && nj >= 0 && nj < fieldResolution) {  // Larger influence radius for smoother patterns
  for (int i = -5; i <= 5; i++) {
    for (int j = -5; j <= 5; j++) {
      int gi = gridX + i;
      int gj = gridY + j;
      
      if (gi >= 0 && gi < fieldResolution && gj >= 0 && gj < fieldResolution) {
        float dist = sqrt(i*i + j*j);
        float influence = strength * exp(-dist * dist / 8.0);  // Wider gaussian
        memoryField[gi][gj] = min(memoryField[gi][gj] + influence * 0.3, 5.0);  // Lower cap
      }
    }
  }
}r

PVector getMemoryGradient(float x, float y) {
  // Calculate gradient of memory field at position
  int gridX = int(x / canvasWidth * fieldResolution);
  int gridY = int(y / canvasHeight * fieldResolution);
  
  if (gridX <= 0 || gridX >= fieldResolution-1 || 
      gridY <= 0 || gridY >= fieldResolution-1) {
    return new PVector(0, 0);
  }
  
  // Sample neighboring cells
  float left = memoryField[gridX-1][gridY];
  float right = memoryField[gridX+1][gridY];
  float up = memoryField[gridX][gridY-1];
  float down = memoryField[gridX][gridY+1];
  
  // Calculate gradient
  PVector gradient = new PVector(right - left, down - up);
  gradient.mult(0.1);  // Scale down influence
  
  return gradient;
}

INTERACTION LAB VISUAL COMMUNICATION UNIVERSITY OF HAIFA SCHOOL OF DESIGN

05_YOEL_ZAJDNER

INTERACTION LAB VISUAL COMMUNICATION UNIVERSITY OF HAIFA SCHOOL OF DESIGN

MRS

(The Mycelium Rosseta Stone) 

Exploration Pulse

Food Signal

Enviromental Distress

Enviromental Distress

I Love You

I Love You, In Fungi

Made in processing

Aided By Chat GPT <3

How Do Mushrooms Say "I LOVE YOU"?

0.12v → 300ms → 0.13v → 310ms → 0.14v → 320ms → 0.13v → 330ms → 0.12v

Mushrooms communicate through electrical pulses that travel through the mycelium – an underground network connecting them.

 

According to research, these patterns resemble the structure of a language – with electrical “words” and “sentences”

Real Mycelium

Simulated Mycelium

Other Visualization Aettempts

"The Hidden Life Of Trees"

Peter Wohllben

Mycelium Communication Research

Forest Mapping Atempts

Simulation Versions

Final Version//14.07.2025

Claude helped too!

bottom of page