Docs · Web
Font controls on the web
A slider on a web page that changes a word's font while you drag it, then a font your users can keep. Each step adds a few lines to the same small page.
Add font controls explains the ideas behind this page: few controls, outlines while dragging, the newest change wins, and a recipe to store. Here they are in JavaScript.
1. A slider and somewhere to draw
The page needs a slider, and an SVG image for the letters. SVG suits outlines: the engine gives each letter as SVG path data.
<input id="weight" type="range">
<svg id="preview" height="120"></svg>
<h1 id="heading">Acme</h1>
<button id="download">Download the font</button>2. Load the engine when it's needed
The engine is 2.5 MB gzipped. Load it when someone opens your font tool rather than with your first page, and import the package with import(), so it stays out of your main bundle. The worker entry runs the engine in a worker: a background thread, so the page never waits while it works.
const { createEngine } = await import("@typomat/engine/worker");
const engine = await createEngine({ licenseKey: "TM2..." });
// The slider's range and starting point, from the engine rather than from your code.
const slider = document.querySelector("#weight");
const weight = engine.catalog.axes.find((axis) => axis.id === "weight");
slider.min = weight.min;
slider.max = weight.max;
slider.value = 118;In the worker, every method returns a promise. catalog, license and presets are ready at once. If your bundler can't follow the worker, the JavaScript SDK reference shows how to serve it yourself.
3. Draw the word while the slider moves
Outlines take about a millisecond a letter, so they can follow the slider. latest() makes a lane where a new call replaces the last one still waiting: the replaced call resolves with null, so the drawing never falls behind. fitted: false skips the curve fitting, for speed.
const svg = document.querySelector("#preview");
const preview = engine.latest();
async function drawPreview() {
const run = await preview.glyphs({ weight: slider.valueAsNumber }, "Acme", { fitted: false });
if (!run) return; // a newer move took its place
// Font units, 1000 to the em, with y pointing down from the baseline.
let x = 0;
const paths = run.glyphs.map((glyph) => {
const path = glyph.d ? `<path transform="translate(${x} 0)" d="${glyph.d}"/>` : "";
x += (glyph.advance ?? 0) + (glyph.kern ?? 0);
return path;
});
const { lineTop, lineBottom } = run.metrics;
svg.setAttribute("viewBox", `0 ${-lineTop} ${x} ${lineTop - lineBottom}`);
svg.innerHTML = paths.join("");
}
slider.addEventListener("input", drawPreview);
drawPreview();What you should see: the word Acme, getting heavier and lighter as you drag.
4. Set real text in the font
To set real text, such as the heading, compile a font of only the characters on screen. That's far quicker than all 271. A second lane keeps it apart from the drawing, and change fires when someone lets go of the slider.
const heading = document.querySelector("#heading");
const compiler = engine.latest();
let face;
async function setHeading() {
const font = await compiler.compile(
{ weight: slider.valueAsNumber },
{ name: "Preview", characters: heading.textContent, format: "woff2" },
);
if (!font) return;
if (face) document.fonts.delete(face);
face = new FontFace("Preview", font);
document.fonts.add(await face.load());
heading.style.fontFamily = "Preview";
}
slider.addEventListener("change", setHeading);
setHeading();The heaviest named styles take far longer than the rest, up to about 3 s, so keep showing the outlines while they finish.
5. Save a recipe, not the file
A recipe is the font's source, about a kilobyte of JSON, and it compiles to the same bytes whenever you ask. Store it on the customer's record, here with a request to a server of yours, and compile when you need the file.
const recipe = await engine.recipe({ weight: slider.valueAsNumber }, { name: "Acme" });
// { format: 1, name: "Acme", parameters: { weight: 118 } }
await fetch("/your/save/endpoint", { method: "POST", body: JSON.stringify(recipe) });
// Later, anywhere the SDK runs, the same font:
const font = await engine.compileRecipe(recipe, { format: "woff2" });6. Let them download it
compile returns a complete file, so a download is one Blob away. People install TTF or OTF on their computers, and websites use WOFF2. Fonts your users make are theirs to use commercially: see Licensing.
document.querySelector("#download").addEventListener("click", async () => {
const ttf = await engine.compile({ weight: slider.valueAsNumber }, { name: "Acme", format: "ttf" });
const link = document.createElement("a");
link.href = URL.createObjectURL(new Blob([ttf], { type: "font/ttf" }));
link.download = "acme.ttf";
link.click();
});