Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages