Docs · Web

JavaScript SDK

@typomat/engine is the Typomat engine for JavaScript: one WebAssembly file and a small layer around it, with TypeScript types. It runs in modern browsers and in Node 20 or newer, with no dependencies.

This page lists everything the package does. If you haven't installed it yet, the web quickstart does that in a few minutes. Ideas the two SDKs share, such as recipes and character sets, are explained once in SDK concepts.

Three ways to import it

The package has three entry points. Most projects only need the first, or the second for a page with sliders.

ImportWhat it is
@typomat/engineThe engine on the thread that calls it. Every method answers straight away. It finds engine.wasm in the package by itself.
@typomat/engine/workerThe same methods in a worker, a background thread, each returning a promise. For pages that must stay smooth: see Workers.
@typomat/engine/coreThe same as the first, with no default location for engine.wasm, for platforms where you always pass it.

Its only network request is for its own engine.wasm. Types come with it, so your editor knows every parameter, option and result.

createEngine(options)

Loads the engine and gives it back. Do this once, and keep the engine: loading takes a moment, and everything after that is fast.

JavaScript
import { createEngine } from "@typomat/engine";

const engine = await createEngine({
  licenseKey: "TM2...",   // from your account. Older TM1 keys work too
  wasm: undefined,        // where to find engine.wasm, or its bytes; defaults to the package's own
  quiet: false,           // true: no note in the console about evaluation mode or a lapsed licence
});
OptionMeaning
licenseKeyYour licence key. Without one that fits, the engine runs in evaluation mode. It isn't a secret, so it can sit in front-end code.
wasmWhere engine.wasm is: a URL, its bytes, a WebAssembly.Module or a Response, or a promise of one. Leave it out to use the package's own.
quiettrue stops the one note the engine prints about evaluation mode or a lapsed licence.

Vite and webpack find engine.wasm by themselves. Serve it yourself, for example from your CDN, when your bundler doesn't, and send it as application/wasm so that it compiles while it downloads. It's 8 MB: 2.5 MB gzipped, and 2 MB with Brotli.

JavaScript
const engine = await createEngine({ licenseKey: "TM2...", wasm: "/typomat/engine.wasm" });

engine.compile(parameters, options)

Makes a whole font and gives back the file's bytes, as a Uint8Array. parameters are the numbers that shape the letters. Every one is optional: anything you leave out stays at its default, and values outside a range are clamped to it. An unknown name throws, so a typo can't silently do nothing. See all the parameters.

JavaScript
const font = engine.compile(
  { weight: 118, contrast: 0.92 },
  { name: "Acme Display", format: "woff2", sets: ["lowercase", "digits"] },
);
OptionMeaning
nameThe family name written into the font, up to 64 characters. "Typomat Font" if you leave it out.
format"ttf" by default, or "otf", "woff" or "woff2".
charactersOnly these characters, as text, a list or a Set. A much smaller file.
setsNamed groups of characters, such as lowercase or digits: the list.
rangesCode points, as a list of [first, last] pairs, both included.
weightClassWhich member of the recipe's family to compile, from 100 to 900.
metadataWhat the font says about itself: credits, licence text, vendor ID, version and embedding rights. See Font output.

characters, sets and ranges combine: a font holds whatever any of them asks for. Leave all three out for all 271. What the engine can't draw is left out without complaint, and coverage tells you what that was.

JavaScript
engine.compile(parameters, { characters: "Acme" });
engine.compile(parameters, { sets: ["lowercase", "digits"] });
engine.compile(parameters, { ranges: [[0x41, 0x5a]] });   // A to Z

parameters can be a whole recipe as well. engine.compileRecipe(recipe, options) does the same, and says so in your code.

Using a font straight away

A compiled font is an ordinary font file, so it goes wherever a font file goes. Nothing needs installing.

JavaScript
// In a page: the text changes to the new font.
const face = new FontFace("Acme Display", font);
document.fonts.add(await face.load());
document.body.style.fontFamily = "Acme Display";

// As a download.
const url = URL.createObjectURL(new Blob([font], { type: "font/ttf" }));

// In Node: a file.
await writeFile("acme-display.ttf", font);

engine.coverage(options)

What a font would hold for a choice of characters, and what the engine can't draw, without compiling anything. It takes the same characters, sets and ranges as compile, and the font's parameters if you give them, since a monospaced font has more to its all.

JavaScript
engine.coverage({ characters: "Aé€Ж" });
// { included: [" ", "A", "é", "€"], unavailable: ["Ж"], sets: { ... } }

included is what the font would hold, the space included. unavailable is what you asked for and the engine can't draw: 256 at most, lowest first. In evaluation mode, withheld lists what it could draw but holds back.

Recipes and families

A recipe is a font's source, about a kilobyte of JSON: store it rather than the file, and compile it again whenever you need the file. SDK concepts explains what's in one. In JavaScript:

JavaScript
const recipe = engine.recipe({ weight: 118 }, { name: "Acme", weights: [{ weightClass: 700 }] });
// { format: 1, name: "Acme", parameters: { weight: 118 }, weights: [{ nudge: 0, weightClass: 700 }] }

engine.compileRecipe(recipe, { format: "woff2" });
engine.family(recipe).members;   // [{ weightClass: 500, style: "Medium" }, { weightClass: 700, style: "Bold" }]
engine.compileRecipe(recipe, { weightClass: 700 });
MethodWhat it gives
engine.recipe(parameters, options)A complete, checked recipe to store. Options: name, metadata, weights, and the hand edits pulls, points, letters and letterStyles.
engine.compileRecipe(recipe, options)The font file, with the same options as compile.
engine.family(recipe){ family, members }: the fonts the recipe's weights make, lightest first.
engine.resolve(parameters){ recipe, hash }: the recipe spelled out and clamped, and sixteen hex digits naming its letters.

compile, glyphs, outline and metrics all take a recipe as well as parameters. The hash doesn't cover the name, metadata, format or characters, so it isn't a key for a file's bytes.

engine.glyphs(parameters, text, options)

The outlines for a line of text, for drawing on a canvas or in SVG without installing a font.

JavaScript
const { metrics, glyphs } = engine.glyphs({ weight: 170 }, "Acme");

// { c: "A", advance: 815.94, kern: -44, d: "M 309.08 -703.13 L ..." }
// { c: "Ж", missing: true }  when the engine can't draw a character
// { c: "7", missing: true, withheld: true }  when it can, but not in evaluation mode

Each d is SVG path data in font units (1000 to the em), with y pointing down and the baseline at 0. Move along by advance + kern after each glyph. metrics has the line's lineTop and lineBottom. By default the outlines go through the font's own curve fitter, so they match the compiled font. Pass { fitted: false } for faster, polygonal outlines in a small live preview. Text can be up to 16,384 bytes.

engine.outline(parameters, character, options)

One glyph, with the touch points of its anatomy. The Typomat app uses these to put its dials on a letter, and you could too. d is SVG path data with y pointing down, like glyphs. anchors maps each parameter to an [x, y] point, with y pointing up, as in a font file.

JavaScript
const { d, advance, anchors } = engine.outline({ weight: 118 }, "a");

steps sets how finely curves are cut into lines, from 2 to 64: 24 by default. fitted is false by default here, for raw polygons, and true runs the curve fitter. A character it can't draw comes back with missing: true.

engine.metrics(parameters)

The font's vertical measurements, in font units: ascender, descender, capHeight, lineTop, lineBottom and fitWidth. And lines: a few sample sentences that suit the font's character.

JavaScript
const { ascender, capHeight, lines } = engine.metrics({ weight: 118 });
// 740, 700, ["Sphinx of black quartz, judge my vow.", ...]

engine.catalog

Everything this engine can do, from the engine itself, so your interface never carries a copy that goes stale. SDK concepts says what each part is for.

JavaScript
engine.catalog.axes;           // [{ id: "weight", label: "weight", group: "stroke", min: 12, max: 210, default: 86, tier: "core" }, ...]
engine.catalog.groups;         // ["form", "stroke", "ends", "flourish", "carve", "style"]
engine.catalog.characters;     // ["a", "b", "c", ...]
engine.catalog.characterSets;  // { lowercase: [...], digits: [...], ... }
engine.catalog.forms;          // which parameter swaps a letter for its other form
engine.catalog.weightClasses;  // [100, 200, ..., 900]
engine.catalog.engine;         // "1.7.1"

engine.presets and engine.explore(seed)

Two ways to a starting point. engine.presets holds the 34 named styles, from Didone to Blackletter, as { name, beta, parameters }. engine.explore(seed) finds a font from a number, as { recipe, hash }, and the same seed always finds the same font.

JavaScript
engine.presets;   // [{ name: "Geometric Sans", beta: false, parameters: { ... } }, ...]

const font = engine.compile(engine.presets[0].parameters, { name: engine.presets[0].name });
const found = engine.explore(42).recipe;

engine.license

What the engine made of your key. mode is "licensed", "lapsed" or "evaluation", and reason says why when it isn't licensed. A key that checked out also gives its licensee, plan, domains, apps, expires and updatesUntil.

JavaScript
engine.license;
// { mode: "licensed", valid: true, licensee: "Acme AG", plan: "standard", domains: [], expires: "2027-11-18", ... }
// { mode: "evaluation", valid: false, reason: "missing" }

A reason of "domain" means the page isn't on a domain your key names. Licensing has the rules, and nothing about a licence ever throws.

Workers: keeping a page smooth

Everything above answers straight away, on the thread that calls it. An outline takes about a millisecond in most styles, but a whole font about 315 ms, which is long enough to make a page stutter while someone drags a slider. A worker is a background thread the browser gives a page. @typomat/engine/worker puts the engine in one, with the same methods, each returning a promise. Calls run one at a time, in order.

JavaScript
import { createEngine } from "@typomat/engine/worker";

const engine = await createEngine({ licenseKey });
const font = await engine.compile({ weight: 118 });

// For a slider: a new call replaces the last one, which resolves with null.
const dial = engine.latest();

slider.oninput = async () => {
  const font = await dial.compile({ weight: slider.valueAsNumber }, { characters: "Acme" });
  if (font) show(font);
};

latest() gives you a lane where the newest call wins. A call that's been replaced resolves with null, so check for it. cancel() on the lane drops whatever it has waiting. A compile can't be stopped halfway: the worker finishes it, and the answer is dropped. license, catalog and presets are ready as soon as the engine is, with no promise.

Vite and webpack pick up the worker and the WebAssembly by themselves. If your bundler doesn't (esbuild, for one), copy engine.worker.classic.js and engine.wasm from node_modules/@typomat/engine to your site, and say where they are:

JavaScript
const engine = await createEngine({
  licenseKey,
  worker: new Worker("/typomat/engine.worker.classic.js"),
  wasm: "/typomat/engine.wasm",
});

Here wasm takes a URL, bytes or a WebAssembly.Module, not a Response. close() ends the worker, and anything still waiting rejects with the code closed. Font controls on the web puts it together.

Errors

Everything throws a TypomatError, which the package exports, with a .code to check and a message to read. In a worker, the promise rejects with one.

JavaScript
import { TypomatError } from "@typomat/engine";

try {
  engine.compile({ wieght: 118 });
} catch (error) {
  if (error instanceof TypomatError && error.code === "unknown_parameter") console.log(error.message);   // There's no parameter called "wieght".
}
CodeMeaning
unknown_parameterA parameter's name doesn't exist, so a typo can't silently do nothing.
unknown_fieldAn option or a recipe field doesn't exist.
invalid_requestA field is the wrong type or out of bounds. The message says which.
unknown_formatA format that isn't ttf, otf, woff or woff2.
unknown_setA character set that doesn't exist.
newer_formatA recipe from a newer engine than this one.
unknown_methodcall() was given a method this engine doesn't have.
internalA bug in the engine. Please tell us what you sent it.
load_failedengine.wasm couldn't be loaded: the message says why.
abi_mismatchengine.wasm and the JavaScript around it come from different versions of the package.
engine_crashedThe engine stopped on an input, and restarted itself. Try again, and please tell us what you sent it.
worker_failedThe worker didn't start. Usually a bundler that can't follow it: see Workers below.
closedThe engine was closed with close().

engine.close()

Gives the engine's memory back, or ends the worker. The engine can't be used after this, and a call throws with the code closed.

engine.call(method, body)

Any engine method by name, with a JSON body: JSON back, or bytes for "compile". You won't need it unless the engine gains a method before this package has a name for it.