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!