canvas-hypertxt
A zero-dependency library for laying out and wrapping text on an HTML5 canvas at high speed.
Repository Health
Technical Analysis
canvas-hypertxt is a featherweight, zero-dependency TypeScript library that computes line-wrapped text layout for the HTML5 Canvas API. Rather than rendering text for you, it focuses purely on the layout step — figuring out where lines should break for a given font, width, and string — and hands the resulting lines back so your own rendering pipeline can draw, align, or animate them however it needs to.
Its headline feature is “hyper wrapping”: once a font has accumulated enough measured samples, the library trains a lightweight per-character width model and stops calling the comparatively expensive ctx.measureText on every guess, instead estimating line breaks from the trained weights. This trades a small, bounded accuracy margin for dramatic speedups on repeated wraps of the same font — the library’s own benchmarks show it running many times faster than comparable canvas text libraries at longer string lengths, and even faster again once hyper wrapping kicks in.
canvas-hypertxt was extracted from Glide’s own glide-data-grid project, where it handles cell text wrapping at scale, and is now published as a standalone package for anyone building canvas-based grids, editors, or custom text renderers who needs fast, dependency-free line-break calculation.
What You Get
- A single
split()function that returns wrapped lines for a given canvas context, string, font, and width - Optional break-opportunity callback support for wrapping non-space-delimited text (e.g. via the
linebreakpackage for CJK or hyphenated content) - A
clearCache()function to reset internal measurement caches, useful after fonts finish loading and metrics change - “Hyper wrapping” mode that trains a per-character width model per font and skips
ctx.measureTextcalls once enough samples are collected, for a large performance gain on repeated wraps - Zero runtime dependencies and dual CJS/ESM builds with bundled TypeScript type declarations
Common Use Cases
- Canvas-based data grids - wrapping cell text to a fixed column width without relying on DOM/CSS text layout, as used internally by glide-data-grid
- Custom canvas text editors or WYSIWYG surfaces - computing line breaks so the app can control exactly how and where text is rendered, including custom alignment
- Game UI and HUD text rendering - laying out dialogue or label text on a canvas at high frame rates where DOM-based wrapping would be too slow
- Chart and visualization labels - wrapping axis labels, tooltips, or annotations drawn directly to canvas rather than as HTML overlays
Under The Hood
Architecture
The library is structured around two files: src/index.ts, a thin re-export barrel exposing split and clearCache, and src/multi-line.ts, which holds all core logic. State lives in module-level Maps — a memoized result cache keyed by text+font+width, a per-font average-character-width table, and per-font hyper-wrapping weight maps — making the library a singleton with implicit shared state rather than an injectable instance. The single entry point checks the cache, splits input on newlines, and for overflowing lines repeatedly computes a split point by guessing an index from a width ratio, then walking forward or backward while re-measuring until the guess fits, finally snapping to the nearest word boundary or a caller-supplied break-opportunity list. There is no abstraction over CanvasRenderingContext2D — it is passed directly into nearly every function — so the library is tightly coupled to that one browser API surface.
Tech Stack
canvas-hypertxt ships with zero runtime dependencies, matching its “featherweight” claim. It is authored in strict TypeScript targeting ES6, built with esbuild into both a minified CommonJS bundle and an ESM bundle, with a separate tsc pass emitting type declarations, all wired together through the package’s exports map for dual CJS/ESM/type resolution. Development tooling includes Jest 28 with ts-jest and jest-environment-jsdom plus jest-canvas-mock to exercise canvas APIs under Node, Storybook 6 for benchmark demos, and an extensive ESLint configuration (typescript-eslint, react, import, sonarjs, unicorn). It has no server, database, or framework dependency — it is a leaf utility meant to be embedded in larger canvas-rendering projects such as Glide’s own glide-data-grid.
Code Quality
Testing lives in a single file, test/multi-line.test.tsx, covering short strings, long word-wrapped strings, custom break-opportunity callbacks, zero-width edge cases, explicit newlines, and a dedicated hyper-wrapping test that runs one million iterations to force the trained fast path — thorough for the public entry point, though internal helpers like the weight-adjustment function and the split-point search are exercised only indirectly. Error handling favors defensive early returns (an empty array for non-positive widths, a sentinel value when a line already fits) over thrown exceptions, with no explicit input validation. Types are strict (strict: true, noImplicitAny) with readonly return types signaling immutability intent, and naming is clear and domain-specific throughout.
API Design
The public surface is deliberately minimal — two functions, split and clearCache — letting a consumer get a working canvas-wrapping call running in under ten lines, as the README demonstrates. This narrowness is itself the library’s design bet: unlike libraries that also render text for you, canvas-hypertxt returns only line breaks and leaves drawing, alignment, and positioning to the caller, trading convenience for composability. Its standout technical idea, “hyper wrapping,” switches a font from per-guess measureText calls to a self-trained per-character width model once enough samples accumulate, estimating breaks with near-zero marginal measurement cost at a small, bounded accuracy trade-off — a genuinely narrow but effective optimization for this specific problem. The README also explicitly documents what is out of scope (text justification, debug rendering, managed line-height handling), which communicates the API boundary clearly rather than leaving gaps implicit.