Have more questions? Join our

This documents framework v1, which is deprecated. Games already on it keep running and their authors can keep editing them, but new games are created on the current framework — start with React frontends.

Layout and overlays

Two optional, viewer-local hooks shape the surface the board renders into: getLayout decides how player areas and the board are arranged; renderOverlay paints connector lines and badges above everything.

getLayout

import type { GetLayoutFn } from "boardweaver";

export const getLayout: GetLayoutFn = (state, viewingPlayerId, viewport) => ({
  type: "grid",
  columns: ["1fr", "2fr", "1fr"],
  rows: ["auto", "1fr", "auto"],
  areas: [
    ["opponent", "opponent", "opponent"],
    ["log",      "board",    "log"     ],
    ["me",       "me",       "me"      ],
  ],
});

Signature:

type GetLayoutFn<MetaData = Record<string, unknown>> = (
  gameState: GameState<MetaData>,
  viewingPlayerId: number,
  viewport: { width: number; height: number },
) => Layout;

When getLayout is not exported, the engine uses { type: "radial" }.

Layout variants

type RadialLayout = { type: "radial" };

type GridLayout = {
  type: "grid";
  columns: string[];                                   // CSS grid track sizes
  rows: string[];
  areas: (string | null)[][];                          // rows × columns; null = empty cell
  gap?: number;                                        // px; default 8
  areaOptions?: Record<string, { rotation?: 0 | 90 | 180 | 270 }>;
};

type Layout = RadialLayout | GridLayout;

areas is a 2D array of area ids. Reserved ids: "board" (main board), "me" (the viewer's player area), and any playerId stringified for other players. Custom area ids may appear in areas to slot in non-player content.

areaOptions[id].rotation rotates that area's visual output by a cardinal angle.

Read-only. Runs on every render.

renderOverlay

A full-bleed, pointer-events: none layer drawn above the board. Use for connector lines (attacker → defender), badges that need to sit outside their piece's bounds, or arrows that span multiple spaces.

import type { RenderOverlayFn } from "boardweaver";

export const renderOverlay: RenderOverlayFn = (state, viewingPlayerId, clientState, overlay) => {
  const from = overlay.rectOf({ piece: "attacker" });
  const to = overlay.rectOf({ piece: "defender" });
  if (!from || !to || from.visibility === "clipped" || to.visibility === "clipped") return [];

  const start = { x: from.rect.x + from.rect.width / 2, y: from.rect.y + from.rect.height / 2 };
  const end   = { x: to.rect.x   + to.rect.width   / 2, y: to.rect.y   + to.rect.height   / 2 };

  return [
    {
      type: "Svg",
      style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%" },
      viewBox: { width: 1000, height: 1000 },
      paths: [
        { d: `M ${start.x} ${start.y} L ${end.x} ${end.y}`, stroke: "#ff3b3b", strokeWidth: 3 },
      ],
    },
  ];
};

Signature:

type RenderOverlayFn<MetaData = Record<string, unknown>> = (
  gameState: GameState<MetaData>,
  viewingPlayerId: number,
  clientState: GlobalClientStateReadView,
  overlay: Overlay,
) => OverlayBwssNode[];

Returns a flat array. Drawn in array / paint order (no z-index). Top-level nodes position themselves with style.position: "absolute" + top / left.

OverlayBwssNode is the piece-side BWSS union: Box | Image | Svg. Connectors are Svg nodes — BWSS has no Line node, but Svg supports viewBox + path d. See styling.

Overlay resolver

type OverlayHandle =
  | { piece: string }     // piece id (registered by the Piece renderer)
  | { space: string }     // space id (registered by the Space renderer)
  | { player: number };   // player area (registered by the PlayerInfo renderer)

type OverlayRect = { x: number; y: number; width: number; height: number };
type OverlayClip = OverlayRect | null;

type ResolvedTarget = {
  rect: OverlayRect;
  clip: OverlayClip;
  visibility: "visible" | "clipped";
};

type Overlay = {
  rectOf(handle: OverlayHandle): ResolvedTarget | null;
  clampToClip(point: { x: number; y: number }, clip: OverlayClip): { x: number; y: number };
};

Coordinates are in overlay-layer local px; {x:0,y:0} is the top-left of the board area.

rectOf is a synchronous host function — calling it subscribes this overlay to re-run when:

  • That handle's rect changes (resize / scroll / mount).
  • The overlay element registry version bumps (mounts / unmounts).
  • The viewer's clientState.global mutates.

A null return (handle present but element unmounted) still subscribes; the overlay re-fires when the element mounts.

clampToClip per-axis clamps a point into a clip rect. clip: null returns the point unchanged.

Client state — viewer-local UI bucket

Both getLayout and renderOverlay are viewer-local. renderOverlay's third argument is the global client-state bucket (read-only):

type GlobalClientStateReadView<G extends Record<string, unknown> = {}> = Readonly<G>;

Pieces and spaces read the same bucket through clientState.global (4th positional arg in render()); onMouseEnter / onMouseLeave write through a writable view of the same bucket. Use it for cross-item viewer state ("currently dragging piece X", "hovered space kind Y"). Never on the wire.

Gotchas

  • getLayout runs per viewer — viewingPlayerId is the viewer, not necessarily currentPlayer. Use it to pin the viewer's area to "me" and others' to their player id.
  • GridLayout.areas must be a true rectangle: areas.length === rows.length and every areas[r].length === columns.length.
  • renderOverlay is read-only on gameState. Convert pointer gestures into actions via the existing dispatch path (not by mutating state from the overlay).
  • Svg nodes have hard caps: max 16 paths, max 4000 chars per d, max 16,000 chars total across paths, viewBox sides ≤ 10,000.
  • An Svg node renders ONLY <svg> and <path> — no <g>, <text>, <image>, <use>, <foreignObject>, etc. See styling.
  • Both hooks must be deterministic in their inputs. Cache nothing across calls (the framework re-runs them on every relevant change).