GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Slime Mold Simulation, 9 Ways (No Library)

No library, no WebGL — just thousands of dots sniffing front, left, right and turning toward the strongest scent, and a slime-mold network grows that finds a

Auto-plays · click a tile to jump to its section · all nine in one zip

One Sensor Angle: Nine Steps from Coral to Blood Vessels

The nine scenes climb one axis: what each agent is allowed to smell. 01 reads only the trail field its own kind left behind, so the network shape comes from nothing but deposits and evaporation. 02 adds maze walls as a scent worth minus one thousand, and suddenly the same dots solve the passage real Physarum polycephalum solved for its 2008 Ig Nobel Prize. 03 and 04 both add food — 03 as six fixed radial points that sort the network into trunk lines and branches, 04 as signed anchors your pointer plants, food or salt by a toggle. 05 makes the smell itself subtractive, so each colony reads the other's trail as an obstacle and the canvas splits into two territories. 06 replaces the trail value with a bare timestamp per cell, 07 turns the sensor geometry into two live sliders, 08 stops time and keeps the corpse, and 09 stages the whole thing under documentary lighting. It is the same steering-without-a-map idea that flies a flock bound by three neighbor rules and no shared field at all — except here the field is the map the swarm writes for itself, which is what makes this a physarum simulation rather than a particle system.

Code per item

Each snippet below is lifted from that item's own script tag, constant names and all, not summarized into pseudocode — drop it into a file and it grows the same network the panel above it does.

01Basic three-sensor agent — one slime cell sniffing front, left, right and turning

Every agent samples the trail at three points ahead, turns toward the strongest, steps forward, and deposits at its new cell — no other rule runs. 1300 agents from a small center seed paint 51.385% of the frame, peaking at 34.913%, and the field wraps at every edge so nothing cuts off at the border.

Basic three-sensor agent — one slime cell sniffing front, left, right and turning
Open in a new tab

01 · Basic three-sensor agent — one slime cell sniffing front, left, right and turning — Measured area 51.385% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDataputImageDatarequestAnimationFrame

Three samples and one turn is the entire steering rule

function stepAgents() {
  for (var i = 0; i < AGENTS; i++) {
    var x = ax[i], y = ay[i], h = ah[i];
    var sf = sample(x + Math.cos(h) * SENSOR_DIST, y + Math.sin(h) * SENSOR_DIST);
    var sl = sample(x + Math.cos(h + SENSOR_ANGLE) * SENSOR_DIST, y + Math.sin(h + SENSOR_ANGLE) * SENSOR_DIST);
    var sr = sample(x + Math.cos(h - SENSOR_ANGLE) * SENSOR_DIST, y + Math.sin(h - SENSOR_ANGLE) * SENSOR_DIST);
    if (sf >= sl && sf >= sr) {
      // straight — the strongest cell is dead ahead
    } else if (sl > sr) {
      h += TURN;
    } else if (sr > sl) {
      h -= TURN;
    } else {
      h += (rand() < 0.5 ? 1 : -1) * TURN;
    }
    h += (rand() - 0.5) * 0.14;
    var nx = x + Math.cos(h) * STEP, ny = y + Math.sin(h) * STEP;
    nx = ((nx % fw) + fw) % fw; ny = ((ny % fh) + fh) % fh;
    ax[i] = nx; ay[i] = ny; ah[i] = h;
    var ii = idx(Math.round(nx), Math.round(ny));
    trail[ii] = Math.min(CAP, trail[ii] + DEPOSIT);
  }
  stepCount++;
}

02Maze shortest path — the Ig Nobel-winning experiment, rebuilt in canvas

A 13x7 maze is carved by randomized depth-first search, food sits at both ends of the middle row, and obstacle cells return a sensor value of -1000 so agents swing hard away from walls. Dead-end trails evaporate first — DECAY is 0.05, faster than item 01's 0.045 — and only the shortest passage stays thick, with the smallest peak frame of the nine at 2.37% because maze cells are narrow.

Maze shortest path — the Ig Nobel-winning experiment, rebuilt in canvas
Open in a new tab

02 · Maze shortest path — the Ig Nobel-winning experiment, rebuilt in canvas — Measured area 43.166% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDatarequestAnimationFrame

Obstacle cells read as minus one thousand

function sense(x, y) {
  var i = clampIdx(x, y);
  if (obstacle[i]) return -1000;
  return trail[i] + food[i] * FOOD_W * CAP;
}

function initAgents() {
  var open = [];
  for (var i = 0; i < obstacle.length; i++) if (!obstacle[i]) open.push(i);
  ax = new Float32Array(AGENTS); ay = new Float32Array(AGENTS); ah = new Float32Array(AGENTS);
  for (var k = 0; k < AGENTS; k++) {
    var oi = open[Math.floor(rand() * open.length)];
    ax[k] = oi % fw; ay[k] = Math.floor(oi / fw);
    ah[k] = rand() * Math.PI * 2;
  }
}

03Multi-food network — optimizing connections like a rail map

Six food points are scattered radially, and each one's scent is added to every sensor reading, so the network sorts itself into thick trunk routes and thin branch lines connecting every point. Evaporation is slowed to DECAY 0.038 so the branches survive long enough for the thickness contrast to show, painting 36.237% of the frame at an average intensity of 15.9.

Multi-food network — optimizing connections like a rail map
Open in a new tab

03 · Multi-food network — optimizing connections like a rail map — Measured area 36.237% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDatarequestAnimationFrame

Diffusion and evaporation run as two lines of arithmetic

function diffuseEvap() {
  for (var y = 0; y < fh; y++) {
    var ym = y === 0 ? fh - 1 : y - 1, yp = y === fh - 1 ? 0 : y + 1;
    var rowY = y * fw, rowYm = ym * fw, rowYp = yp * fw;
    for (var x = 0; x < fw; x++) {
      var xm = x === 0 ? fw - 1 : x - 1, xp = x === fw - 1 ? 0 : x + 1;
      var i = rowY + x;
      var avg = (trail[rowY + xm] + trail[rowY + xp] + trail[rowYm + x] + trail[rowYp + x]) * 0.25;
      trailB[i] = (trail[i] * (1 - DIFFUSE) + avg * DIFFUSE) * (1 - DECAY);
    }
  }
  var t = trail; trail = trailB; trailB = t;
}

04Pointer food or repellent — the network grows toward or flees your click

Clicking plants a signed anchor at that point — food pulls branches toward it, salt bends them away, and a toggle button flips which of the two the next click plants — and each anchor expires after 5.2 seconds. The sensor reading adds every anchor's sign x remaining life x exp(-distance²/30), and without pointer input a scripted timetable of five mixed anchors replays automatically, covering 41.776% of the frame.

Pointer food or repellent — the network grows toward or flees your click
Open in a new tab

04 · Pointer food or repellent — the network grows toward or flees your click — Measured area 41.776% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointermoveImageDatarequestAnimationFrame

Signed gaussian anchors expire on a timer

function anchorField(x, y) {
  var v = 0;
  for (var i = 0; i < anchors.length; i++) {
    var a = anchors[i];
    var age = curT - a.born;
    if (age < 0 || age > ANCHOR_LIFE) continue;
    var life = 1 - age / ANCHOR_LIFE;
    var dx = x - a.x, dy = y - a.y;
    var d2 = dx * dx + dy * dy;
    v += a.sign * life * Math.exp(-d2 / 30);
  }
  return v;
}

05Multi-species territory — two colored colonies splitting the canvas

Each species keeps its own independent trail field, and the sensor reading is own field - other field x REPEL_W with REPEL_W=1.7, so each colony treats the other's trail as an obstacle. Two colonies of 700 agents each start from the left and right thirds and push each other into sharply bordered territories — the widest coverage of the nine at 53.258% cumulative, 41.897% at the peak frame.

Multi-species territory — two colored colonies splitting the canvas
Open in a new tab

05 · Multi-species territory — two colored colonies splitting the canvas — Measured area 53.258% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDatarequestAnimationFrame

Own field minus other field draws the border

function senseFrom(own, other, x, y) {
  var i = clampIdx(x, y);
  return own[i] - other[i] * REPEL_W;
}

function initSpecies(count, cx0) {
  var ax = new Float32Array(count), ay = new Float32Array(count), ah = new Float32Array(count);
  var cx = fw * cx0, cy = fh / 2, r = Math.min(fw, fh) * 0.13;
  for (var i = 0; i < count; i++) {
    var a = rand() * Math.PI * 2, rr = rand() * r;
    ax[i] = cx + Math.cos(a) * rr;
    ay[i] = cy + Math.sin(a) * rr;
    ah[i] = rand() * Math.PI * 2;
  }
  return [ax, ay, ah];
}

06Trail-age coloring — the freshest paths glow neon-bright

No separate trail strength is accumulated at all — each cell stores only the step number it was last visited, and color comes from ageK = 1 - (currentStep - lastVisit) / 46. Freshly visited cells glow neon cyan while older ones fade to dim indigo, so only actively used trunks stay bright, and the average intensity of 76.0 is the highest of the nine by a wide margin.

Trail-age coloring — the freshest paths glow neon-bright
Open in a new tab

06 · Trail-age coloring — the freshest paths glow neon-bright — Measured area 35.206% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDataputImageDatarequestAnimationFrame

lastVisit is the only value the age field stores, and sense reads it too

function sense(x, y) {
  var i = clampIdx(x, y);
  // the age field doubles as the scent: a recently visited cell reads as a strong trail
  var lv = lastVisit[i];
  var ageK = lv < 0 ? 0 : Math.max(0, 1 - (stepCount - lv) / DECAY_STEPS);
  return ageK * 4 + food[i] * FOOD_W;
}

07Sensor-angle morph — one angle value turns coral into blood vessels

Two sliders — sensor spread angle from 10 to 90 degrees and sensor distance from 4 to 30 — feed straight into the steering math every frame, so the same rule draws dense coral, sparse vessels, or a grid-like city network. The automatic demo morphs both along sine waves, holding a 49.385% painted area with a 16.321% peak frame across the whole sweep.

Sensor-angle morph — one angle value turns coral into blood vessels
Open in a new tab

07 · Sensor-angle morph — one angle value turns coral into blood vessels — Measured area 49.385% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDatarequestAnimationFrame

Two sliders feed the same sense math live

function stepAgents() {
  var sa = sensorAngleDeg * Math.PI / 180;
  var sd = sensorDist / 3;
  for (var i = 0; i < AGENTS; i++) {
    var x = ax[i], y = ay[i], h = ah[i];
    var sf = sense(x + Math.cos(h) * sd, y + Math.sin(h) * sd);
    var sl = sense(x + Math.cos(h + sa) * sd, y + Math.sin(h + sa) * sd);
    var sr = sense(x + Math.cos(h - sa) * sd, y + Math.sin(h - sa) * sd);
    if (sf >= sl && sf >= sr) {
      // straight
    } else if (sl > sr) {
      h += TURN;
    } else if (sr > sl) {
      h -= TURN;
    } else {
      h += (rand() < 0.5 ? 1 : -1) * TURN;
    }
    h += (rand() - 0.5) * 0.14;
    var nx = x + Math.cos(h) * STEP, ny = y + Math.sin(h) * STEP;
    nx = ((nx % fw) + fw) % fw; ny = ((ny % fh) + fh) % fh;
    ax[i] = nx; ay[i] = ny; ah[i] = h;
    var ii = idx(Math.round(nx), Math.round(ny));
    trail[ii] = Math.min(CAP, trail[ii] + DEPOSIT);
  }
}

08Organic texture panel — capturing one frame as a card background

The network grows for only 130 steps, then the finished field is frozen into a static snapshot laid down at lowered opacity, and a fresh texture crossfades in over 0.7 seconds at every capture. The frame never sits dead even though each image is frozen — and the numbers say so honestly: 34.819% painted area and 15.5 average intensity, both the lowest of the nine, exactly as a faint static texture should measure.

Organic texture panel — capturing one frame as a card background
Open in a new tab

08 · Organic texture panel — capturing one frame as a card background — Measured area 34.819% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ImageDatarequestAnimationFrame

capture freezes the field and then clears it

function capture() {
  // freeze the whole field as the new texture, then clear it and start growing again
  var tmp = snapOld;
  snapOld = snapNew;
  snapNew = tmp;
  if (!snapNew) snapNew = new Float32Array(fw * fh);
  snapNew.set(trail);
  captureCount++;
  if (nOut) nOut.textContent = String(captureCount);
  for (var i = 0; i < trail.length; i++) trail[i] = 0;
  stepCount = 0;
  initAgents();
}

09Macro bioluminescent scene — a documentary-lit slime mold on a dark stage

A central light is baked once into a pixel buffer under a black stage, slow evaporation (DECAY 0.04) grows a network that reaches even into the dark edges of the frame, and any cell visited within the last 10 steps gets a pulsing tip glow driven by (1 - age/10) * (0.55 + 0.45 * sin(5t + 0.3x + 0.2y)). The trail tips keep writhing like a macro shot even after growth settles, at 54.7 average intensity with a 28.611% peak frame.

Macro bioluminescent scene — a documentary-lit slime mold on a dark stage
Open in a new tab

09 · Macro bioluminescent scene — a documentary-lit slime mold on a dark stage — Measured area 49.253% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

createRadialGradientImageDatarequestAnimationFrame

One radial gradient baked into lightBase pixels

// central light base built once with createRadialGradient, baked into lightBase pixels
function buildLightBase() {
  var off = document.createElement('canvas');
  off.width = fw; off.height = fh;
  var octx = off.getContext('2d');
  var cx = fw / 2, cy = fh / 2, r = Math.min(fw, fh) * LIGHT_RADIUS;
  var g = octx.createRadialGradient(cx, cy, 0, cx, cy, r);
  g.addColorStop(0, 'rgb(' + C_LIGHT[0] + ',' + C_LIGHT[1] + ',' + C_LIGHT[2] + ')');
  g.addColorStop(0.55, 'rgb(' + Math.round((C_LIGHT[0] + C_DARK[0]) / 2) + ',' + Math.round((C_LIGHT[1] + C_DARK[1]) / 2) + ',' + Math.round((C_LIGHT[2] + C_DARK[2]) / 2) + ')');
  g.addColorStop(1, 'rgb(' + C_DARK[0] + ',' + C_DARK[1] + ',' + C_DARK[2] + ')');
  octx.fillStyle = g;
  octx.fillRect(0, 0, fw, fh);
  var d = octx.getImageData(0, 0, fw, fh).data;
  lightBase = new Float32Array(fw * fh * 3);
  for (var i = 0, p = 0; i < d.length; i += 4, p += 3) {
    lightBase[p] = d[i]; lightBase[p + 1] = d[i + 1]; lightBase[p + 2] = d[i + 2];
  }
}

Steering straight at the goal instead of sensing trail is where stigmergy breaks

Steering toward the goal point is not stigmergy

Telling each agent the exit's coordinates and turning it that way solves the maze, but it is gradient descent on a map you drew — the entire claim of the 2008 experiment is that the organism holds no map at all. The sensor may read trail, food, obstacles, or the other colony's field, never the goal's direction. Break that and item 02 stops being a slime mold pathfinding demonstration and becomes an animated arrow.

Deposit without decay thickens everything equally

Shipping the steering and the deposit but skipping the (1 - DECAY) multiply leaves every trail that ever formed still on the field, so trunk and dead end sit at the same cap and the contrast that selects the shortest route never appears. Diffusion without evaporation is just as broken — it spreads the sludge evenly. The two operations must run together, every substep, which is why all nine items run their diffusion-and-evaporation pass between agent steps.

Walls you forget to sense stop being walls

If sense() returns the trail value on an obstacle cell instead of the -1000 sentinel, agents steer straight through maze walls and item 02 degenerates into item 01 with a wall texture behind it. The large negative value is what makes a wall read as the worst possible smell, so the turn-away logic does the rest. Forgetting it is the single fastest way to break the maze scene while every other number still looks healthy.

clearRect throws away the afterglow and the age

Clearing the canvas and redrawing from scratch each frame erases two things the measurements depend on: the evaporation afterglow that lets painted area climb past 34%, and item 06's per-cell age information, which lives in the rendered fade itself. Every item here writes one putImageData per frame and lets the field's own decay (or age) do the fading. That is how the nine items hold their 34.819% to 53.258% painted-area band with zero animation errors.

Why does slime mold converge on the shortest path? — trail reinforcement and evaporation

Deposits make the busy route thicker

Every agent that completes a step adds DEPOSIT to one cell, capped at CAP — so a cell on a route many agents walk fills to its cap almost instantly while a cell on a route nobody walks stays empty. Length matters too: a longer route spends more of its deposit spread over more cells, so per-cell concentration drops even at identical traffic. That is the reinforcement half of any slime mold simulation algorithm, and it needs no agent to know anything about the maze's geometry.

Evaporation is what deletes the wrong paths

The multiply by (1 - DECAY) every substep charges every cell the same rent, so a trail only survives where deposits arrive faster than decay collects. Detours and dead ends stop being re-walked once the food scent at the far end pulls traffic onto the shorter passage, their trails dry out, and the agents that still sample them find nothing worth turning for. This is the same single-value cooling that runs a heat field that fades by one multiply per frame — there it dims embers, here it prunes branches.

Diffusion widens a vein into a corridor

Blending each cell with its neighbors' average by DIFFUSE spreads a thin deposit line into a soft ridge wide enough for sensors 5 cells ahead to notice. Without it, trails stay one cell wide and the three-point sampling mostly misses them; with it, a trunk route becomes the brightest thing in every sensor's fan. It is the same neighbor-mixing that lets a grid where two chemicals react as they spread settle into patterns — except here the chemicals are replaced by agents that carry the reaction along with them.

The shortest path wins without anyone knowing it

Nobody stores the maze, runs a search, or compares route lengths — the shortest path is simply the route whose re-walk rate keeps it above the decay line after every other candidate has dried out. Convergence is an accounting outcome: reinforcement minus evaporation, evaluated cell by cell, thousands of times. All nine items run this one slime mold simulation algorithm with different things added to the sensor reading, which is why the same 24-frame check records 23 of 23 transitioning frames on every one of them.

Three variants that swap sensor angle and decay rate, tuned for reduced-motion

One constant, three different networks

Variant Changed value What it feels like
Sensor spread angle 07 sensor angle 22.5° → 45° The network grows sparser and spreads wider, coral into vessels
Evaporation rate 01 trail DECAY 0.02 → 0.06 Old branches vanish faster, leaving only the optimal route
Agent count 03 agents 4,000 → 12,000 The network densifies and converges noticeably sooner

Nothing here is drawn by hand, so widening one constant reshapes the entire network rather than asking anyone to redraw a frame.

Reduced motion still shows a network, not a blank panel

Every item checks matchMedia('(prefers-reduced-motion: reduce)') before the requestAnimationFrame loop is even started, and skips it entirely for a visitor who has asked for less motion. Instead of a blank canvas, the simulation is run ahead for a few hundred steps and one grown frame is drawn once, tagged with an .is-still class — a mid-growth network sits there, still legible, just not crawling.

One pointer-driven item, eight self-running panels

Item 04's anchors are planted by pointerdown, so they need a pointer — but the mode toggle is a real button, and the scripted anchor timetable runs on its own when no pointer input arrives at all. Nothing above is retyped or trimmed — hv25u6ur unlocks the same nine source files the snippets came from, HTML, CSS, and JS together, inside this page's zip.

Questions before you wire a slime mold network into a page

How is this different from an ant colony or astar pathfinding demo

An ant colony simulation shares the trail-following idea but usually lays pheromone between nest and food along fixed paths, while every item here grows the transport network itself from nothing — no nest, no preset edges. Against an A solver the difference is sharper: A matches a slime mold pathfinding algorithm only in outcome, because it holds the whole graph and scores nodes by design. Here no agent ever sees the graph; the shortest route survives as the only trail whose traffic outpaces evaporation.

Can I paste this code in and use it as-is

Yes — nothing in the nine items reaches out to a library, WebGL, or a CDN, so the canvas markup and script tag lift into any page and grow the same network they grow here. Each item is one self-contained HTML file with its own seeded generator, so the fixed seed means the preview regrows the exact same shape every load. That reproducibility is also why people post these as a slime mold simulation github snippet and the demo still matches the write-up.

Why does item 08 measure the lowest numbers of the nine

Item 08 records both the lowest painted area (34.819%) and the lowest average intensity (15.5) because it deliberately dims its frozen snapshot to texture opacity — a background panel, not a hero. Its peak frame of 5.666% is equally honest: a static image crossfading into the next static image cannot spike the way item 05's 41.897% territory push does. The 23-of-23 transitioning frames confirm the one thing it must not lose — the frame still never repeats its predecessor.

Sources: MDN — CanvasRenderingContext2D · MDN — ImageData · MDN — Window.requestAnimationFrame() · MDN — prefers-reduced-motion · Wikipedia — Physarum polycephalum · Wikipedia — Slime mold.

Rail-map infographics and a clickable hero — where these nine actually get used

A transit or logistics story reaches for item 03 first — a slime mold finding near-optimal connections on its own makes a network-optimization idea readable without the reader ever hearing the word algorithm. A hero area built for visitors who like to click reaches for item 04 instead: a network that grows toward or flees the pointer turns a passive diagram into something a reader steers.

A documentary-style intro or a science site's cover screen is where item 09 belongs — a single lit slime mold on a dark stage is already a finished shot, so a headline is the only thing left to add. All three ship as one self-contained file, so none of them need a bundler to sit inside a static section.

Enter the archive password

The password is inside this article. You will find it as you read.