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.
| Import | What it is |
|---|---|
@typomat/engine | The engine on the thread that calls it. Every method answers straight away. It finds engine.wasm in the package by itself. |
@typomat/engine/worker | The same methods in a worker, a background thread, each returning a promise. For pages that must stay smooth: see Workers. |
@typomat/engine/core | The 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.
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
});| Option | Meaning |
|---|---|
licenseKey | Your 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. |
wasm | Where 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. |
quiet | true 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.
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.
const font = engine.compile(
{ weight: 118, contrast: 0.92 },
{ name: "Acme Display", format: "woff2", sets: ["lowercase", "digits"] },
);| Option | Meaning |
|---|---|
name | The 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". |
characters | Only these characters, as text, a list or a Set. A much smaller file. |
sets | Named groups of characters, such as lowercase or digits: the list. |
ranges | Code points, as a list of [first, last] pairs, both included. |
weightClass | Which member of the recipe's family to compile, from 100 to 900. |
metadata | What 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.
engine.compile(parameters, { characters: "Acme" });
engine.compile(parameters, { sets: ["lowercase", "digits"] });
engine.compile(parameters, { ranges: [[0x41, 0x5a]] }); // A to Zparameters 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.
// 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.
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:
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 });| Method | What 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.
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 modeEach 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.
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.
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.
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.
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.
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.
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:
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.
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".
}| Code | Meaning |
|---|---|
unknown_parameter | A parameter's name doesn't exist, so a typo can't silently do nothing. |
unknown_field | An option or a recipe field doesn't exist. |
invalid_request | A field is the wrong type or out of bounds. The message says which. |
unknown_format | A format that isn't ttf, otf, woff or woff2. |
unknown_set | A character set that doesn't exist. |
newer_format | A recipe from a newer engine than this one. |
unknown_method | call() was given a method this engine doesn't have. |
internal | A bug in the engine. Please tell us what you sent it. |
load_failed | engine.wasm couldn't be loaded: the message says why. |
abi_mismatch | engine.wasm and the JavaScript around it come from different versions of the package. |
engine_crashed | The engine stopped on an input, and restarted itself. Try again, and please tell us what you sent it. |
worker_failed | The worker didn't start. Usually a bundler that can't follow it: see Workers below. |
closed | The 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.