Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gridmath

test license: MIT

Computational, RTL-aware grid and layout mathematics. Zero runtime dependencies.

Built from an exhaustive survey of every open-source and academic approach to computational grid/layout design (154 items across 12 search angles — see docs/RESEARCH.md). The survey's central finding: no existing project anywhere reasons about grid geometry in bidi-aware inline/block axes. Every masonry engine, bento generator, and grid-template tool outputs physical CSS properties (left/right, literal column numbers) with zero direction-awareness baked in. This library exists to fill that gap, alongside the classical/well-established pieces (margin canons, modular type scales, baseline rhythm, font-metric-driven trims) that do have solid prior art — ported and cited rather than reinvented.

What's in here

Module What it does Status
canons.mjs Classical margin canons (Van de Graaf, Progressive, Villard de Honnecourt, Ateliers, Rosarivo) as pure f(W,H,params) → margins Well-established, ported from documented sources
type-scale.mjs Modular type scale (size(n) = base × ratio^n), multi-strand normalization, named ratio table Well-established
font-metrics.mjs Zero-dependency sfnt (TrueType/OpenType) table parsing — unitsPerEm, ascent/descent, capHeight/xHeight, fsType — plus Capsize-style baseline trims Well-established math, hand-rolled parser
baseline-rhythm.mjs Line-height snapping, brute-force baseline-unit derivation, largest-remainder apportionment Well-established
grid-modules.mjs Row/column track computation composing the above Well-established
axis-resolver.mjs The novel piece. Resolves logical (inline-start/end) margins, column order, and nested grid-grammar trees into physical left/right coordinates given a reading direction New — this is the confirmed gap the research found
line-breaker.mjs Knuth-Plass paragraph line-breaking, extended with a kashida (Arabic elongation) glue primitive New — the single most important invention the research identified; the kashida-priority distribution logic is this project's own, not validated against professional Arabic typesetting standards yet
grid-grammar.mjs Seeded stochastic split-grammar for procedural grid variation (Gerstner's "design programme" idea, made concrete) — logical axes only, direction-agnostic by design New — inspired by comparing WFC/cellular-automata/split-grammar approaches; a split-grammar can't fail to tile (no backtracking class), unlike WFC's documented "locally plausible, globally incoherent" failure mode
index.mjs Composes stages 1–7 of the full pipeline into one buildGrid() call

87 tests, all passing, npm test. Every "well-established" module is tested against hand-verified, independently-computed values, not just "doesn't crash." The novel modules (axis-resolver, line-breaker, grid-grammar) are the ones to scrutinize hardest before relying on them in production — read their source comments for the honest scope/caveat notes before extending.

Usage

import { buildGrid } from 'gridmath';
import { readFileSync } from 'node:fs';

const grid = buildGrid({
  page: { width: 595, height: 842, unit: 'pt' },   // A4 in points
  direction: 'rtl',                                 // or 'ltr'
  scripts: {
    primary: { file: readFileSync('Vazirmatn-Regular.ttf') },
    secondary: { file: readFileSync('Roboto-Regular.ttf') }, // optional, for bilingual composition
  },
  columns: 6,
  rows: 8,
  marginCanon: 'vdg',        // 'vdg' | 'progressive' | 'baseline' | 'vdh' | 'ateliers' | 'rosarivo' | 'grid' | literal {top,start,end,bottom}
  gutterUnits: 1,
  typeScale: { ratio: 1.333, baseSizePt: 12 },
  typeSteps: { body: 0, h2: 2, caption: -1 },
});

// grid.margins, grid.columns, grid.rows are all physical (already RTL-resolved) coordinates.
// grid.typeStyles[name] = { fontSize, lineHeight, trims: { primary: {...}, secondary?: {...} } }

For per-paragraph justified-text line-breaking (a separate concern from the page grid — see docs/RESEARCH.md §4 for why these are split):

import { breakParagraph, makeBox, makeInterwordGlue, makeKashidaGlue, distributeStretch } from 'gridmath/line-breaker';

const items = [
  makeBox(40, 'word1'), makeInterwordGlue(8, 4, 2),
  makeBox(40, 'word2'), makeKashidaGlue(15),  // an Arabic joining point that can elongate
  makeBox(40, 'word3'),
];
const { lines } = breakParagraph(items, 120); // 120pt column width

For procedural grid variation — a seeded split-grammar producing many valid layouts from one designer-authored rule-set, then resolved to physical coordinates for a given direction:

import { generateVariations } from 'gridmath/grid-grammar';
import { resolveGrammarTree, flattenPhysicalCells } from 'gridmath/axis-resolver';

const ruleSet = {
  axiom: 'PAGE',
  rules: {
    PAGE: [
      { weight: 2, split: { axis: 'inline', tracks: [{ size: '1fr', symbol: 'COL' }, { size: '1fr', symbol: 'COL' }] } },
      { weight: 1, split: { axis: 'inline', tracks: [{ size: 60, symbol: 'SIDEBAR' }, { size: '2fr', symbol: 'COL' }] } },
    ],
    COL: [{ weight: 1, terminal: 'module' }],
    SIDEBAR: [{ weight: 1, terminal: 'module' }],
  },
  maxDepth: 4,
};
const frame = { iStart: 0, iSize: 595, bStart: 0, bSize: 842 };
const variants = generateVariations(ruleSet, frame, { count: 6, seed: 7, gutter: 8 });

// each variant.tree is in logical (inline/block) coordinates — resolve for a direction:
const physicalCells = flattenPhysicalCells(resolveGrammarTree(variants[0].tree, 'rtl'));

Why this exists as its own repo

Deliberately decoupled from any one product (it started as research for an RTL-native design tool, Tarrah, but the grid math itself is generally useful — modular scales and margin canons have nothing product-specific about them). Zero-dep, MIT-licensed, human-editable: matches the "Tool Independence" principle that a library like this should outlive any one consumer.

Research

  • docs/RESEARCH.md — the full synthesis: taxonomy of 10 technique families with precise math and best reference implementations, an ADOPT/ADAPT/REJECT call per technique, the 7 explicit gaps this project had to invent (no prior art existed), and the 9-stage architecture this code implements.
  • docs/REFERENCES.md — the full 154-item raw reference list (name/URL/language/license/ maturity/algorithm) across all 12 search angles.

Honest limitations (read before shipping on top of this)

  • line-breaker.mjs's kashida-priority distribution is this project's own invention, not a port of a validated reference implementation — none exists anywhere in the 154-item survey. It encodes the documented convention (kashida preferred over widened word-spacing) but the actual per-joining-point elongation limits are caller-supplied, not derived from real font/ script data (Gap #3 in the research: no validated baseline-trim or kashida-length data exists for Arabic fonts yet).
  • villardDeHonnecourt()'s exact historical ratio has source disagreement — see the honest caveat in src/canons.mjs's docstring. Implemented as one documented, self-consistent interpretation, not a verified historical fact.
  • resolvePageMargins() requires an explicit bindingSide rather than deriving spine position from direction + page number — that derivation is a book-pagination convention, not a pure-geometry fact, and encoding it wrong silently would be worse than requiring the caller to state it (see the module's scope note).
  • No hyphenation-point flagged-pair demerit or fitness-class jump demerit in the line- breaker — both are real refinements from the original Knuth-Plass algorithm, left out of v1 to keep the first implementation reviewable; add once the core is validated further.
  • grid-grammar.mjs scores size/aspect-ratio validity only — it has no notion of whitespace-ratio or cross-cell alignment scoring (the kind of objective GRIDS' MILP solver or O'Donovan et al.'s energy function computes). Also, solveRepeat only tiles fixed-size tracks; route baseline-integer-constrained repeats through grid-modules.mjs instead — this module has no opinion about baseline snapping by design (see the module's header comment).

About

Computational, RTL-aware grid and layout mathematics. Zero runtime dependencies.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages