BLIT386BLIT386

Bitmap Fonts

The system font and custom .btfont bitmap fonts: format, loading, and BMFont conversion.

BLIT386 ships with a built-in system font and supports custom .btfont bitmap fonts with variable-width glyphs, per-character offsets, Unicode characters, and either embedded or external textures.

Built-in system font

The engine includes a 6×14 monospace bitmap font covering printable ASCII (characters 32-126) plus a set of extra glyphs (dashes, accented Latin punctuation, media icons, arrows, Greek capitals, and more) - see scripts/system-font-extra-chars.mjs for the full list. It requires no file loading and is available immediately after initialization:

// Draw text with the system font (palette index selects the color)
.(new (10, 10), 1, 'Hello World!');

// Measure text dimensions
const  = .('Hello'); // returns Vector2i(width, height)

Editing the system font

The glyph data is stored as bit patterns in src/assets/fonts/systemFontData.ts. To edit it visually:

pnpm run system-font:export   # exports current data to assets/system-font.png (96×154, 16×11 grid of 6×14 cells)
# open assets/system-font.png in a pixel editor and redraw glyphs
pnpm run system-font:convert  # reads the PNG and regenerates systemFontData.ts

White pixels become foreground (palette index 1), black pixels become transparent (palette index 0). Only the top 6 bits of each byte are used (6-pixel glyph width within an 8-bit row).

Custom bitmap fonts (.btfont)

For proportional fonts, Unicode support, or custom aesthetics, load a .btfont file:

Quick start

import { , , ,  } from 'blit386';

// Load a font and register its colors in the palette
const  = new (256);
const  = await .('fonts/MyFont.btfont');
.().(); // required before draw
.();

// Render text (optional 4th arg is paletteOffset, not a Color32)
.(, new (10, 10), 'Hello World!');
.(, new (10, 30), 'Team B', 16); // shift glyph indices by 16

// Measure text width
const  = .('Hello');
const  = .('Hello'); // { width, height } (TextSize)

// Access font properties
.(.); // "MyFont"
.(.); // 14
.(.); // 15
.(.); // 98

Font file format (.btfont)

The .btfont format is a JSON file with the following structure:

{
  "name": "PragmataPro",
  "size": 14,
  "lineHeight": 15,
  "baseline": 13,
  "texture": "PragmataPro14.png",
  "glyphs": {
    " ": { "x": 169, "y": 54, "w": 5, "h": 17, "ox": -2, "oy": -1, "adv": 7 },
    "A": { "x": 80, "y": 18, "w": 9, "h": 17, "ox": -1, "oy": -1, "adv": 7 },
    "×": { "x": 0, "y": 54, "w": 9, "h": 17, "ox": -1, "oy": -1, "adv": 7 }
  }
}

Properties

Prop

Type

Glyph properties

Prop

Type

Texture options

The texture field supports two formats:

{
  "texture": "MyFont.png"
}

The path is resolved relative to the .btfont file location. This is convenient during development as you can update the PNG without regenerating the JSON.

Loading limits

BitmapFont.load() enforces the same resource limits as sprite sheets. See API: Assets for the full table. In short:

  • .btfont JSON must be 1 MiB or smaller; glyph maps are capped at 8192 entries.
  • Embedded PNG payloads must stay within 512 KiB of base64 data (after the data:image/png;base64, prefix).
  • Font atlas textures obey the 8192 per-side and 16,777,216 total-pixel decoded limits.
  • Invalid glyph metrics (non-integer values, negative sizes, rectangles outside the atlas) throw AssetLimitError before rendering.

When validation fails, loading throws AssetLimitError with a beginner-friendly message. Prefer a separate PNG texture for large atlases instead of embedding multi-megabyte base64 in the JSON file.

Converting from BMFont format

Most bitmap font tools export to the BMFont format (.fnt + .png). Use the included conversion script to convert these to .btfont:

Using the conversion script

# From project root
node scripts/convert-bmfont.mjs <input.fnt> <output.btfont> [--embed]

# Or via pnpm. Arguments after `--` are forwarded; the separator itself is ignored:
pnpm run convert-font -- <input.fnt> <output.btfont> [--embed]

# Examples:
node scripts/convert-bmfont.mjs tmp/MyFont.fnt examples/fonts/MyFont.btfont
node scripts/convert-bmfont.mjs tmp/MyFont.fnt examples/fonts/MyFont.btfont --embed

Options:

  • --embed - Embed the texture as base64 (recommended for production)

Running on different platforms

The conversion script works best when run directly in your terminal:

cd /path/to/blit386
node scripts/convert-bmfont.mjs input.fnt output.btfont --embed

Run from a real terminal

For best results with file I/O operations, run Node.js scripts directly from the terminal (not through an IDE's embedded shell).

Manual conversion

If you prefer to convert manually:

Parse the .fnt XML file

Extract:

  • Font info: face, size from <info>.
  • Metrics: lineHeight, base from <common>.
  • Glyphs: all <char> elements.

Convert each character element to a glyph entry

<char id="65" x="80" y="18" width="9" height="17" xoffset="-1" yoffset="-1" xadvance="7" />

Becomes:

"A": { "x": 80, "y": 18, "w": 9, "h": 17, "ox": -1, "oy": -1, "adv": 7 }

Set the texture path

Point it at the PNG filename, or embed it as base64.

Creating bitmap fonts

BMFont (Windows)

  • The original bitmap font generator.
  • Export as XML format, then convert to .btfont.

Hiero (cross-platform)

  • Java-based, works on Mac/Linux/Windows.
  • Export as BMFont format.

ShoeBox (cross-platform)

  • Adobe AIR application.
  • Can create bitmap fonts from existing images.

Tips for creating fonts

  1. Use power-of-two texture sizes (256×128, 512×256, etc.) for best GPU compatibility.

  2. Include an outline (1-2px) for better readability on varied backgrounds.

  3. Export with padding between characters to prevent bleeding artifacts.

  4. Use white glyphs on a transparent background - palette slots are assigned at indexize time; recolor at draw time via paletteOffset on BT.printFont(), not RGBA tinting.

  5. Include common symbols beyond ASCII: ×, ÷, ·, -, …, etc.

API reference

BitmapFont

BitmapFont Since 0.1.0 TextSize Since 0.1.0
declare class  {
  static (: string): <>;
  readonly : string;
  readonly : number;
  readonly : number;
  readonly : number;
  readonly : number;
  readonly : readonly number[];
  (: string): Glyph | null;
  (: string): number;
  (: string): TextSize;
  (: string, : TextSize): TextSize;
  (: string): boolean;
  (): ;
}

Exported type TextSize is { width: number; height: number }.

Fallback glyph

Include a glyph keyed by U+FFFD (the Unicode replacement character) in a .btfont file's glyphs map, and getGlyph() / getGlyphByCode() (and measureText(), which shares their lookup) substitute it for any character missing its own entry, instead of silently skipping the character. A skipped character does not advance the pen position either, so a missing glyph without a fallback can make the character after it overdraw the one before it - a defined fallback keeps both the glyph and the spacing correct. hasGlyph() is unaffected: it still reports whether a character has its own glyph, not whether the fallback would cover it.

A font with no U+FFFD entry keeps the previous behavior exactly. The built-in system font (BT.systemPrint) defines one, alongside a set of extra glyphs beyond plain ASCII - see codePoints below for the live, always-current list, or scripts/system-font-extra-chars.mjs in the engine source for the same list annotated with human-readable labels.

Listing covered characters

codePoints returns every Unicode code point a font defines a glyph for, ascending - derived live from the same glyph map getGlyph() / hasGlyph() read, so it can never list a character the font doesn't actually render. Use it to build a glyph browser or a coverage check instead of hand-maintaining a duplicate character list next to the font:

for (const  of .) {
  .(.());
}

BT.printFont()

.(, , );
.(, , , ); // Per-draw index shift (default 0); not a Color32

Examples

Multi-line text

const  = ['Line 1', 'Line 2', 'Line 3'];
let  = 10;

for (const  of ) {
  .(, new (10, ), );
   += .;
}

Centered text

const  = 'Centered';
const  = .();
const  = ..;
const  = .(( - ) / 2);

.(, new (, 10), );

Per-character palette offsets

let  = 10;
for (let  = 0;  < .; ++) {
  const  = [];
  const  =  % 2 === 0 ? 0 : 16; // alternate palette ranges

  .(, new (, 10), , );

  const  = .();
   +=  ? . : .;
}

Troubleshooting

API history

SymbolSinceLast changedStatus
BitmapFont0.1.01.7.0stable
TextSize0.1.0–stable

1.7.0 - Fri Sep 11 2026

  • ChangedBitmapFontAdded the `codePoints` getter, returning every Unicode code point the font defines a glyph for (ascending, derived live from the same glyph map `getGlyph()` and `hasGlyph()` read).

1.5.0 - Tue Aug 11 2026

  • ChangedBitmapFont`getGlyph` / `getGlyphByCode` (and `measureText`, which now shares their lookup) substitute a font-defined fallback glyph for a missing character instead of returning `null`, when the font's glyph map has an entry keyed by `U+FFFD`.

0.1.0 - Sat Dec 13 2025

  • AddedBitmapFont
  • AddedTextSize

See also

Last updated on October 18, 2026

On this page