# tiny-sdf: signed distance fields for glyphs, generated in the browser

> TinySDF is a small JavaScript library that rasterises a single character with Canvas 2D and converts it into a signed distance field using the Felzenszwalb and Huttenlocher distance transform, so that text can be drawn with WebGL. It ships as an ES module and has no runtime dependencies.

**mapbox/tiny-sdf** — Browser-side SDF font generator

- Repository: https://github.com/mapbox/tiny-sdf
- Website: http://mapbox.github.io/tiny-sdf/
- Stars: 676 · Forks: 66
- Language: JavaScript
- License: BSD-2-Clause
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/mapbox-tiny-sdf

## What the library produces, and why WebGL wants it

TinySDF generates a signed distance field for a single glyph, and the README frames the purpose in one line: it is very useful for rendering text with WebGL. The problem it solves is a rendering one. A canvas or WebGL pipeline that draws text as coloured quads needs a description of each glyph that is independent of the font file at draw time, and a distance field is that description. Instead of storing an outline, it stores, for every pixel of a small grid, how far that pixel is from the edge of the shape, which is a single number per pixel and which lets a shader draw crisp edges, outlines, glows and halos at any size from one texture. The mechanism is straightforward and worth understanding in two steps. The character is first rasterised with Canvas 2D using the font parameters you supply, producing an ordinary bitmap of the glyph with a whitespace buffer around it, and the bitmap is then run through the Felzenszwalb and Huttenlocher distance transform, the classic linear time algorithm for computing an exact euclidean distance field, which converts the binary shape into per pixel distances. The result is then encoded into alpha values, so the field travels as an ordinary texture.

## buffer, radius and cutoff decide the texture you get

Three of the constructor parameters are the ones that determine the quality and the cost of the result, and the README comments are precise about each. The buffer is the whitespace buffer around a glyph in pixels, so it is the margin of background around the shape inside the returned bitmap, and it is where the distance field has room to grow. The radius is how many pixels around the glyph shape are used for encoding the distance, which is the resolution of the field: a larger radius means distances are recorded more finely and the shader can place the edge more precisely, at the cost of a larger texture and more work per glyph. The cutoff is how much of the radius, relatively, is used for the inside part of the glyph, so it is the threshold that decides where the shape interior ends and the outside distance ramp begins. The other three parameters are ordinary font properties, the font size in pixels, the CSS font family and the CSS font weight and style, and they are handed to Canvas 2D for the rasterisation step. The relationship between font size and radius is the practical decision. The radius is measured in pixels, so a glyph rasterised at 24 pixels with a radius of 8 encodes distances finely relative to its size, while the same radius on a small glyph says little about the shape, and a large radius on a big glyph costs a bigger texture for every character in the string. The buffer is the other half of the budget, since it adds background pixels around every glyph in the returned bitmap, and a field with no room outside the shape cannot represent a glow or an outline in the shader.

## The returned object, and the glyphLeft value to plan around

The object returned by a draw call carries the field and the metrics a text layout engine needs, and the README lists all of it. The data property is a Uint8ClampedArray of alpha values from 0 to 255 covering a width by height grid, which is exactly the shape a WebGL texture upload wants. The width and height give the size of that bitmap. Then come the glyph metrics: glyphTop is the maximum ascent of the glyph from the alphabetic baseline, glyphWidth and glyphHeight are the width and height of the rasterised portion of the glyph, and glyphAdvance is the layout advance. One of them deserves a warning, because it is a design decision rather than an oversight. The README says glyphLeft is currently hardwired to 0, and that the actual glyph differences are encoded in the rasterisation. In other words, the horizontal offset of the shape is not reported separately, it is baked into the pixels. If your layout code computes positions from these metrics, you need to account for that, and it is the kind of detail that shows up as a subtle one pixel drift in a line of text rather than as an error.

## ES module only, in the browser and in Node

TinySDF is provided as an ES module, so the README states it is only supported on modern browsers, excluding IE. In a browser the module is loaded from a CDN inside a script tag with the module type.

```html
<script type="module">
import TinySDF from 'https://cdn.skypack.dev/@mapbox/tiny-sdf';
...
</script>
```

In Node the README is equally direct: you cannot use require, only import, and only in ESM capable versions, which it puts at v12.15 and later.

```js
import TinySDF from '@mapbox/tiny-sdf';
```

For a first use, the shape of the code is short. You construct one TinySDF with the font parameters, then call draw with a character and use the result.

```js
const tinySdf = new TinySDF({
    fontSize: 24,             // Font size in pixels
    fontFamily: 'sans-serif', // CSS font-family
    fontWeight: 'normal',     // CSS font-weight
    fontStyle: 'normal',      // CSS font-style
    buffer: 3,                // Whitespace buffer around a glyph in pixels
    radius: 8,                // How many pixels around the glyph shape to use for encoding distance
    cutoff: 0.25              // How much of the radius (relative) is used for the inside part of the glyph
});
```

The README's own example then draws a single CJK character, which is a deliberate choice: that is where a naive word wrap or a single font assumption breaks down and where a distance field earns its keep.

## The paper's algorithm, not the paper's C++ port

The licence section carries a sentence that tells you what kind of code this is. The implementation is based directly on the algorithm published in the Felzenszwalb and Huttenlocher paper, and is not a port of the existing C++ implementation provided by the paper's authors. That distinction is a licence and maintenance statement in one. The distance transform is a known, published method, so a reimplementation in JavaScript can be distributed under a permissive licence, BSD 2-Clause here, without inheriting anything from the authors' code. It also means the algorithm is a fixed reference rather than a moving target, and the correctness of the output can be checked against the paper. The repository itself is small and built for that kind of tight scope. The published package contains the index.js implementation and the index.d.ts typings, and the repository adds an index.html demo page, a bench.js benchmark script, an eslint configuration, and a test directory. The development dependencies reflect the same minimalism: the canvas package for a Node side rasteriser, pixelmatch and pngjs for image comparison in the tests, and st for the demo server.

## Runtime generation against a pre-baked font atlas

The design choice that defines tiny-sdf is when the glyph data is produced. The alternative approach is the one the Mapbox write-up on rendering text with WebGL is about, where glyphs are prepared ahead of time into a texture atlas, usually as part of a font pipeline, and the renderer draws from that atlas. The difference is not visual quality in principle, it is where the work happens and what you have to ship. With tiny-sdf, the rasterisation and the distance transform happen in the browser, on the device, from whatever system font is available, which means no font atlas, no atlas build step and no bundling of font binaries, and it means the text follows the user's own font settings. The cost of that is a per glyph cost at runtime, which is why a typical application generates a glyph once and caches it, and the reason the radius and buffer parameters matter so much, since they determine how much work each generated glyph is. The other cost is consistency, because two devices with different system fonts produce different glyph shapes from the same call, so if you need pixel identical rendering everywhere you want the pre-baked atlas instead, and if you need a specific typeface you ship it and rasterise from it.

## A small package with a slow enough release rhythm

TinySDF is one of those projects where the size of the code is a feature, and the release history reflects that. The three listed releases are v2.1.0 on 2026-04-08, v2.2.0 on 2026-05-05 and v2.3.0 on 2026-10-08, and the last push to the repository was on 2026-10-08, the same day as the newest tag. The version is at 2.x, so the API has settled enough to have had a major bump, but the interval between releases is months rather than days, which is what you would expect from a library whose entire surface is one constructor and one method. The package declares no runtime dependencies at all, the dependency list in the manifest is entirely devDependencies for linting, testing and the demo server, and the scripts are minimal: the test script runs node --test, the bench script runs the benchmark file, and the start script serves the demo page with caching disabled. The licence is BSD 2-Clause, which permits use, modification and redistribution in both commercial and closed source work, and the author credit in the manifest points to Vladimir Agafonkin, with the code of conduct and ownership files in the repository. For a project this size, the practical risk is not churn but stagnation, so if you depend on it, the thing to check is whether a change to Canvas 2D behaviour or to the ES module target in your toolchain has moved underneath it, since the browser side of the library is exactly where that would show up.

## Conclusion

Adopt tiny-sdf if you are drawing text with WebGL and you want glyphs generated from the system fonts at runtime, without shipping a font atlas or a rasterisation toolchain. It does not fit a project that needs a fixed, pre-baked atlas for consistent cross-device output, since every device rasterises its own system font, and it does not fit a build that targets Internet Explorer or a CommonJS Node version below 12.15. Before you use it, decide how large your radius should be relative to the font size, since that choice sets the resolution of the distance field, and check that the glyph metrics the library returns, in particular the hardwired glyphLeft, match what your text layout code expects.

## FAQ

### What does tiny-sdf produce and how does it work?

It generates a signed distance field for a single character by rasterising the glyph with Canvas 2D and then applying the Felzenszwalb and Huttenlocher distance transform to the bitmap. The result is a Uint8ClampedArray of alpha values for a width by height grid, which is useful for rendering text with WebGL.

### How do I use tiny-sdf in a browser and in Node?

In the browser you import it as an ES module in a script tag with type module, for example from the skypack CDN. In Node the README says you cannot use require, only import, and only in ESM capable versions, v12.15 and later.

### What do the buffer, radius and cutoff options control?

The buffer is the whitespace around the glyph in pixels, the radius is how many pixels around the glyph shape are used for encoding the distance, and the cutoff is how much of that radius, relatively, is used for the inside part of the glyph. The font size, family, weight and style are passed to Canvas 2D for rasterisation.

### Is the tiny-sdf output usable for text layout?

It returns the field data, the bitmap width and height, and the metrics glyphTop, glyphWidth, glyphHeight and glyphAdvance. The README notes that glyphLeft is currently hardwired to 0, with the actual glyph differences encoded in the rasterisation, so horizontal offsets are not reported separately.

### How do I run the tiny-sdf tests and demo locally?

The package scripts are npm test to run the tests and npm start to start the server for the demo page, and there is a bench script for the benchmark. The test script runs node --test, and the pretest step lints the implementation, the demo page and the tests.

## Sources

- [License: BSD-2-Clause](https://github.com/mapbox/tiny-sdf/blob/main/LICENSE)
- [mapbox/tiny-sdf on GitHub](https://github.com/mapbox/tiny-sdf)
- [Project website](http://mapbox.github.io/tiny-sdf/)
- [README](https://github.com/mapbox/tiny-sdf/blob/main/README.md)
- [Releases](https://github.com/mapbox/tiny-sdf/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mapbox-tiny-sdf
