Skip to content

carnworkstudios/boxwood

Repository files navigation

Boxwood

npm version GitHub Actions License

A Declarative Box-Split and Collision Engine for Vector Graphics. It provides a CSS-like layout grammar (padding, gap, cols, rows, auto-height) and resolves it into a clean JSON coordinate tree for any visual canvas (SVG, <canvas>, WebGL).

Unlike typical layout libraries (like Yoga or standard web browsers) that hide resolved position values inside a black box, Boxwood exposes absolute coordinate geometry ({x, y, w, h}) directly. That makes it easy to draw responsive visuals, route connectors, and manage collision-safe compositions.


Demo Animation

Quick start

npm install @canwork/boxwood
import { resolveLayout, defaultMeasure, LNode } from "@canwork/boxwood";

const layoutTree: LNode = {
  style: { padding: 20 },
  split: { cols: ["60%", "40%"], gap: 15 },
  children: [
    { id: "main-panel", style: { w: "100%", h: "100%" } },
    {
      style: { w: "100%", h: "100%" },
      split: { rows: 2, gap: 10 },
      children: [
        { id: "sub-card-1", style: { padding: 12 } },
        { id: "sub-card-2", style: { padding: 12 } },
      ],
    },
  ],
};

const boundary = { x: 0, y: 0, w: 1920, h: 1080 };
const result = resolveLayout(layoutTree, boundary, { measure: defaultMeasure });
console.log(result.boxes);

CDN (no build step)

<script src="https://cdn.jsdelivr.net/npm/@canwork/boxwood/dist/boxwood.global.js"></script>
<script>
  const { resolveLayout, defaultMeasure } = Boxwood;
  const result = resolveLayout(layoutTree, boundary, { measure: defaultMeasure });
</script>

Or via unpkg:

<script src="https://unpkg.com/@canwork/boxwood/dist/boxwood.global.js"></script>

Layout flow at a glance

flowchart TD
    A["LNode tree\nstyle / split / children"] --> B["resolveLayout(root, boundary, opts)"]
    B --> C["Resolve layout rules\npadding, gap, cols, rows, grid"]
    B --> D["Measure content\ntext wrap / overflow"]
    C --> E["Compute contentFrame and box geometry"]
    D --> E
    E --> F["Resolved boxes\n{x, y, w, h}"]
    F --> G["Render with SVG / Canvas / WebGL"]
Loading
flowchart TB
    R["root LNode"] --> C1["child LNode"]
    R --> C2["child LNode"]
    C1 --> G1["grandchild LNode"]
    C2 --> G2["grandchild LNode"]
    C1 --> G3["grandchild LNode"]
    C2 --> G4["grandchild LNode"]
    G1 --> B1["resolved box"]
    G2 --> B2["resolved box"]
    G3 --> B3["resolved box"]
    G4 --> B4["resolved box"]
Loading

Why Boxwood?

Boxwood is designed for developers who need a declarative layout model but also need the exact coordinates to render shapes, arrows, labels, and collision-aware content.

Layout Library Declarative Box Model Coordinates for Lines/Arrows Lightweight & Zero-DOM Collision Separation
Yoga / Flexbox ✅ Yes ❌ No ⚠️ Complex C++ bindings ❌ No
D3 / Cytoscape ❌ No ✅ Yes ✅ Yes ⚠️ Force-directed
Boxwood ✅ Yes ✅ Yes ✅ Yes ✅ Yes

What Boxwood gives you

  • Declarative layout rules for cols, rows, grids, gap, padding, margin, and auto sizing
  • Absolute coordinate output ready for SVG, <canvas>, WebGL, or custom rendering
  • Nested layout composition with resolved geometry for every node
  • Text measurement and overflow-aware sizing via measure
  • Collision resolution so layout boxes do not overlap unexpectedly

Demo

Open examples/interactive-demo.html in your browser to see the engine running with a live resize demo.


Core API & Types

1. Defining a Layout Tree (LNode)

Layouts are represented as a nested tree structure:

import { LNode } from "@canwork/boxwood";

const layoutTree: LNode = {
  style: { padding: 20 },
  split: { cols: ["60%", "40%"], gap: 15 },
  children: [
    {
      id: "main-panel",
      style: { w: "100%", h: "100%" }
    },
    {
      style: { w: "100%", h: "100%" },
      split: { rows: 2, gap: 10 },
      children: [
        { id: "sub-card-1", style: { padding: 12 } },
        { id: "sub-card-2", style: { padding: 12 } }
      ]
    }
  ]
};

2. Resolving Layout Coordinates (resolveLayout)

Run the solver on your layout tree by passing the root node and the parent boundary dimensions:

import { resolveLayout } from "@canwork/boxwood";

const boundary = { x: 0, y: 0, w: 1920, h: 1080 }; // 1080p frame
const result = resolveLayout(layoutTree, boundary);

console.log(result.boxes);
/*
Outputs a flat list of absolute coordinate boxes:
[
  { id: "main-panel", box: { x: 20, y: 20, w: 1128, h: 1040 } },
  { id: "sub-card-1", box: { x: 1163, y: 20, w: 737, h: 515 } },
  { id: "sub-card-2", box: { x: 1163, y: 545, w: 737, h: 515 } }
]
*/

Drawing Layout Connections

Because Boxwood returns absolute coordinate values, routing arrows and lines between elements is simple:

// Find the resolved boxes
const boxA = result.boxes.find(b => b.node.id === "sub-card-1")?.box;
const boxB = result.boxes.find(b => b.node.id === "sub-card-2")?.box;

if (boxA && boxB) {
  // Center coordinates of both boxes
  const x1 = boxA.x + boxA.w / 2;
  const y1 = boxA.y + boxA.h / 2;
  const x2 = boxB.x + boxB.w / 2;
  const y2 = boxB.y + boxB.h / 2;

  // Render SVG or Canvas lines using these points
  // e.g. <line x1={x1} y1={y1} x2={x2} y2={y2} stroke="gold" />
}

Advanced Features

1. Dynamic Text Measurements & Font Sizing

You can pass custom measurement rules to child nodes using a measure hook. The engine will automatically evaluate if text fits inside a container. If text wraps or overflows, it raises a structured OverflowSignal in the solver result, letting you decrease font size or handle resizing programmatically.

2. Collision Separation Solver

When visual cards enter with scale spring animations, or dynamic data changes their sizes on the fly, Boxwood runs an overlap solver to push boxes apart, guaranteeing your nodes never overlap.


Visual Examples & Use Cases

Start here — one self-contained page, no build step

examples/showcase.html — open it directly in any browser. It loads Boxwood from a CDN and runs one complete use case: a declarative pipeline dashboard that re-resolves on every resize. Drag the ◢ corner and the same tree reflows from columns to rows while the SVG connectors re-route to follow the boxes. The entire integration is the drawScene() function at the bottom of the file — read it to see exactly how you'd wire Boxwood into your own project: build a tree → resolveLayout() → draw the boxes → connect their coordinates.

Four worked TypeScript scenes

Each scene is a real *.scene.ts module — the way you'd actually structure Boxwood inside a TypeScript app. Run the build to bundle them into interactive, resizable HTML pages:

npm run examples   # → writes examples/01..04 *.html

Then open any of the generated pages and drag the corner handle:

  1. 01-bento-grid.scene.ts: A dashboard "Bento" grid built from nested col/row splits, with a hero tile and a KPI cluster. Resize and it reflows from a 3-column layout to a single-column stack; every tile auto-sizes to its wrapped copy.
  2. 02-vector-connections.scene.ts: A 1-source / 3-worker / 1-sink pipeline. The six bezier connectors are routed purely from the resolved box coordinates, so they stay correct whether the split is laid out as columns (wide) or rows (tall).
  3. 03-absolute-to-responsive.scene.ts: Takes a PDF extract's absolute x/width spans, converts them to percentage columns once, and lets the engine reflow them — the two-column body narrows and the paragraphs rewrap as you shrink the panel.
  4. 04-dynamic-reflow-overflow.scene.ts: Auto-height (a card with no fixed height grows to its text), a real OverflowSignal (a fixed-height card surfaces a banner once the text no longer fits), and collision separation (two badges placed at the same coordinate are nudged apart).

The shared helpers live in examples/kit.ts (connectors, intrinsic sizing) and examples/shell.ts (the resize runtime + SVG drawing). The original brutalist playground is still available at examples/interactive-demo.html.


License

MIT © Canwork Studios

About

A Declarative Box-Split and Collision Engine for Vector Graphics. It provides a CSS-like layout grammar (padding, gap, cols, rows, auto-height), but outputs a clean JSON coordinate tree that any visual canvas (SVG, <canvas>, WebGL) can use to draw pixels, connectors, and particles.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors