Skip to content

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,
});

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 * height

This is useful for inspecting the current state, running analytics, or saving a snapshot that can be restored later.

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 snapshot

The function validates every index in the array and throws if any value is out of range.

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);

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.

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);
});

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);
});

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 rules
setAutomaton({ 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.

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 needed
destroy();

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.

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.