npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tamb/gamegrid

v1.0.0

Published

A 2D HTML grid for creating web games (or other things that could use a 2D matrix)

Readme

GameGrid

A 2D HTML Grid for Creating Web Games

or other things that could use a 2D matrix

Docs & demo (GitHub Pages): site home · API reference · interactive demo

Goals

  • 2D grid in memory with coordinates and movement rules
  • Hooks: callbacks, middleware, and DOM-optional rendering
  • TypeScript types included
  • Have fun with it

Demo (Parcel app)

Try the published demo on GitHub Pages, or run it locally (below).

The demo/ package depends on this library via "@tamb/gamegrid": "file:.." so npm install inside demo/ always picks up the built dist/ next to it (no npm pack tarball).

Demo scripts (run from repo root)

| Script | Purpose | |--------|---------| | npm run demo | Clean lib + demo artefacts, build library, install demo deps, compile Handlebars, start Parcel | | npm run demo:safe | Same as demo, but runs tests before building | | npm run demo:dev | Start Parcel only (after demo:prepare or a prior demo run) | | npm run demo:test | Run library Vitest suite | | npm run demo:prepare | npm run build + npm install in demo/ | | npm run demo:build | Production Parcel build in demo/dist | | npm run demo:link | Build, npm link the library, install demo deps, link @tamb/gamegrid in the demo | | npm run demo:link:lib | Build + npm link only (registers @tamb/gamegrid globally) |

Fresh start:

npm run demo

Fast iteration after library changes (rebuild dist/, then reload Parcel):

npm run build && npm run demo:dev

npm link workflow (optional):

npm run demo:link
npm run demo:dev

After a change to the library, run npm run build again so dist/ updates; Parcel will pick it up on reload when using file:.. or a npm link symlink.

API documentation (TypeDoc)

Browse the published API reference on GitHub Pages, or generate HTML locally:

npm run docs

HTML lands in gh-pages/docs/ (open gh-pages/docs/index.html locally). The TypeDoc landing page uses docs/API.md (overview + table of contents); the full README stays on GitHub.

Combined demo + docs bundle for GitHub Pages:

npm run gh-pages

That clears gh-pages/docs and gh-pages/demo, npm run build, reinstalls demo/ deps, runs TypeDoc to gh-pages/docs, and parcel build to gh-pages/demo/. The checked-in gh-pages/index.html links to demo/output.html and docs/.

GitHub Pages

Live site: https://tamb.github.io/game-grid/ (API docs at /docs/, demo at /demo/output.html).

Use gh-pages/ as the site root /: keep index.html and .nojekyll tracked. Generated gh-pages/docs/ and gh-pages/demo/ are gitignored (so they won't show up in git status) — editors may hide gitignored folders; this repo sets explorer.excludeGitIgnore to false in .vscode/settings.json so gh-pages/demo stays visible locally. Confirm with ls gh-pages/demo after npm run gh-pages.

TSDoc tip: {@link …} tags must appear in normal comment text. Wrapping the whole {@link …} in inline code (Markdown backticks) stops TypeDoc from resolving links in the generated HTML.

Coordinates

Movement and state use [x, y]: column (x), then row (y). The backing matrix is a normal 2D array: matrix[row][col] i.e. matrix[y][x]. Methods like getCell([x, y]), setActiveCell(x, y, …), and getState().activeCoords all follow that convention.

The class

Install @tamb/gamegrid. The default export is GameGrid. Many constants and types are named exports (see Public exports).

import type { IGameGrid } from "@tamb/gamegrid";
import GameGrid from "@tamb/gamegrid";

// Optional second argument: container to render into immediately.
const grid: IGameGrid = new GameGrid(config, rootElement);

// Headless (no DOM): omit the container.
const memory: IGameGrid = new GameGrid(config);

When you pass a container in the constructor, render(container) runs immediately. Otherwise call render(element) later. Headless mode sets refs.cells to your matrix reference and state.rendered to false.

config: IConfig

export interface IConfig {
  options?: IOptions;
  matrix: ICell[][];
  state?: IDefaultState | IState;
}

options: IOptions

export type MiddlewareFn = (
  gamegridInstance: IGameGrid,
  patch: StatePatch,
) => void;

export interface IOptions {
  id?: string;
  /** Dispatches custom events here; defaults to `window`. */
  eventTarget?: EventTarget;
  arrowControls?: boolean;
  wasdControls?: boolean;
  infiniteX?: boolean;
  infiniteY?: boolean;
  clickable?: boolean;
  rewindLimit?: number;
  middlewares?: {
    pre?: MiddlewareFn[];
    post?: MiddlewareFn[];
  };
  callbacks?: {
    onMove?: (gamegridInstance: IGameGrid, newState: IState) => void;
    onLand?: (gamegridInstance: IGameGrid, newState: IState) => void;
    onBlock?: (gamegridInstance: IGameGrid, newState: IState) => void;
    onCollide?: (gamegridInstance: IGameGrid, newState: IState) => void;
    onDettach?: (gamegridInstance: IGameGrid, newState: IState) => void;
    onBoundary?: (gamegridInstance: IGameGrid, newState: IState) => void;
    onBoundaryX?: (gamegridInstance: IGameGrid, newState: IState) => void;
    onBoundaryY?: (gamegridInstance: IGameGrid, newState: IState) => void;
    onWrap?: (gamegridInstance: IGameGrid, newState: IState) => void;
    onWrapX?: (gamegridInstance: IGameGrid, newState: IState) => void;
    onWrapY?: (gamegridInstance: IGameGrid, newState: IState) => void;
    onZoomSet?: (gamegridInstance: IGameGrid, newState: IState) => void;
    onZoomCleared?: (gamegridInstance: IGameGrid, newState: IState) => void;
    onZoomEdge?: (gamegridInstance: IGameGrid, newState: IState) => void;
    onZoomExit?: (gamegridInstance: IGameGrid, newState: IState) => void;
    onRegionChange?: (gamegridInstance: IGameGrid, newState: IState) => void;
  };

  /** Cell `type` values you cannot step onto; you stay on the previous cell. */
  blockOnType?: string[];
  /** Cell `type` values that trigger collision when entered (you still move unless also blocked). */
  collideOnType?: string[];
  /**
   * If non-empty, only these `type` values are enterable (in addition to `blockOnType`).
   * If omitted or empty, any non-blocked cell is enterable.
   */
  moveOnType?: string[];

  /** Default whether zoom transitions animate. Overridden by per-call `IZoomOptions.animate`. */
  animateZoom?: boolean;
  /** When zoom is set, keep movement inside the zoom window (default `true`). */
  constrainToZoom?: boolean;
  /** Opt-in region tracking (`2` = quadrants, `3` = ninths). */
  regionDivisions?: number;

  /** CSS transition duration (ms) for zoom slide. Default: `300`. */
  zoomSlideDuration?: number;
  /** When `true` with `regionDivisions`, `ZOOM_EDGE` auto-zooms to the adjacent region. Default: `false`. */
  slideZoomOnEdge?: boolean;
  /** Appended to `.gamegrid__viewport` when zoom is active. */
  zoomViewportClasses?: string[];

  activeClasses?: string[];
  cellClasses?: string[];
  containerClasses?: string[];
  rowClasses?: string[];
}

Default options (before your config.options spread):

{
  arrowControls: true,
  wasdControls: false,
  infiniteX: false,
  infiniteY: false,
  clickable: true,
  rewindLimit: 20,
  blockOnType: [cellTypeEnum.BARRIER],
  collideOnType: [cellTypeEnum.INTERACTIVE],
  moveOnType: [],
  animateZoom: false,
  constrainToZoom: true,
  zoomSlideDuration: 300,
  slideZoomOnEdge: false,
}

Use cellAttributes on ICell for per-cell attributes; activeClasses / cellClasses / containerClasses / rowClasses append classes on render.

matrix: ICell[][]

Rows of cells. Each ICell must include type (see cellTypeEnum). Optional render, cellAttributes, etc.

export interface ICell extends IRef {
  type: string;
  render?: (context: ICellContext) => HTMLElement;
  cellAttributes?: string[][];
  eventTypes?: { onEnter: string; onExit: string };
  coords?: number[];
}

interface ICellContext {
  coords: number[];
  cell: ICell;
  gamegrid: IGameGrid;
}

state: IState

export interface IState {
  activeCoords: number[];
  prevCoords: number[];
  moves: number[][];
  rendered?: boolean;
  currentDirection?: string;
  zoom: IZoomBounds | null;
  region: IRegionTile | null;
}

export type StatePatch = Partial<IState> & Record<string, unknown>;

setStateSync(patch) shallow-merges a StatePatch into state. StatePatch still allows arbitrary extra keys for your own bookkeeping.

Initial merge uses INITIAL_STATE from the package (actual export lives in src/enums.ts):

export const INITIAL_STATE: IState = {
  activeCoords: [0, 0],
  prevCoords: [0, 0],
  rendered: false,
  moves: [],
  currentDirection: directionEnum.DOWN,
  zoom: null,
  region: null,
};

Middleware

pre runs before the merge; you can mutate the patch object in place before it is merged.

post runs after the merge; use gamegridInstance.getState() for the full merged IState. The second argument remains the patch passed to setStateSync.

Refs

export interface IRefsObject {
  container: HTMLElement | null;
  rows: IRow[];
  cells: ICell[][];
}

export interface IRow extends IRef {
  index: number;
  cells: ICell[];
}

IRow can carry a current HTMLDivElement for the row when rendered. IRefs is a deprecated alias for IRefsObject.

IGameGrid (instance API)

export interface IGameGrid {
  refs: IRefsObject;
  options: IOptions;

  render(container: HTMLElement): void;
  /** Rebuild DOM from current `matrix` and re-apply active cell UI. Requires a prior render. */
  refresh(): void;
  /** Tear down listeners and DOM when rendered; always emits DESTROYED. */
  destroy(): void;
  getOptions(): IOptions;
  setOptions(newOptions: IOptions): void;

  getState(): IState;
  setStateSync(obj: StatePatch): void;

  getActiveCell(): ICell;
  getPreviousCell(): ICell;
  getCell(coords: readonly [number, number] | number[]): ICell;
  getAllCellsByType(type: string): ICell[];
  setActiveCell(x: number, y: number, direction?: string): void;

  getMatrix(): ICell[][];
  setMatrix(matrix: ICell[][]): void;

  moveUp(): void;
  moveRight(): void;
  moveDown(): void;
  moveLeft(): void;

  getZoom(): IZoomBounds | null;
  setZoom(bounds: IZoomBounds, options?: IZoomOptions): void;
  clearZoom(options?: IZoomOptions): void;
  getZoomAround(center: readonly [number, number] | number[], radiusX: number, radiusY?: number): IZoomBounds;
  getQuadrantZoom(quadrant: ZoomQuadrant): IZoomBounds;
  getFractionZoom(divisions: number, tileX: number, tileY: number): IZoomBounds;
  zoomAround(center: readonly [number, number] | number[], radiusX: number, radiusY?: number, options?: IZoomOptions): void;
  zoomQuadrant(quadrant: ZoomQuadrant, options?: IZoomOptions): void;
  zoomFraction(divisions: number, tileX: number, tileY: number, options?: IZoomOptions): void;
  getRegionAt(coords: readonly [number, number] | number[], divisions?: number): IRegionTile;
  getActiveRegion(): IRegionTile | null;
}

The GameGrid class implements IGameGrid. The mounted root element is refs.container after render; it stays null on headless constructions until render runs.

Events

Events are bubbling CustomEvents. Their detail objects implement IGameGridEventDetail: at minimum { gameGridInstance: IGameGrid } (plus any extra keys you pass if you call fireGameGridEvent yourself). For typing listeners, use GameGridDOMEvent (CustomEvent<IGameGridEventDetail>).

By default the grid dispatches on window. Set options.eventTarget (for example a dedicated EventTarget) so multiple grids do not all share the global bus.

gameGridEventsEnum is an identical compatibility alias — use either name.

export const gridEventsEnum = {
  // Dispatched after GameGrid.render wires the container (`detail` follows IGameGridEventDetail).
  RENDERED: "gamegrid:grid:rendered",
  // Dispatched at the end of construction (after optional initial render).
  CREATED: "gamegrid:grid:created",
  // Dispatched from GameGrid.destroy; fires even if the grid stayed headless / unmounted.
  DESTROYED: "gamegrid:grid:destroyed",

  // Keyboard / pointer path: onMove already ran; these fire before setActiveCell.
  MOVE_LEFT: "gamegrid:move:left",
  MOVE_RIGHT: "gamegrid:move:right",
  MOVE_UP: "gamegrid:move:up",
  MOVE_DOWN: "gamegrid:move:down",

  // Target rejected by blockOnType or moveOnType allow-list; coords roll back.
  MOVE_BLOCKED: "gamegrid:move:blocked",
  // Entered a collideOnType cell (movement may still succeed).
  MOVE_COLLISION: "gamegrid:move:collide",
  // Left a collide-type cell from the square occupied before this move attempt.
  MOVE_DETTACH: "gamegrid:move:dettach",
  // Finished block/collide/boundary/wrap resolution; mirrors callbacks.onLand.
  MOVE_LAND: "gamegrid:move:land",

  // Aggregate finite-edge clamp — axis BOUNDARY_X / BOUNDARY_Y first when relevant.
  BOUNDARY: "gamegrid:move:boundary",
  // X-axis requested outside row span when infiniteX is off — coordinate clamped.
  BOUNDARY_X: "gamegrid:move:boundary:x",
  // Y-axis requested outside matrix height when infiniteY is off — coordinate clamped.
  BOUNDARY_Y: "gamegrid:move:boundary:y",

  // Aggregate infinite wrap — WRAP_X / WRAP_Y first when relevant.
  WRAP: "gamegrid:move:wrap",
  // Horizontal infinite teleport; runs alongside callbacks.onWrapX.
  WRAP_X: "gamegrid:move:wrap:x",
  // Vertical infinite teleport; runs alongside callbacks.onWrapY.
  WRAP_Y: "gamegrid:move:wrap:y",

  // Zoom viewport lifecycle
  ZOOM_SET: "gamegrid:zoom:set",
  ZOOM_CLEARED: "gamegrid:zoom:cleared",
  ZOOM_EDGE: "gamegrid:zoom:edge",
  ZOOM_EXIT: "gamegrid:zoom:exit",
  REGION_CHANGE: "gamegrid:region:change",
};

This mirrors src/enums.ts (same keys and string literals). Import gridEventsEnum or gameGridEventsEnum from @tamb/gamegrid rather than duplicating. The published TypeDoc site (or npm run docs locally) expands the same members with full cross-links.

Instantiation quick start

import GameGrid, { gridEventsEnum, type GameGridDOMEvent } from "@tamb/gamegrid";

const gg = new GameGrid(
  {
    matrix: myMatrix,
    state: { activeCoords: [0, 0] },
    options: { wasdControls: true },
  },
  document.querySelector("#root")!,
);

gg.moveDown();
window.addEventListener(gridEventsEnum.MOVE_LAND, (e: Event) => {
  const ce = e as GameGridDOMEvent;
  console.log(ce.detail.gameGridInstance);
});

For a grid created without a container, call render(el) when you want DOM.

Zoom

Zoom defines a viewport window over the full matrix. Coordinates stay world [x, y] — zoom does not create a submatrix or remap the origin.

| Term | Meaning | |------|---------| | zoom | Current viewport bounds (IZoomBounds on state) | | region | A tile from partitioning the grid (regionDivisions; quadrants when 2) | | zoom edge | Active cell tried to move past the zoom window while constrainToZoom is enabled | | zoom exit | Active cell left the zoom window while constrainToZoom is disabled | | region change | Active cell moved from one region tile to another (e.g. SE → SW) | | zoom slide | Moving the zoom window with animate: true (built-in CSS transform on .gamegrid__viewport) |

When zoom is active, the grid renders only cells inside the zoom window inside a .gamegrid__viewport wrapper. Useful CSS hooks:

| Class / attribute | When | |-------------------|------| | gamegrid--zoomed | Container while zoom is set | | gamegrid__viewport | Viewport wrapper (always after render) | | gamegrid__cell--zoom-edge | Perimeter cells of the zoom window | | gamegrid--zoom-animating | During CSS slide transition | | data-gamegrid-zoom | Viewport; "minX,minY,maxX,maxY" | | data-gamegrid-region | Viewport; quadrant label when regionDivisions: 2 |

import GameGrid, { gridEventsEnum, type ZoomQuadrant } from "@tamb/gamegrid";

const gg = new GameGrid({
  matrix: largeMap,
  options: {
    regionDivisions: 2,
    animateZoom: true,
    slideZoomOnEdge: true,
    constrainToZoom: true,
    zoomSlideDuration: 300,
  },
});

gg.zoomQuadrant("se");

// Manual edge slide (when slideZoomOnEdge is false):
target.addEventListener(gridEventsEnum.ZOOM_EDGE, (e) => {
  const { gameGridInstance } = e.detail;
  gameGridInstance.zoomQuadrant("sw", { animate: true });
});

With slideZoomOnEdge: true, the library auto-advances to the adjacent region on ZOOM_EDGE — no listener required.

Public exports

Besides the default GameGrid, the package re-exports:

  • Types: IConfig, IOptions, IState, IGameGrid, IGameGridEventDetail, GameGridDOMEvent, ICell, ICellContext, IRefsObject, IRow, IDefaultState, IZoomBounds, IZoomOptions, IRegionTile, ZoomQuadrant, MiddlewareFn, StatePatch, and deprecated IRefs
  • Values: gridEventsEnum, gameGridEventsEnum, cellTypeEnum, classesEnum, directionEnum, directionClassEnum, INITIAL_STATE, keycodeEnum

cellTypeEnum values are constants on an object (not an enum). classesEnum and directionEnum are TypeScript enums. Example:

import GameGrid, {
  cellTypeEnum,
  classesEnum,
  directionEnum,
  gridEventsEnum,
} from "@tamb/gamegrid";

// cell — const object:
cellTypeEnum.OPEN;

// enums:
classesEnum.GRID;
directionEnum.DOWN;

// Event name strings — see [Events](#events) for the full map
gridEventsEnum.BOUNDARY === "gamegrid:move:boundary";