webdevpuneet.com

Canvas Conway's Game of Life — Free Cellular Automaton Snippet

Canvas Conway's Game of Life · Visualizers · Plain HTML, CSS & JS · Live preview

CategoryVisualizers

Copy & paste

Source Code

<div class="gl-wrap">
  <canvas id="glCanvas" class="gl-canvas"></canvas>
  <div class="gl-bar">
    <button class="gl-btn" id="glPlay">Play</button>
    <button class="gl-btn" id="glStep">Step</button>
    <button class="gl-btn" id="glRandom">Random</button>
    <button class="gl-btn" id="glClear">Clear</button>
    <label class="gl-speed">Speed
      <input type="range" id="glSpeed" min="1" max="30" value="10">
    </label>
  </div>
  <p class="gl-hint">Click cells to toggle them while paused. <span id="glGen">Generation 0</span></p>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Typed array grid
Uint8Array stores the whole grid compactly.
Wrapping edges
The grid behaves as a torus, avoiding boundary bias.
Snapshot-based rules
Next generation computed from a frozen prior state.
Play/pause/step controls
Full manual and automatic simulation control.
Adjustable tick rate
A slider decouples simulation speed from frame rate.
Click-to-edit
Toggle individual cells while paused.
Random seeding
One click reseeds the grid at ~28% density.
Generation counter
Live label tracks how many generations have run.

About this UI Snippet

Canvas Conway's Game of Life — A Playable Cellular Automaton

Screenshot of the Canvas Conway's Game of Life snippet rendered live

Conway's Game of Life is the classic cellular automaton: a grid of cells that are either alive or dead, evolving generation by generation according to four simple rules based purely on each cell's living neighbor count. This snippet implements it fully on the Canvas 2D API, with a typed-array grid, play/pause/step/random controls, and an adjustable simulation speed.

A Uint8Array grid, not a 2D array of booleans

The grid is stored as a single flat Uint8Array(COLS * ROWS), indexed with a helper idx(x, y) => y * COLS + x. A typed array of 1s and 0s is both faster to allocate and iterate than a nested array of arrays, and it maps cleanly onto the two states a Life cell can have — no need for a richer data structure for a strictly binary grid.

Neighbor counting wraps at the edges

countNeighbors checks all eight surrounding cells, but wraps out-of-bounds coordinates back around with (x + dx + COLS) % COLS — so the grid behaves like a torus, where the right edge is a neighbor of the left edge and the bottom wraps to the top. This is the standard way to avoid dead zones at the grid boundary that would otherwise behave differently from interior cells.

Rules apply to a snapshot, not in place

step() is the heart of the simulation: it builds a brand new next array and computes every cell's next state purely from the *current* grid, following Conway's four rules — a live cell with 2 or 3 neighbors survives, a dead cell with exactly 3 neighbors becomes alive, everything else dies or stays dead. Applying the rules to a fresh buffer (rather than mutating grid cell by cell) is essential: mutating in place would let cells computed earlier in the loop affect the neighbor counts of cells computed later in the same generation, corrupting the simulation.

A real play/pause loop with adjustable tick rate

loop(ts) runs every frame via requestAnimationFrame, but only calls step() when enough time has passed based on the speed slider (1000 / speed ms between generations) — decoupling the simulation's tick rate from the display's refresh rate, so speed changes take effect immediately without restarting the loop. Pausing simply stops the interval check from ever triggering step, while clicking a cell is only allowed while paused, matching the classic editable-then-run interaction.

Customizing it

Change COLS/ROWS for a finer or coarser grid, tweak the random seed density in randomize, or seed a specific classic pattern (glider, blinker) programmatically instead of randomizing. Pair it with tic tac toe game or whack a mole game for more canvas-and-grid-based casual games.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA grid canvas seeds randomly and controls render.
  2. 2
    Click PlayGenerations advance automatically at the set speed.
  3. 3
    Click PauseThe simulation freezes on the current generation.
  4. 4
    Click cells while pausedToggle individual cells alive or dead.
  5. 5
    Click StepAdvance exactly one generation at a time.
  6. 6
    Click Random or ClearReseed the grid randomly or wipe it empty.
  7. 7
    Drag the speed sliderChange how many generations run per second.

Real-world uses

Common Use Cases

Casual browser games
Place alongside a tic tac toe game and a whack-a-mole game in a games section, with play, pause, step and speed controls.
Computer science education
Teach cellular automata and emergent behaviour, with the next generation computed from a frozen snapshot of the previous one.
Portfolio technical pieces
Show command of canvas and simulation in a portfolio piece, storing the whole grid compactly in a Uint8Array for speed.
Screensaver-style backgrounds
Run it passively behind other content, with wrapping edges making the grid behave as a torus and avoiding boundary bias.
Interview practice and generative art
Study a well-known algorithm, or seed patterns and watch what they grow into, using a random seed button for new starting points.
Related: Blackjack Card Game vs Dealer
See the Blackjack Card Game vs Dealer for a related games pattern worth pairing with this one.
Related: Tower of Hanoi Game
See the Tower of Hanoi Game for a related games pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Any live cell with two or three live neighbors survives to the next generation; any dead cell with exactly three live neighbors becomes alive; every other live cell dies (from under- or overpopulation) and every other dead cell stays dead. All four outcomes are derived purely from each cell's eight-neighbor count, applied simultaneously to the whole grid.

Because every cell's next state depends on the current state of its neighbors, mutating cells one at a time in the same array would mean cells processed later in the loop see already-updated neighbors instead of the previous generation's values, corrupting the simulation. Building a separate next array from an untouched snapshot of grid guarantees every cell's rule is evaluated against the same consistent prior generation.

Without wrapping, cells at the grid's border would have fewer possible neighbors than interior cells, causing edge behavior to diverge from the mathematically "correct" infinite-plane Game of Life. Wrapping x and y coordinates with the modulo operator makes the grid behave like a torus, so every cell — edge or interior — always has exactly eight neighbors to evaluate.

The requestAnimationFrame loop runs every frame regardless of the slider, but it only calls step() when enough real time has elapsed since the last generation, computed as 1000 divided by the slider's value in generations-per-second. That decouples how often the simulation advances from the display's refresh rate, so dragging the slider changes speed immediately without restarting or re-scheduling the loop.

Move the canvas setup, grid state, and the requestAnimationFrame loop into a mount effect scoped to a canvas ref, keeping the grid, generation counter, and running flag in refs rather than component state so the loop doesn't get torn down on every render. Cancel the animation frame and remove event listeners in the cleanup function, and drive the play/pause/step buttons through the same refs.