Canvas interaction
The setup function returns several utilities for interacting with the grid at runtime. Beyond update and draw, you can read the current state, overwrite the entire grid, update individual cells, or swap the rules — all while the simulation is running.
const { update, draw, readGrid, writeGrid, writeCell, writeCellAt, writeCells } = await setup({ canvas, automaton,});Reading the grid
Section titled “Reading the grid”readGrid returns a Promise that resolves to a flat array of element indices, in row-major order (left to right, top to bottom). Each value corresponds to an element’s .index.
const snapshot = await readGrid();// snapshot is a number[] of length width * heightThis is useful for inspecting the current state, running analytics, or saving a snapshot that can be restored later.
Writing the entire grid
Section titled “Writing the entire grid”writeGrid replaces every cell at once. Pass a flat array (or a Uint32Array) of element indices whose length must equal width * height:
writeGrid(snapshot); // restore a previously saved snapshotThe function validates every index in the array and throws if any value is out of range.
Writing a single cell
Section titled “Writing a single cell”writeCell updates one cell without touching the rest of the grid. Pass the flat index of the cell and the element index to assign:
writeCell(index, alien.index);The flat index for a cell at column x and row y in a grid of width width is y * width + x.
If you prefer working with grid coordinates instead of flat indices, use writeCellAt:
writeCellAt(x, y, alien.index);Writing a span of cells
Section titled “Writing a span of cells”writeCells updates consecutive cells, in row-major order, with a single upload. Pass the flat index of the first cell and a Uint32Array of element indices. If you keep an image of the grid as a Uint32Array, a subarray of it works without a copy:
writeCells(first, image.subarray(first, last + 1));It throws if the span doesn’t fit in the grid or if any of its values is out of range.
Practical examples
Section titled “Practical examples”Click to paint
Section titled “Click to paint”Let users draw on the canvas by converting mouse coordinates to cell coordinates and calling writeCellAt. Scale by the grid size, which is canvas.width × canvas.height unless you pass width and height to setup:
const width = 64;const height = 64;const { update, draw, writeCellAt } = await setup({ canvas, automaton, width, height });
canvas.addEventListener("click", (e) => { const rect = canvas.getBoundingClientRect();
// Scale from CSS pixels to grid cells const x = Math.floor((e.clientX - rect.left) * (width / rect.width)); const y = Math.floor((e.clientY - rect.top) * (height / rect.height));
writeCellAt(x, y, alien.index);});Pause, snapshot, resume
Section titled “Pause, snapshot, resume”Save the grid state so you can rewind:
const { update, draw, readGrid, writeGrid } = await setup({ canvas, automaton });
let saved: number[] | null = null;
document.getElementById("save")!.addEventListener("click", async () => { saved = await readGrid();});
document.getElementById("restore")!.addEventListener("click", () => { if (saved) writeGrid(saved);});Changing the rules
Section titled “Changing the rules”setAutomaton replaces the automaton of a running simulation and keeps the grid. Its neighborhood and wrapping replace the old ones too. Cells hold element indices, so the new automaton’s element at index i takes over every cell holding i:
const { setAutomaton } = await setup({ canvas, automaton: life });
// later, for example after the user edits the rulessetAutomaton({ automaton: vi.create() });If the new automaton has fewer elements, cells holding an index it doesn’t have become element 0. Adding the elements back later doesn’t restore those cells.
Cleaning up
Section titled “Cleaning up”destroy frees the GPU memory held by the simulation. Call it when you no longer need the simulation, for example when you remove its canvas from the page or replace it with a new one:
const { update, draw, destroy } = await setup({ canvas, automaton });
// later, when the simulation is no longer neededdestroy();After destroy, update, draw and evolve do nothing, so an animation loop that is still running won’t crash. readGrid, writeGrid, writeCell, writeCellAt, writeCells and setAutomaton throw an error. Calls made before destroy still complete: a readGrid you are awaiting resolves with the grid as it was. Calling destroy more than once is safe.
Handling device loss
Section titled “Handling device loss”The GPU device can be lost, for example when the graphics driver crashes or resets. Pass onDeviceLost to setup to be notified:
const simulation = await setup({ canvas, automaton, onDeviceLost: (info) => { console.warn("GPU device lost:", info.message); // for example, show a button that calls setup again },});Once the device is lost, the simulation behaves as if destroy had been called, and its grid is gone: it lived in GPU memory. Call setup again to create a new simulation; it gets a new device automatically. To resume where you were, take snapshots with readGrid while the simulation runs, and pass the last one to setup as initialGrid.
