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

Conway Game of Life in Canvas, 9 Patterns

Conway game of life: The grid looks random, but a live-neighbor value decides life or death, ages into fading color, and carries a signal along wires -- nine

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

01 runs the plain rule every other pattern here builds on or breaks from, before 02 drops the same idea to a single row and 03 fixes one small cluster in place as a repeating source; 04 and 05 swap the rule's output for an age value and a pointer value, 06 and 09 swap the rule table itself mid-run or per region, and 07 and 08 change what counts as a neighbor in the first place -- six hex sides instead of eight, or a four-state wire instead of two states. Read as a cellular automaton simulator, the nine differ only in which value the shared step-and-render loop is built around.

Where the No-wrap Edge Assumption Breaks Six of the Nine Grids

02's row strip wraps its edges on purpose -- (x - 1 + COLS) % COLS and (x + 1) % COLS pull the leftmost and rightmost neighbors from the opposite edge, so a pattern that drifts off one side reappears on the other. Carry that same modulo math into 01, 03, 04, 05, 06, or 08 and the fix breaks: those bound-check instead (if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue), so a cell at the border simply has fewer neighbors to count, and a glider or a signal head that reaches the edge stops or dies there rather than wrapping around. A second trap sits in GEN_INTERVAL_MS -- every pattern steps on an accumulator (acc += dt; while (acc >= GEN_INTERVAL_MS)), not once per requestAnimationFrame call, so a 144 Hz display doesn't quietly run generations 2.4x faster than a 60 Hz one the way a naive one-step-per-frame loop would.

How One Neighbor-count Value Builds Every Screen on This Page

Read step() in 01, 03, 04, 05, 06, or 08 and the shape repeats: for every cell, count the live neighbors in the surrounding ring, then decide the next state by comparing that count against two small numbers -- a survive range for a cell that's already alive, a single birth count for one that's dead -- and write the result into a second buffer before swapping it in. 02 keeps that same neighbor-then-compare shape but narrows the ring to three cells in one dimension and swaps the survive/birth comparison for an 8-bit lookup table; 07 keeps the 3x3 ring's spirit but resizes it to six hex-offset cells instead of eight square ones. 9 CSS Loading Animation Patterns, Canvas Physics's ninth pattern runs this exact survive/birth rule too, but only to compute one number every generation -- the share of lit cells -- and stops stepping the moment that share clears a progress target; none of the nine patterns on this page stop, because the generations themselves are the point, not a percentage they're counting toward.

Nine Conway Game of Life Patterns Compared by Moved Area, Intensity, and Frame Count

Every pattern recomputes its full grid on its own accumulator-driven interval and repaints every live cell with fillRect or a hex path, so the area and intensity below track how much of the 480x300 canvas actually changes paint, not how fast the JavaScript runs.

Pattern Cumulative moved area Average intensity Moved frames
01 Neighbor-count life-or-death 51.9% 144.5 23/23
02 Elementary rule strip 53.9% 47.8 23/23
03 Glider-gun scene 27.6% 47.6 23/23
04 Color-age trail 55.7% 32.8 23/23
05 Pointer seed draw 17.0% 146.7 23/23
06 Rule-variant lab 60.3% 55.1 23/23
07 Hex-grid automaton 49.5% 55.0 23/23
08 Wireworld circuit 26.7% 136.8 23/23
09 Hero scene 62.4% 148.5 23/23

01Grid Automaton Whose Life-or-Death Follows a Live-Neighbor Count

Each cell counts its eight live neighbors, compares that count against SURVIVE_MIN/SURVIVE_MAX if it's already alive or against BIRTH_COUNT if it's dead, and the whole 48x30 grid is recomputed into a second buffer before the two swap -- a broad, evenly spread rewrite that's part of why patterns like this one average past half the canvas.

Grid Automaton Whose Life-or-Death Follows a Live-Neighbor Count
Open in a new tab

01 · Grid Automaton Whose Life-or-Death Follows a Live-Neighbor Count — Measured area 51.899% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

SURVIVE_MINBIRTH_COUNTfillRect
  function step(){
    for (let y = 0; y < ROWS; y++){
      for (let x = 0; x < COLS; x++){
        const alive = grid[idx(x, y)] === 1;
        const n = neighborCount(x, y);
        let willLive = 0;
        if (alive){
          willLive = (n >= SURVIVE_MIN && n <= SURVIVE_MAX) ? 1 : 0;
        } else {
          willLive = (n === BIRTH_COUNT) ? 1 : 0;
        }
        next[idx(x, y)] = willLive;
      }
    }
    const tmp = grid; grid = next; next = tmp;
    generation++;
  }

02Strip Automaton Whose Next Row Pattern Follows a Single Rule-Number Value

RULE is unpacked once into an 8-bit lookup table, one entry per possible left-self-right combination, and every generation the whole buffer shifts up a row with copyWithin before the bottom row is recomputed from that table -- because there's no up-down neighbor in a strip like this, one row only ever depends on the row directly above it.

Strip Automaton Whose Next Row Pattern Follows a Single Rule-Number Value
Open in a new tab

02 · Strip Automaton Whose Next Row Pattern Follows a Single Rule-Number Value — Measured area 53.91% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

RULEcopyWithinfillRect
  let grid = new Uint8Array(COLS * ROWS);

  function idx(x, y){ return y * COLS + x; }

  function clearGrid(){
    grid.fill(0);
  }

  function seedRow(seedX){
    grid[idx(seedX, ROWS - 1)] = 1;
  }

  function step(){
    grid.copyWithin(0, COLS, ROWS * COLS);
    const base = (ROWS - 2) * COLS;
    const outBase = (ROWS - 1) * COLS;
    for (let x = 0; x < COLS; x++){
      const l = grid[base + ((x - 1 + COLS) % COLS)];
      const m = grid[base + x];
      const r = grid[base + ((x + 1) % COLS)];
      const pattern = l * 4 + m * 2 + r;
      grid[outBase + x] = ruleBits[pattern];
    }
  }

03Scene Automaton Where a Fixed Emitter Fires a Traveling Shape at a Fixed Interval

GUN_ROWS places three mirrored copies of a 36-cell conway game of life glider gun cluster at fixed rows, and because that cluster's own cells satisfy the same survive/birth rule as everything else on the grid, it regenerates itself every cycle and keeps pushing out a small diagonal shape that holds its form until it exits the canvas -- the lowest average area of the nine (27.6%) because most of the frame stays empty between the emitter and the drifting shapes it fires.

Scene Automaton Where a Fixed Emitter Fires a Traveling Shape at a Fixed Interval
Open in a new tab

03 · Scene Automaton Where a Fixed Emitter Fires a Traveling Shape at a Fixed Interval — Measured area 27.599% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

GUN_ROWSplaceGunseedGrid
  function placeGun(ox, oy, mirror){
    for (let i = 0; i < GUN_CELLS.length; i++){
      const dx = GUN_CELLS[i][0], dy = GUN_CELLS[i][1];
      const x = ox + (mirror ? (GUN_W - 1 - dx) : dx);
      const y = oy + dy;
      if (x >= 0 && x < COLS && y >= 0 && y < ROWS) grid[idx(x, y)] = 1;
    }
  }

  const GUN_ROWS = [4, 18, 32];

  function seedGrid(offset){
    grid.fill(0);
    for (let i = 0; i < GUN_ROWS.length; i++){
      placeGun(2 + offset, GUN_ROWS[i], false);
      placeGun(COLS - 2 - (GUN_W - 1) - offset, GUN_ROWS[i], true);
    }
  }

04Automaton Whose Color Fades by a Generation-Age Value

A separate age buffer climbs toward AGE_CAP while a cell stays alive and falls by TRAIL_DECAY once it dies, and rendering reads that age back into a color lerp between a low and a high trail color -- so a cell that just died still paints a visible, fading rectangle for a few generations after it has already flipped to dead.

Automaton Whose Color Fades by a Generation-Age Value
Open in a new tab

04 · Automaton Whose Color Fades by a Generation-Age Value — Measured area 55.72% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

TRAIL_DECAYAGE_CAPTRAIL_HIGH
  function step(){
    for (let y = 0; y < ROWS; y++){
      for (let x = 0; x < COLS; x++){
        const i = idx(x, y);
        const wasAlive = alive[i] === 1;
        const n = neighborCount(x, y);
        let willLive = 0;
        if (wasAlive){
          willLive = (n >= SURVIVE_MIN && n <= SURVIVE_MAX) ? 1 : 0;
        } else {
          willLive = (n === BIRTH_COUNT) ? 1 : 0;
        }
        nextAlive[i] = willLive;
      }
    }
    for (let i = 0; i < alive.length; i++){
      if (nextAlive[i]){
        age[i] = Math.min(AGE_CAP, age[i] + 1);
      } else {
        age[i] = Math.max(0, age[i] - TRAIL_DECAY);
      }
    }
    const tmp = alive; alive = nextAlive; nextAlive = tmp;
  }

05Drawing Automaton Where the Pointer Coordinate Value Toggles a Cell Directly

A pointer's client coordinates are converted into a grid cell on every move while a button is down, and that cell is force-set alive and rendered immediately -- generations stay frozen the whole time a pointer is down, so a drawn shape sits still on screen until release hands control back to the same survive/birth loop every other pattern here uses.

Drawing Automaton Where the Pointer Coordinate Value Toggles a Cell Directly
Open in a new tab

05 · Drawing Automaton Where the Pointer Coordinate Value Toggles a Cell Directly — Measured area 16.988% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

autoSeedpointermoveSURVIVE_MIN
  function onPointerDown(e){
    human();
    isDrawing = true;
    paused = true;
    updateAria();
    paintAt(e);
  }

  function onPointerMove(e){
    if (!isDrawing) return;
    paintAt(e);
  }

  function onPointerUp(){
    if (!isDrawing) return;
    isDrawing = false;
    paused = false;
    updateAria();
    render();
  }

06Rule-Lab Automaton Whose Pattern Changes With a Swapped Rule-Table Value

RULESETS holds four complete survive/birth tables, and every CYCLE_GENERATIONS steps the whole table is swapped for the next one while seedSame() restores the exact starting layout from a saved snapshot -- so the same initial fill runs through four different rule tables in turn and ends up looking nothing alike by the fourth pass.

Rule-Lab Automaton Whose Pattern Changes With a Swapped Rule-Table Value
Open in a new tab

06 · Rule-Lab Automaton Whose Pattern Changes With a Swapped Rule-Table Value — Measured area 60.251% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

RULESETSCYCLE_GENERATIONSbaseSeedSnapshot
  function step(){
    const rule = RULESETS[ruleIndex];
    for (let y = 0; y < ROWS; y++){
      for (let x = 0; x < COLS; x++){
        const alive = grid[idx(x, y)] === 1;
        const n = neighborCount(x, y);
        const willLive = alive ? rule.survive.indexOf(n) !== -1 : rule.born.indexOf(n) !== -1;
        next[idx(x, y)] = willLive ? 1 : 0;
      }
    }
    const tmp = grid; grid = next; next = tmp;
    genCount++;
    if (genCount >= CYCLE_GENERATIONS){
      genCount = 0;
      ruleIndex = (ruleIndex + 1) % RULESETS.length;
      seedSame();
    }
  }

07Honeycomb-Grid Automaton Whose State Splits on a Six-Neighbor Count Value

Every cell only ever has six neighbors here instead of eight, but which six depends on whether its own column is even or odd -- OFF_EVEN and OFF_ODD list two different offset tables for that reason, and the neighbor count picks one with a single parity check before summing the usual way, so a pattern spreads through the honeycomb along directions a square grid's eight-neighbor rule can't reach.

Honeycomb-Grid Automaton Whose State Splits on a Six-Neighbor Count Value
Open in a new tab

07 · Honeycomb-Grid Automaton Whose State Splits on a Six-Neighbor Count Value — Measured area 49.486% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

OFF_EVENOFF_ODDhexPath
  // Odd-q offset hex neighbors -- six per cell, and which six depends on whether the column is even or odd.
  const OFF_EVEN = [[-1,-1],[-1,0],[0,-1],[0,1],[1,-1],[1,0]];
  const OFF_ODD  = [[-1,0],[-1,1],[0,-1],[0,1],[1,0],[1,1]];

  function seedGrid(){
    for (let i = 0; i < grid.length; i++) grid[i] = rnd() < 0.33 ? 1 : 0;
  }

  function neighborCount(x, y){
    const offs = (x & 1) ? OFF_ODD : OFF_EVEN;
    let n = 0;
    for (let i = 0; i < 6; i++){
      const nx = x + offs[i][0], ny = y + offs[i][1];
      if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
      n += grid[idx(nx, ny)];
    }
    return n;
  }

08Signal-Circuit Automaton Where a Four-State Value Travels Along Drawn Wires

Every cell holds one of four values -- empty, wire, signal head, or signal tail -- and a wire cell only flips to a head when its live-head neighbor count lands between HEAD_NEIGHBORS_MIN and HEAD_NEIGHBORS_MAX; a head always decays to a tail and a tail always decays back to wire the very next generation, so a signal can only travel one way along whatever zigzag path the circuit was built with.

Signal-Circuit Automaton Where a Four-State Value Travels Along Drawn Wires
Open in a new tab

08 · Signal-Circuit Automaton Where a Four-State Value Travels Along Drawn Wires — Measured area 26.672% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

HEAD_NEIGHBORS_MAXbuildCircuitneighborHeadCount
  function step(){
    for (let y = 0; y < ROWS; y++){
      for (let x = 0; x < COLS; x++){
        const i = idx(x, y);
        const s = grid[i];
        let ns = s;
        if (s === WIRE){
          const hc = neighborHeadCount(x, y);
          if (hc >= HEAD_NEIGHBORS_MIN && hc <= HEAD_NEIGHBORS_MAX) ns = HEAD;
        } else if (s === HEAD){
          ns = TAIL;
        } else if (s === TAIL){
          ns = WIRE;
        }
        next[i] = ns;
      }
    }
    const tmp = grid; grid = next; next = tmp;
  }

09Hero-Scene Automaton Where Several Rule Values Run Side by Side in One Frame

QUADS splits the canvas into four regions and RULESETS assigns each one its own survive/birth table, so the neighbor count for a cell clamps its search to that region's own bounds and never lets a neighbor from another quadrant leak across the boundary -- four different rule tables run at once on one canvas, which is also part of why 09 posts the highest average area and intensity of the nine.

Hero-Scene Automaton Where Several Rule Values Run Side by Side in One Frame
Open in a new tab

09 · Hero-Scene Automaton Where Several Rule Values Run Side by Side in One Frame — Measured area 62.378% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

RULESETSQUADSneighborCountInQuad
  function step(){
    for (let qi = 0; qi < 4; qi++){
      const q = QUADS[qi];
      const rule = RULESETS[qi];
      for (let y = q.y0; y <= q.y1; y++){
        for (let x = q.x0; x <= q.x1; x++){
          const alive = grid[idx(x, y)] === 1;
          const n = neighborCountInQuad(x, y, q);
          const willLive = alive ? rule.survive.indexOf(n) !== -1 : rule.born.indexOf(n) !== -1;
          next[idx(x, y)] = willLive ? 1 : 0;
        }
      }
    }
    const tmp = grid; grid = next; next = tmp;
  }

What Changes When the Survival Floor, the Decay Speed, or the Signal Range Moves

All three variants below change exactly one constant and nothing else -- same grid, same seeded layout, same event handlers -- so whatever looks different comes entirely from that one number.

Variant Changed value What it feels like
Adjust the live-neighbor survival threshold value 01 SURVIVE_MIN 2 → 1 Lowering the survival minimum from 2 to 1 lets a cell live on with just one live neighbor, so cells persist far longer than the base pattern and the grid tends to lock into a dense fill instead of thinning out.
Adjust the age-color decay speed value 04 TRAIL_DECAY 1 → 2 Raising the decay value from 1 to 2 halves a dead cell's age value every generation, so the trail fades and disappears in about half the length it takes in the base pattern.
Adjust the signal-head neighbor-count allowed range value 08 HEAD_NEIGHBORS_MAX 2 → 1 Narrowing the allowed maximum from 2 to 1 means a wire only becomes a head when exactly one neighbor is already a head, so at a fork where two signals meet, the condition fails more often and the signal breaks instead of continuing.

What prefers-reduced-motion Removes From All Nine Patterns -- an Accessibility Note

Every tile checks window.matchMedia('(prefers-reduced-motion: reduce)') once on load -- MDN's prefers-reduced-motion reference covers exactly this media query. When it matches, the free-running requestAnimationFrame loop never starts at all, and the first click, Enter, or Space press computes exactly one step and renders once -- the grid still advances a single generation, but nothing loops or ticks on its own afterward. Every tile also keeps a live aria-label that swaps between a running and a paused description on every update, so a keyboard user tabbing to a tile (tabindex="0", role="button") always knows its state without needing to watch the canvas. Listeners stay the same across eight of the nine -- click, keydown, and resize on the tile, plus R inside that same keydown handler to reseed -- and only 05 adds pointerdown, pointermove, and pointerup for its drawing mode. Every step() shown above, plus the SCSS and React version of all nine, ships in one zip that opens with p9w3yqnr, the same password across every pattern on this page.

Where a Generative Background, a Circuit Diagram, and a Repeating Loop Actually Use This

09's four rule tables running at once read well as a generative background behind a hero section or an empty-state illustration -- no two visits look quite the same, but nothing ever needs a designer to hand-place shapes, because the seeded layout and the rule tables do that work every load. 08's wire-and-signal automaton is closer to an explainer diagram than a background: a fixed circuit shape with a signal head crawling along it communicates "this connects to that" in a way static arrows or a looping video can't, without shipping a video file.

03's glider gun earns its keep in any spot that wants a small repeating loop rather than a generative field -- a section divider or a corner accent that fires a shape outward on a steady interval reads as alive without redrawing the whole canvas every frame the way 09's hero scene does.

How This Differs From a Reaction-diffusion Grid and a Falling-sand Grid

9 Reaction Diffusion Simulations, No Library tracks two continuous concentration values per cell and updates both with a diffusion-and-reaction formula every frame, so its patterns bleed and blur between neighboring cells the way ink spreads through water. Every cell state on this page is a small integer instead -- 0 or 1, or one of four wire states -- decided by counting neighbors and checking a fixed table, so nothing here ever blends; a cell is alive or it isn't, on or off, wire or head, with no value in between.

Falling Sand Simulator, 9 Ways (No Library) moves material downward under a gravity rule and swaps cells with the ones below or beside them, so a single grain's position changes from frame to frame. None of these nine patterns ever move a cell's position -- a cell's x, y never changes, only whether it's counted as alive, and the sense of motion comes entirely from which fixed cells switch on or off next.

HTML5 Canvas Animation: 9 Warp & Droplet Effects reads and writes individual pixels, so its refraction and lensing math can bend a value that sits between two grid cells. These nine never touch a pixel directly; the smallest unit here is a whole cell, and fillRect or a hex path is the only thing that ever reaches the canvas.

Questions Worth Answering Before These Grids Run in Your Own Project

Can I reuse the step() logic as game of life javascript without canvas?

Yes -- every step function above only reads and writes plain Uint8Array buffers; rendering is the only part of each pattern that touches the canvas context, so the rule logic itself has no canvas dependency and drops into a Node script, a test file, or a different renderer unchanged.

Why does item 07 count six neighbors instead of eight?

Item 07 lays cells out as offset hexagons rather than squares, and a hexagon only ever touches six others edge-to-edge, so its offset tables list six neighbor positions each instead of the eight a square grid checks.

Does the glider gun in item 03 ever stop firing?

No -- the emitter cluster is built from ordinary cells that live or die under the grid's normal rule table, so it rebuilds itself automatically after every generation and keeps firing for as long as the tile keeps stepping.

Enter the archive password

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