npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zakkster/lite-bmfont

v1.2.0

Published

Zero-GC bitmap font canvas renderer. O(1) kerning via 64K Int16 LUT, multi-line alignment, zero-alloc numeric HUD output, and pre-laid-out wrapped text with H/V alignment + ellipsis.

Readme

@zakkster/lite-bmfont

npm version npm bundle size npm downloads npm total downloads TypeScript Dependencies License: MIT

→ Live Interactive Playground

🔤 What is lite-bmfont?

@zakkster/lite-bmfont renders BMFont-format bitmap text to Canvas2D with zero allocations.

It gives you:

  • 🔤 BMFont JSON format support
  • ⚡ O(1) kerning lookup via 64K Int16Array LUT
  • 📏 Multi-line \n text with left / center / right alignment
  • 📐 measure() for kerning-aware width calculation
  • 🔢 drawFast() — zero-alloc number renderer (1 decimal place) for HUDs, scores, timers
  • 📦 drawWrapped() — render a pre-laid-out Float32Array of lines into a bounding box, with H/V alignment and an optional ellipsis flag
  • 🧹 Zero allocation on every hot-path call — no string splitting, no array creation
  • 🎯 Pixel-snapped rendering for crisp pixel fonts
  • 🪶 ~1.3 KB gzipped

Note: Supports ASCII characters 0–255. Unicode is intentionally excluded for zero-GC performance.

Part of the @zakkster/lite-* ecosystem — micro-libraries built for deterministic, cache-friendly game development.

🚀 Install

npm i @zakkster/lite-bmfont

🕹️ Quick Start

import { BitmapFont } from '@zakkster/lite-bmfont';

const font = new BitmapFont(atlasImage, fontJson);

// Draw left-aligned at the baseline.
font.draw(ctx, 'SCORE: 1000', 10, 30);

// Draw centered (align: 0=left, 1=center, 2=right).
font.draw(ctx, 'GAME OVER', canvas.width / 2, 200, 2.0, 1);

// Measure width.
const w = font.measure('Hello', 1.5);

// Zero-alloc number drawing — ideal for per-frame HUDs.
font.drawFast(ctx, fps,   10, 20);                      // "60.0"
font.drawFast(ctx, 33.49, 10, 40);                      // "33.5"  (rounded)
font.drawFast(ctx, score, canvas.width / 2, 60, 1, 1);  // centered

📦 Wrapped Text (drawWrapped)

drawWrapped renders multi-line text into a bounding box with both horizontal and vertical alignment, plus an optional ellipsis-on-overflow flag. To stay zero-alloc, it does not do word-wrapping itself — you hand it a Float32Array describing the lines, and it does the rest. That separation lets you compute the layout once and re-render it every frame for free.

Layout buffer format

Each line is 4 consecutive Float32 values:

| Slot | Meaning | |------|---------| | [0] | startIdx — char index in text where this line begins (inclusive) | | [1] | endIdx — char index in text where this line ends (exclusive) | | [2] | lineWidth — measured pixel width of this line at scale=1 | | [3] | flags0 = normal line; 1 = append ellipsis after content |

The buffer must hold at least lineCount * 4 floats; surplus capacity is ignored, so you can reuse one fat buffer across many strings without reallocating.

Drawing a layout

font.drawWrapped(
  ctx, text, layoutBuffer, lineCount,
  boxWidth, boxHeight, boxX, boxY,
  scale,   // default 1
  align,   // 0 = left,  1 = center, 2 = right
  vAlign   // 0 = top,   1 = middle, 2 = bottom
);

(boxX, boxY) is the container's top-left corner, not a baseline. The renderer positions line 1's visual top edge at boxY when vAlign=0.

Producing the layout

The package does not ship a wrapper because layout strategy is application-specific (word-break vs. char-break, hyphenation, soft-wrap markers, etc.). Here is a tiny greedy word-break helper you can drop into your own code — keep one buffer alive and reuse it:

// Greedy word-wrap. Returns the number of lines written into `out`.
// `out` must hold at least Math.ceil(text.length / 4) * 4 floats (worst case: every char a line).
function layoutWrap(font, text, maxWidth, out) {
    let line = 0, i = 0, len = text.length;

    while (i < len) {
        let lineStart = i;
        let lastBreak = -1;          // index of last whitespace seen
        let lastBreakWidth = 0;
        let width = 0;
        let prevId = -1;

        while (i < len) {
            const id = text.charCodeAt(i);
            if (id === 10) break;                       // \n
            if (id === 32) { lastBreak = i; lastBreakWidth = width; }

            const advance = font.glyphs[id * 7 + 6];
            const kern = prevId === -1 ? 0 : font.kerning[(prevId << 8) | id];
            const nextWidth = width + kern + advance;

            if (nextWidth > maxWidth && i > lineStart) {
                // Wrap at last whitespace, else hard-break.
                if (lastBreak !== -1) { i = lastBreak + 1; width = lastBreakWidth; }
                break;
            }

            width = nextWidth;
            prevId = id;
            i++;
        }

        const lineEnd = (lastBreak !== -1 && i === lastBreak + 1) ? lastBreak : i;
        const o = line * 4;
        out[o]     = lineStart;
        out[o + 1] = lineEnd;
        out[o + 2] = width;
        out[o + 3] = 0;          // set to 1 to draw "..." after this line
        line++;

        if (i < len && text.charCodeAt(i) === 10) i++;  // skip the \n
    }
    return line;
}

// Use it:
const layout = new Float32Array(64);          // room for 16 lines, allocated once
const lines  = layoutWrap(font, story, 300, layout);
font.drawWrapped(ctx, story, layout, lines, 300, 200, 20, 20, 1, 1, 1); // center/center

Ellipsis on overflow

If your layout truncates a line and you want appended, set its flags slot to 1. The renderer will draw three '.' glyphs after the line's content (so make sure '.' is in your atlas).

// Line 0 was truncated by your wrap logic — ask the renderer to draw an ellipsis.
layout[3] = 1;

🧠 Why This Exists

Existing BMFont renderers allocate line arrays and substring objects per draw call. lite-bmfont uses charCodeAt() to index directly into an Int16Array glyph table — 7 values per glyph, accessed via id * 7 + offset. The 64K kerning LUT trades 128 KB of memory for O(1) lookup speed.

drawFast() extends the same philosophy to numeric output: it converts a number to ASCII char codes inside a pre-allocated Uint8Array scratch buffer, never producing a string. Drawing value.toFixed(1) per frame in a HUD allocates a fresh string every call; drawFast() allocates nothing.

drawWrapped() extends it again to wrapped paragraphs: the layout (lines, widths, ellipsis state) is computed once into a Float32Array and re-rendered every frame with zero per-frame work — no String.split('\n'), no per-line substring(), no per-frame measurement.

📊 Comparison

| Library | Size (gzip) | Allocations | Kerning | Multi-line | Wrap + align | Install | |---------|------|-------------|---------|------------|----|---------| | bmfont-text | ~4 KB | Arrays per draw | Slow | Basic | Some | npm i bmfont-text | | msdf-bmfont-xml | ~8 KB | High | Yes | Yes | Yes | npm i msdf-bmfont-xml | | lite-bmfont | ~1.3 KB | Zero | O(1) LUT | Yes + alignment | Yes (BYO layout) | npm i @zakkster/lite-bmfont |

⚙️ API

new BitmapFont(imageAtlas, fontJson)

  • imageAtlas: loaded HTMLImageElement or HTMLCanvasElement
  • fontJson: standard BMFont JSON with common, chars, and optional kernings

measure(text, scale?) → number

Returns kerning-aware pixel width.

draw(ctx, text, x, y, scale?, align?) → void

Multi-line \n-aware renderer. align: 0 = left, 1 = center, 2 = right. x, y is the baseline anchor of the first line.

drawFast(ctx, value, x, y, scale?, align?) → void

Zero-alloc number renderer with one decimal place.

  • NaN, +Infinity, -Infinity → silently skipped (returns).
  • Negative values → clamped to 0.
  • Decimal → rounded to nearest tenth (33.49 → "33.5").
  • Requires '0''9' (codes 48–57) and '.' (code 46) in the atlas.

drawWrapped(ctx, text, layoutBuffer, lineCount, boxWidth, boxHeight, x, y, scale?, align?, vAlign?) → void

Renders a pre-laid-out Float32Array of lines into a box. See the Wrapped Text section above for buffer format and a layout helper recipe.

  • x, y is the box's top-left corner.
  • align: 0 = left, 1 = center, 2 = right.
  • vAlign: 0 = top, 1 = middle, 2 = bottom.
  • A line with flags === 1 is rendered followed by an ellipsis.

destroy() → void

Releases the atlas reference and typed arrays.

🧪 Benchmark

Rendering 1000 characters per frame:
  bmfont-text:  Allocates line arrays per draw
  lite-bmfont:  Zero allocation, charCodeAt() + Int16Array lookup per glyph

Rendering 60 numeric HUD values per frame:
  value.toFixed(1) + draw():  allocates a new String each call
  drawFast(value):            zero allocation — char codes go into a reused Uint8Array

Rendering a 12-line wrapped paragraph at 60 fps:
  ctx.measureText + split('\n'): allocates arrays + TextMetrics each frame
  drawWrapped(layout):           zero allocation — layout buffer is reused

📦 TypeScript

Full TypeScript declarations included in BitmapFont.d.ts. The Align, VAlign, BMFontJson, BMFontChar, and BMFontKerning types are also exported for downstream typing of layout helpers and JSON loaders.

📚 LLM-Friendly Documentation

See llms.txt for AI-optimized metadata and usage examples.

🗒️ Changelog

1.2.0

  • Added: drawWrapped(ctx, text, layoutBuffer, lineCount, boxWidth, boxHeight, x, y, scale?, align?, vAlign?) — renders pre-laid-out wrapped text into a bounding box with horizontal and vertical alignment, plus an optional ellipsis flag per line. Layout consumed as a Float32Array for zero per-frame allocation.
  • Added: Exported types Align, VAlign, BMFontJson, BMFontChar, BMFontKerning from BitmapFont.d.ts.

1.1.0

  • Added: drawFast(ctx, value, x, y, scale?, align?) — zero-alloc number renderer with one decimal place. Built for per-frame HUD output (FPS, score, time) without producing GC pressure.
  • Internal: scratch buffer for drawFast is allocated once in the constructor and released by destroy().

1.0.x

  • Initial release: draw, measure, multi-line alignment, O(1) kerning LUT.

License

MIT