Quick Start#

humument loads a single page of A Human Document — its words, OCR boxes, line groupings, whitespace geometry, and a navigation graph — and gives you a POS-pattern phrase chunker, two river pathfinders, and balloon/ribbon geometry to draw over it. It is renderer-agnostic: every drawing primitive returns plain {x, y} point arrays, so you render with any 2D API. The full 367-page dataset is published on npm and fetched from a CDN, so there is nothing to host.

Install#

npm install humument

Zero runtime dependencies. The examples below draw with the browser's built-in Canvas2D API, so nothing else is needed.

No build step? CDN#

Load the IIFE bundle from a CDN — it exposes a HumumentLib global, so it works in a plain <script> with no bundler:

<script src="https://cdn.jsdelivr.net/npm/humument@0.1/dist/index.global.js"></script>
<script>
  const { Humument } = HumumentLib;
  // ... same API as the ESM import
</script>

Load a page#

The full dataset (humument-data for OCR words + geometry, humument-images for page scans) is published on npm, and the library's defaults point at it via jsDelivr. Humument.load({ page: 33 }) works from any origin without hosting anything.

Humument.load is async — it returns a promise. Await it (or use .then), then read the page's data off the returned H instance:

import { Humument } from 'humument'; // CDN: const { Humument } = HumumentLib;

const H = await Humument.load({ page: 33 });

console.log(H.page.number, H.page.width, H.page.height);
const phrases = H.selectChunks({ nSeeds: 4, minLineDist: 3, seed: 42 });

H.page.imageUrl is the URL of the page scan; the library never loads the image itself — that's the host's job (see below).

Draw it (Canvas2D)#

Load the page image, then stroke a balloon around each selected phrase. Every coordinate is already in the page image's pixel space, so it lines up 1:1.

import { Humument } from 'humument';

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

const H = await Humument.load({ page: 33 });
canvas.width = H.page.width;
canvas.height = H.page.height;

// 1. draw the page scan
const img = new Image();
img.crossOrigin = 'anonymous';
img.src = H.page.imageUrl;
await img.decode();
ctx.drawImage(img, 0, 0);

// 2. wrap a balloon around each selected phrase
ctx.strokeStyle = '#141414';
ctx.fillStyle = '#ffffff';
for (const phrase of H.selectChunks({ nSeeds: 4, minLineDist: 3, seed: 42 })) {
  const outline = H.geom.balloon(H.bboxOf(phrase), { wobble: 0.15 }); // → Pt[]
  ctx.beginPath();
  outline.forEach((p, i) => (i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)));
  ctx.closePath();
  ctx.fill();
  ctx.stroke();
}

The same pattern renders to SVG, WebGL, or a server-side canvas — H.geom.* returns points, and you draw them however you like (see Geometry).

Draw a faithful blob chain#

For the true A Humument look, fuse the phrases into one text-hugging silhouette with H.geom.blob (0.2.0) — tight hulls around every word, joined by tapered necks:

const phrases = H.selectChunks({ nSeeds: 4, minLineDist: 3, seed: 42 });
const spec = H.geom.blobSpec(phrases);            // auto-necks between phrases
for (const loop of H.geom.blob(spec, { pad: 8, blend: 10, seed: 7 })) {
  ctx.beginPath();
  loop.forEach((p, i) => (i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)));
  ctx.closePath();
  ctx.fill();   // fill + stroke one path = one ink rim
  ctx.stroke();
}

Draw a river#

Connect two words with a whitespace river, then fill its ribbon:

const seg = H.river.between(wordA, wordB); // ChannelSegment | null
if (seg) {
  const ribbon = H.geom.channel(seg); // → Pt[] (closed ring)
  ctx.beginPath();
  ribbon.forEach((p, i) => (i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)));
  ctx.closePath();
  ctx.fill();
}

Next#