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.
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.
getLayoutimport 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 variantstype 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.
renderOverlayA 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 resolvertype 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:
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.
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.
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.Svg node renders ONLY <svg> and <path> — no <g>, <text>, <image>, <use>, <foreignObject>, etc. See styling.