Skip to content

insomni-msdf-text

insomni-msdf-text turns TTF/OTF font files into a GPU multi-channel signed distance field (MSDF) atlas, glyph by glyph. It owns three responsibilities:

  1. Font loading — parse a .ttf / .otf via opentype.js and expose glyph outlines in em-relative coordinates (Font, loadFont).
  2. Atlas management — lazily generate one MSDF per (font, codepoint) into a single rgba8unorm GPU texture, with a shelf packer (MsdfGlyphAtlas).
  3. MSDF generation — the compute-shader pipeline and the CPU prep that feeds it (MsdfGenerator, prepareGlyphSegments, and friends).

All glyph metrics live in em-relative units (1 em = unitsPerEm font units). One MSDF entry per codepoint is reused at every render size — the shader scales the distance field at draw time. Coordinates are y-up (font files are y-down; the loader flips Y at parse time).

loadFont accepts a URL, an ArrayBuffer, or a Uint8Array and resolves to a Font.

import { loadFont } from "insomni-msdf-text";
// From a URL.
const font = await loadFont("/fonts/Inter.ttf");
// From bytes (e.g. a fetched / base64-decoded buffer).
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
const font2 = await loadFont(bytes, { family: "Inter" });
ParamTypeNotes
sourceFontSource = string | ArrayBuffer | Uint8ArrayURL is fetched; a Uint8Array is copied into a fresh ArrayBuffer.
options.familystringOptional human-readable family label. Defaults to the font’s own family name.

Returns Promise<Font>. Throws if a URL fetch fails.

A parsed font with cached, em-relative glyph outlines keyed by codepoint.

MemberTypeDescription
familystringFamily label (from options.family or the font’s own name).
unitsPerEmnumberFont design units per em.
ascendernumberAscender, em units.
descendernumberDescender, em units (negative).
lineGapnumberLine gap from the hhea table, em units.
getOutline(codepoint)GlyphOutline | nullEm-relative outline (y-up). null if the font has no glyph. Whitespace returns zero contours but a real advance. Cached.
getKerning(left, right)numberHorizontal kerning between two codepoints, em units. 0 when the font has no kerning data for the pair.

You can also construct Font directly from an opentype.Font via new Font(otf, options?) if you have already parsed the bytes yourself.

MsdfGlyphAtlas owns the GPU texture and generates each glyph’s MSDF on first request. One atlas corresponds to one Font — use multiple atlases for multiple fonts.

import { loadFont, MsdfGlyphAtlas } from "insomni-msdf-text";
const font = await loadFont("/fonts/Inter.ttf");
const atlas = new MsdfGlyphAtlas({
device, // a GPUDevice
font,
atlasSize: 2048, // square texture side, texels (default 2048)
atlasFontSize: 64, // em → texels base resolution (default 64)
pxRange: 8, // distance range in texels (default 8)
});
// Warm the atlas with the glyphs you expect to draw.
atlas.preload("Hello, world! 0123456789");
// Look one up.
const g = atlas.getGlyph("H".codePointAt(0)!);
if (g) {
// g.uvMinX..uvMaxY index into atlas.textureView (rgba8unorm);
// g.advance / bearingX / bearingY / width / height are em-relative.
}
// Sample atlas.texture / atlas.textureView from your own pipeline.
OptionTypeDefaultDescription
deviceGPUDeviceRequired. The device that owns the texture and compute pipeline.
fontFontRequired. The font this atlas rasterizes.
atlasSizenumber2048Side length of the square rgba8unorm texture.
atlasFontSizenumber64Em size in atlas texels — base rasterization resolution. Larger = sharper at high zoom, more atlas pressure.
pxRangenumber8Distance range in atlas texels. Wider = thicker outlines / softer shadows, lower precision.
labelstringmsdf-atlas:<family>GPU texture label.
MemberTypeDescription
textureGPUTextureThe rgba8unorm atlas texture (usage includes TEXTURE_BINDING, STORAGE_BINDING, COPY_DST).
textureViewGPUTextureViewDefault view of texture.
font / atlasSize / atlasFontSize / pxRangeThe resolved config values.
getGlyph(codepoint)MsdfGlyph | nullReturns the entry, generating it on first request. null if the font lacks the glyph or the atlas is full (a warning is logged once). Whitespace returns an entry with zero w/h but the correct advance.
getGlyphById(id)MsdfGlyph | undefinedFetch by stable atlasGlyphId (allocation order).
preload(text)voidPre-generate every glyph in a string — useful for warming the atlas at startup.
glyphCountnumberNumber of glyphs allocated so far.
versionnumberMonotonic counter bumped on each new allocation. Compare against a stored value to detect new entries (e.g. to sync a parallel GPU metrics buffer).
fillPercentnumberApproximate atlas fill, 0–100, from the shelf cursor.
destroyedbooleanWhether destroy() has run.
destroy()voidDestroys the GPU texture + generator and clears caches.

The per-glyph record returned by getGlyph / getGlyphById. UVs are normalized [0,1]; the metric fields are em-relative.

FieldTypeDescription
codepointnumberThe Unicode codepoint.
atlasGlyphIdnumberStable sequential id (allocation order). Use as the index into a parallel GPU metrics buffer.
uvMinX / uvMinY / uvMaxX / uvMaxYnumberNormalized UV bounds of the glyph region (inset by half a texel to avoid cross-region bleed).
pxRangenumberDistance range used at generation, in MSDF texel units.
advancenumberHorizontal advance, em units.
bearingX / bearingYnumberLeft / top bearing, em units.
width / heightnumberPadded glyph box size, em units.

Font.getOutline returns a GlyphOutline built from these shared shapes (exported as types). These are the CPU-side representation the MSDF generator consumes.

TypeShapeNotes
Vec2{ x, y }2D point, y-up in em space.
Edge{ kind: "line"; p0; p1 } | { kind: "quad"; p0; p1; p2 } | { kind: "cubic"; p0; p1; p2; p3 }One contour edge. Quads/cubics carry control points.
Contour{ edges: Edge[]; windingSign: 1 | -1 }A closed sequence of edges; +1 outer (CCW in y-up), -1 hole.
GlyphOutline{ codepoint; contours: Contour[]; advance; bbox }contours is empty for whitespace. bbox is the tight ink box, em units.

MsdfGlyphAtlas drives generation for you, but the lower-level pieces are exported for custom packers or offline tooling. The two-step flow is: prep the CPU segment buffer with prepareGlyphSegments, then dispatch the compute shader with a MsdfGenerator.

import { MsdfGenerator, prepareGlyphSegments, type GenerateRegion } from "insomni-msdf-text";
const generator = new MsdfGenerator(device, atlasTextureView);
const outline = font.getOutline("g".codePointAt(0)!)!;
const { data, count } = prepareGlyphSegments(outline);
const region: GenerateRegion = {
originX: 0,
originY: 0,
width: 96,
height: 96,
emMin: { x: outline.bbox.minX, y: outline.bbox.minY },
emMax: { x: outline.bbox.maxX, y: outline.bbox.maxY },
range: 8 / 64, // pxRange / atlasFontSize, in em units
};
generator.generate(data, count, region); // writes into the atlas texture
ExportKindDescription
MsdfGeneratorclassOwns the WGSL compute pipeline and a reusable, power-of-2-grown segment storage buffer. new MsdfGenerator(device, atlasView); generate(segments, count, region); destroy().
prepareGlyphSegments(outline)fnFull CPU prep: outline → flattened, edge-colored, packed { data: Float32Array, count }. Filters degenerate contours and normalizes winding.
packSegments(contours)fnLower-level packer: colored-segment lists → { data, count }.
GenerateRegiontypeThe atlas region + em-space bounds + distance range (em units) for one dispatch.
SEGMENT_FLOATSconst8 — floats per segment in the GPU storage buffer (32 bytes).
SEGMENT_FLAG_LINEconst1 << 0 — segment is a line (vs. a quadratic Bézier).
SEGMENT_FLAG_HOLEconst1 << 1 — parent contour is a hole (winding -1).
MSDF_WGSLconstThe raw WGSL compute-shader source string.
  • Text — how insomni/text-ttf consumes this atlas and the shared font/atlas interfaces the renderer’s shaper expects.