BitmapText.js

A JavaScript library for rendering pixel-identical, consistent bitmap text across all browsers and devices, for both browser (HTML5 Canvas) and node (.png or .qoi image).

See demos and examples

Documentation Navigation:

Problem Statement

Browser text rendering on Canvas is inconsistent - different browsers apply anti-aliasing differently, making pixel-identical text rendering impossible with standard Canvas APIs. This library solves that by pre-rendering glyphs as bitmaps.

Features

Limitations

Compound Emoji Support: The library operates on Unicode code points, not grapheme clusters. Basic emojis work (‘😀’), but compound emojis don’t (‘👨‍👩‍👧’ family emoji, ‘🏳️‍🌈’ rainbow flag). See docs/ARCHITECTURE.md for details.

Distribution & Usage Options

BitmapText.js uses a static class architecture with zero configuration needed for most use cases:

For applications that consume pre-built bitmap fonts, simply include the static BitmapText class:

  // Import only the static BitmapText class (~15-18KB)
  import { BitmapText } from './src/runtime/BitmapText.js';

  // Optional: Import helper classes for type safety and advanced usage
  import { FontProperties } from './src/runtime/FontProperties.js';
  import { TextProperties } from './src/runtime/TextProperties.js';

  // Font data self-registers when loaded
  // No configuration needed in browser environments

Best for: Production web apps, mobile apps, games where bundle size matters

Node.js Distribution

For Node.js environments, minimal configuration is required:

  // Import static BitmapText class
  import { BitmapText } from './src/runtime/BitmapText.js';
  import { Canvas } from './src/platform/canvas-mock.js';

  // Configure for Node.js environment
  BitmapText.configure({
    fontDirectory: './font-assets/',
    canvasFactory: () => new Canvas()
  });

Best for: Server-side rendering, CLI tools, automated image generation

Font Assets Building Distribution

For applications that need to generate bitmap fonts at runtime:

  // Import full toolkit including font assets building tools (~55KB+)
  import { BitmapText } from './src/runtime/BitmapText.js';
  import { AtlasDataStore } from './src/runtime/AtlasDataStore.js';
  import { FontMetricsStore } from './src/runtime/FontMetricsStore.js';
  import { BitmapTextFAB } from './src/builder/BitmapTextFAB.js';
  import { AtlasDataStoreFAB } from './src/builder/AtlasDataStoreFAB.js';
  import { FontMetricsStoreFAB } from './src/builder/FontMetricsStoreFAB.js';
  import { FontPropertiesFAB } from './src/builder/FontPropertiesFAB.js';

Best for: Font building tools, development environments, CI/CD pipelines

Bundle Size Comparison

Distribution Type Bundle Size Use Case
Static Runtime ~15-18KB Production apps (browser & Node.js)
Full Toolkit ~50KB+ Development tools generating fonts

Recommendation: Use static runtime in production and build font assets during your build process using the full toolkit.

For production deployments, use the pre-built minified bundles:

Browser (Single Script Tag):

  <!-- Single file: ~32KB minified + gzipped ~12KB -->
  <script src="dist/bitmaptext.min.js"></script>

  <script>
    const fontProps = new FontProperties(1, "Arial", "normal", "normal", 19);
    await BitmapText.loadFont(fontProps.idString);
    BitmapText.drawTextFromAtlas(ctx, "Hello World", 10, 50, fontProps);
  </script>

Node.js (With Your Canvas):

  import { createCanvas } from 'node-canvas';  // or 'skia-canvas'
  import './dist/bitmaptext-node.min.js';

  BitmapText.configure({
    fontDirectory: './font-assets/',
    canvasFactory: () => createCanvas()
  });

  await BitmapText.loadFont(fontProps.idString);
  BitmapText.drawTextFromAtlas(ctx, "Hello", 10, 50, fontProps);

Build from source:

  ./scripts/build-runtime-bundle.sh --all
  # or
  npm run build-bundle-all

Benefits:

What’s included: StatusCode, FontProperties, TextProperties, BitmapText, FontLoader, Atlas/Metrics stores, MetricsExpander, TightAtlasReconstructor, and all supporting classes.

What’s excluded (Node.js): Canvas implementation (you provide), PNG encoder (image I/O not core library).

See dist/README.md for complete bundle documentation.

Quick Start

Get up and running with the production-ready bundles in seconds.

Browser Usage

  <!DOCTYPE html>
  <html>
  <head>
    <title>BitmapText Demo</title>
  </head>
  <body>
    <canvas id="myCanvas" width="400" height="100"></canvas>

    <!-- Single script tag - complete runtime (~32KB minified) -->
    <script src="dist/bitmaptext.min.js"></script>

    <script>
      const canvas = document.getElementById('myCanvas');
      const ctx = canvas.getContext('2d');

      // IMPORTANT - Pixel Density Configuration:
      // BitmapText requires explicit pixel density specification:
      // - Standard displays: Use 1.0
      // - HiDPI/Retina displays: Use window.devicePixelRatio (typically 2.0+)
      // - No automatic detection - you must provide this value
      // For detailed HiDPI setup, see "Understanding Coordinate Systems & Transforms" section below

      // Create font configuration
      const fontProperties = new FontProperties(
        1,          // pixelDensity (1.0 = standard, 2.0 = Retina)
        "Arial",    // fontFamily
        "normal",   // fontStyle
        "normal",   // fontWeight
        19          // fontSize in CSS pixels
      );

      // Optional: Configure font directory if needed
      BitmapText.setFontDirectory('./font-assets/');

      // Load font and render
      BitmapText.loadFont(fontProperties.idString).then(() => {
        // Optional: Create text rendering configuration
        const textProperties = new TextProperties({
          isKerningEnabled: true,      // Enable kerning (default: true)
          textBaseline: 'bottom',      // Baseline positioning (default: 'bottom')
          textAlign: 'left',           // Alignment (default: 'left')
          textColor: '#000000'         // Color (default: '#000000')
        });

        // IMPORTANT: Do NOT call ctx.scale() - BitmapText handles scaling internally
        // IMPORTANT: Coordinates are ABSOLUTE from canvas origin, transforms are ignored

        // Render text using static API
        BitmapText.drawTextFromAtlas(ctx, "Hello World", 10, 50, fontProperties, textProperties);
      });
    </script>
  </body>
  </html>

Node.js Usage

  import { createCanvas } from 'node-canvas';  // or 'skia-canvas'
  import './dist/bitmaptext-node.min.js';

  // Configure with your Canvas implementation
  BitmapText.configure({
    fontDirectory: './font-assets/',
    canvasFactory: () => createCanvas()
  });

  // Create font configuration
  // Node.js pixel density: Use 1.0 for standard rendering, 2.0+ for HiDPI pre-rendering
  // See "Node.js Pixel Density" section below for details
  const fontProperties = new FontProperties(1, "Arial", "normal", "normal", 19);

  // Load font
  await BitmapText.loadFont(fontProperties.idString);

  // Create canvas and render
  const canvas = createCanvas(400, 100);
  const ctx = canvas.getContext('2d');

  // Coordinates are ABSOLUTE from canvas origin (0,0)
  BitmapText.drawTextFromAtlas(ctx, "Hello World", 10, 50, fontProperties);

  // Note: Image export requires separate PNG encoder (not included in bundle)
  // See dist/README.md for details

With Status Checking

  // Measure text - returns { metrics, status }
  const { metrics, status } = BitmapText.measureText("Hello World", fontProperties, textProperties);

  if (status.code !== StatusCode.SUCCESS) {
    console.warn('Measurement issues:', getStatusDescription(status));
  }

  // Draw text - returns { rendered, status }
  const result = BitmapText.drawTextFromAtlas(ctx, "Hello World", 10, 50, fontProperties, textProperties);

  if (result.status.code !== StatusCode.SUCCESS) {
    console.warn('Rendering issues:', getStatusDescription(result.status));
    if (result.status.placeholdersUsed) {
      console.info('Some glyphs rendered as placeholder rectangles');
    }
  }

See dist/README.md for complete bundle documentation.

For development and debugging: See the “Development & Debugging” section below for using individual source files.

Font-Invariant Character Support

BitmapText.js includes automatic support for special Unicode font-invariant characters via the BitmapTextInvariant font:

☺☹♠♡♦♣│─├└▶▼▲◀✔✘≠↗

Emoji Character Aliasing

Modern emoji characters are automatically mapped to their corresponding font-invariant symbols:

Input Emoji Rendered As Unicode Name
😊 😀 😃 Smiling faces → White Smiling Face
😢 ☹️ Sad faces → White Frowning Face

This allows you to use modern emojis in your text while rendering them using the bitmap font glyphs.

Using Font-Invariant Characters in Text

Font-invariant characters automatically render using the BitmapTextInvariant font when included in text:

// Load both your text font AND BitmapTextInvariant
await BitmapText.loadFonts([
  'density-1-0-Arial-style-normal-weight-normal-size-19-0',
  'density-1-0-BitmapTextInvariant-style-normal-weight-normal-size-19-0'
]);

// Use font-invariant characters anywhere in your text - they auto-redirect to font-invariant font
const fontProps = new FontProperties(1, "Arial", "normal", "normal", 19);
BitmapText.drawTextFromAtlas(ctx, "Task completed ✔", 10, 50, fontProps);
BitmapText.drawTextFromAtlas(ctx, "Score: 100 ♦", 10, 80, fontProps);
BitmapText.drawTextFromAtlas(ctx, "Hello 😊 World", 10, 110, fontProps);  // 😊 renders as ☺

How Font-Invariant Character Auto-Redirect Works

Loading Font-Invariant Fonts

Font-invariant fonts follow the same naming convention as regular fonts:

// For size 18
'density-1-0-BitmapTextInvariant-style-normal-weight-normal-size-18-0'

// For size 19
'density-1-0-BitmapTextInvariant-style-normal-weight-normal-size-19-0'

// For HiDPI (2×) at size 19
'density-2-0-BitmapTextInvariant-style-normal-weight-normal-size-19-0'

Always load the font-invariant font at the same pixel density and size as your text font.

Font-Invariant Font Characteristics

Understanding Coordinate Systems & Transforms

Transform Behavior - CRITICAL

BitmapText IGNORES all context transforms. Coordinates are always absolute from canvas origin (0,0).

// Setup
const ctx = canvas.getContext('2d');
ctx.scale(2, 2);
ctx.translate(100, 50);
ctx.rotate(Math.PI / 4);

// BitmapText ignores ALL transforms above
const fontProps = new FontProperties(2.0, "Arial", "normal", "normal", 19);
BitmapText.drawTextFromAtlas(ctx, "Hello", 10, 50, fontProps);
// ✅ Text renders at exactly (10, 50) CSS pixels from origin
// ❌ NOT at (120, 80) which would be 10+100 translate, 50+50 translate
// ❌ NOT rotated 45 degrees

Why: BitmapText needs direct control over physical pixel positioning for pixel-perfect rendering. It temporarily resets the context transform to identity during drawing, then restores it.

Coordinate System Overview

All BitmapText coordinates and measurements use CSS pixels:

API Input Units Output Units
drawTextFromAtlas(ctx, text, x_CssPx, y_CssPx, ...) x_CssPx, y_CssPx = CSS pixels rendered status
measureText(text, ...) N/A width, bounds = CSS pixels
FontProperties(density, family, style, weight, size) size = CSS pixels N/A

Internal conversion: physicalPixels = cssPixels × pixelDensity

Canvas Setup for HiDPI

Standard Display (pixelDensity = 1.0)

const fontProps = new FontProperties(1.0, "Arial", "normal", "normal", 18);
const canvas = document.getElementById('myCanvas');
canvas.width = 400;   // 400 physical pixels
canvas.height = 100;  // 100 physical pixels
const ctx = canvas.getContext('2d');

// No special setup needed
BitmapText.drawTextFromAtlas(ctx, "Hello", 10, 50, fontProps);

HiDPI Display (Retina, 2× or higher)

const dpr = window.devicePixelRatio;  // e.g., 2.0 for Retina
const fontProps = new FontProperties(dpr, "Arial", "normal", "normal", 18);

const canvas = document.getElementById('myCanvas');
const cssWidth = 400;
const cssHeight = 100;

// Set physical dimensions
canvas.width = cssWidth * dpr;    // e.g., 800 physical pixels
canvas.height = cssHeight * dpr;  // e.g., 200 physical pixels

// Set CSS dimensions for proper display size
canvas.style.width = cssWidth + 'px';
canvas.style.height = cssHeight + 'px';

const ctx = canvas.getContext('2d');

// ⚠️ IMPORTANT: Do NOT call ctx.scale(dpr, dpr)
// BitmapText handles density scaling internally!

// Draw with CSS pixel coordinates
BitmapText.drawTextFromAtlas(ctx, "Hello", 10, 50, fontProps);
// Internally converts to physical (20, 100) for pixel-perfect rendering

Comparison with HTML5 Canvas

BitmapText uses a different pattern than standard HTML5 Canvas:

HTML5 Canvas (Standard HiDPI):

ctx.scale(dpr, dpr);  // Scale context
ctx.font = '19px Arial';
ctx.fillText('Hello', 10, 50);  // Transform applied automatically

BitmapText:

// NO ctx.scale() - BitmapText handles scaling internally
const fontProps = new FontProperties(dpr, 'Arial', 'normal', 'normal', 19);
BitmapText.drawTextFromAtlas(ctx, 'Hello', 10, 50, fontProps);

Key Differences:

Aspect HTML5 Canvas BitmapText
Context scaling ctx.scale(dpr, dpr) NO scaling
Transform handling Respects transforms IGNORES transforms
Coordinate system Relative to transform Absolute from origin
Font size String '19px' Number 19
Pixel density Implicit in scale Explicit in FontProperties

Node.js Pixel Density

Node.js has no window.devicePixelRatio. Choose based on your use case:

Standard Server-Side Rendering:

const fontProps = new FontProperties(1.0, "Arial", "normal", "normal", 18);
const canvas = new Canvas(400, 100);  // Output is 400×100 pixels

Pre-rendering for HiDPI Displays:

const targetDensity = 2.0;  // Target display is 2× (Retina)
const fontProps = new FontProperties(targetDensity, "Arial", "normal", "normal", 18);

// Output will be 2× larger
const cssWidth = 400;
const cssHeight = 100;
const canvas = new Canvas(
  cssWidth * targetDensity,   // 800 physical pixels
  cssHeight * targetDensity   // 200 physical pixels
);

Common Pitfalls

DON’T scale the context when using BitmapText:

ctx.scale(dpr, dpr);  // ❌ This will be IGNORED
BitmapText.drawTextFromAtlas(ctx, text, 10, 50, fontProps);
// Works, but the scale is wasted (reset then restored)

DON’T expect transforms to work:

ctx.translate(100, 50);  // ❌ This will be IGNORED
BitmapText.drawTextFromAtlas(ctx, text, 10, 50, fontProps);
// Text renders at (10, 50), NOT (110, 100)

DON’T mix density values:

const fontProps = new FontProperties(2.0, ...);  // Font at 2×
canvas.width = 400;  // ❌ Canvas at 1× - glyphs will be too large!

DO keep density consistent:

const density = window.devicePixelRatio;
const fontProps = new FontProperties(density, ...);
canvas.width = 400 * density;  // ✅ Matching density

DO use absolute positioning:

// Calculate exact position you want
const x = 10;  // Absolute CSS pixels from origin
const y = 50;  // Absolute CSS pixels from origin
BitmapText.drawTextFromAtlas(ctx, text, x, y, fontProps);

Text Baseline Positioning

BitmapText supports all six HTML5 Canvas textBaseline values. The y-coordinate you provide corresponds to the position of the chosen baseline.

Available Baselines

Baseline Description Use Case
top Top of em square Aligning text to top edge
hanging Hanging baseline Tibetan, Devanagari scripts
middle Middle of em square Vertically centering text
alphabetic Alphabetic baseline Standard Latin text (HTML5 default)
ideographic Ideographic baseline CJK characters
bottom Bottom of em square Aligning to bottom edge (BitmapText default)

Baseline Examples

Standard alphabetic baseline (HTML5 Canvas default):

const textProps = new TextProperties({ textBaseline: 'alphabetic' });
BitmapText.drawTextFromAtlas(ctx, 'Hello', 10, 50, fontProps, textProps);
// y=50 is at the alphabetic baseline (bottom of most letters, excluding descenders)

Middle baseline (vertical centering):

const textProps = new TextProperties({ textBaseline: 'middle' });
BitmapText.drawTextFromAtlas(ctx, 'World', 10, 75, fontProps, textProps);
// y=75 is at the vertical center of the em square

Top baseline (hanging down):

const textProps = new TextProperties({ textBaseline: 'top' });
BitmapText.drawTextFromAtlas(ctx, 'Top', 10, 100, fontProps, textProps);
// y=100 is at the top of the em square, text hangs down from this point

Bottom baseline (BitmapText default):

const textProps = new TextProperties({ textBaseline: 'bottom' });
// or simply omit textBaseline to use default
BitmapText.drawTextFromAtlas(ctx, 'Bottom', 10, 125, fontProps, textProps);
// y=125 is at the bottom of the em square

Visual Demo

See public/baseline-alignment-demo.html for a comprehensive visual demonstration of all baseline and alignment combinations.

Baseline Coordinate System

Text Alignment

BitmapText supports three horizontal text alignment modes. The x-coordinate you provide serves as the alignment anchor point.

Available Alignments

Alignment Description Anchor Point
left Text starts at x Leftmost point (BitmapText default)
center Text is centered at x Horizontal midpoint
right Text ends at x Rightmost point

Alignment Examples

Left alignment (default):

const textProps = new TextProperties({ textAlign: 'left' });
// or simply omit textAlign to use default
BitmapText.drawTextFromAtlas(ctx, 'Left', 100, 50, fontProps, textProps);
// Text starts at x=100 and extends rightward

Center alignment (horizontal centering):

const textProps = new TextProperties({ textAlign: 'center' });
BitmapText.drawTextFromAtlas(ctx, 'Center', 200, 50, fontProps, textProps);
// Text is centered at x=200, extending equally left and right

Right alignment (right-justify):

const textProps = new TextProperties({ textAlign: 'right' });
BitmapText.drawTextFromAtlas(ctx, 'Right', 300, 50, fontProps, textProps);
// Text ends at x=300 and extends leftward

Combining alignment and baseline:

// Center text both horizontally (textAlign) and vertically (textBaseline)
const textProps = new TextProperties({
  textAlign: 'center',
  textBaseline: 'middle'
});
BitmapText.drawTextFromAtlas(ctx, 'Centered', 200, 150, fontProps, textProps);
// Text is centered both horizontally and vertically at point (200, 150)

Visual Demo

See public/baseline-alignment-demo.html for a comprehensive visual demonstration of all baseline and alignment combinations.

How Alignment Works

Development & Debugging

For development with maximum debugging flexibility, you can use individual source files instead of the production bundle.

Using Individual Source Files (Browser)

  <!-- Load core runtime classes (StatusCode must be loaded first) -->
  <script src="src/runtime/StatusCode.js"></script>
  <script src="src/runtime/FontProperties.js"></script>
  <script src="src/runtime/TextProperties.js"></script>
  <script src="src/runtime/FontMetrics.js"></script>
  <script src="src/runtime/AtlasImage.js"></script>
  <script src="src/runtime/AtlasPositioning.js"></script>
  <script src="src/runtime/AtlasData.js"></script>
  <script src="src/runtime/AtlasDataStore.js"></script>
  <script src="src/runtime/FontMetricsStore.js"></script>
  <script src="src/runtime/FontManifest.js"></script>
  <script src="src/runtime/TightAtlasReconstructor.js"></script>
  <script src="src/runtime/AtlasReconstructionUtils.js"></script>
  <script src="src/runtime/AtlasCellDimensions.js"></script>
  <script src="src/platform/FontLoader-browser.js"></script>
  <script src="src/runtime/FontLoaderBase.js"></script>
  <script src="src/builder/MetricsExpander.js"></script>
  <script src="src/runtime/BitmapText.js"></script>

  <!-- Load pre-generated font data (self-registers automatically) -->
  <script src="font-assets/metrics-density-1-0-Arial-style-normal-weight-normal-size-19-0.js"></script>
  <script src="font-assets/atlas-density-1-0-Arial-style-normal-weight-normal-size-19-0-webp.js"></script>

  <canvas id="myCanvas" width="400" height="100"></canvas>

  <script>
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');

    const fontProperties = new FontProperties(1, "Arial", "normal", "normal", 19);
    const textProperties = new TextProperties();

    // Render text using static API
    BitmapText.drawTextFromAtlas(ctx, "Hello World", 10, 50, fontProperties, textProperties);
  </script>

Benefits:

Trade-offs:

Example: See public/hello-world-demo.html for a complete unbundled example.

Using Individual Source Files (Node.js)

  import { BitmapText } from './src/runtime/BitmapText.js';
  import { FontProperties } from './src/runtime/FontProperties.js';
  import { TextProperties } from './src/runtime/TextProperties.js';
  import { Canvas } from './src/platform/canvas-mock.js';

  // Configure for Node.js
  BitmapText.configure({
    fontDirectory: './font-assets/',
    canvasFactory: () => new Canvas()
  });

  const fontProperties = new FontProperties(1, "Arial", "normal", "normal", 19);
  await BitmapText.loadFont(fontProperties.idString);

  const canvas = new Canvas(400, 100);
  const ctx = canvas.getContext('2d');

  BitmapText.drawTextFromAtlas(ctx, "Hello World", 10, 50, fontProperties);

Using Unminified Bundle

For a middle ground between individual files and minified bundle, use the unminified bundle:

  <!-- Easier to debug than minified, but still a single file -->
  <script src="dist/bitmaptext.js"></script>

Benefits:

Trade-offs:

Building from Source

Quick Reference

Rebuild all bundles:

  npm run build

Rebuild and test:

  ./run-node-demos.sh  # Builds bundles + runs Node.js demos

Build Commands

Command What It Builds
npm run build Browser + Node.js bundles
npm run build-bundle Browser bundle only
npm run build-bundle-node Node.js bundle only
./scripts/build-runtime-bundle.sh --all Both bundles (explicit)

What Gets Built

For detailed rebuild instructions, see dist/README.md.

Generating Your Own Bitmap Fonts

  npm run watch-fonts
  # or
  ./scripts/watch-font-assets.sh

Then use the font-assets-builder.html - files will be processed automatically!

Generate multiple font configurations automatically from JSON specifications:

  node scripts/automated-font-builder.js --spec=specs/font-sets/test-font-spec.json

See docs/FONT_SET_FORMAT.md for specification format details and scripts/README.md for complete usage documentation.

Manual Process

  1. Open public/font-assets-builder.html in a web browser
  2. Select font family, style, weight, and size range
  3. Click “Download Font Assets” to generate QOI bitmap font atlas
  4. Manually process files (see scripts/README.md for details)
  5. Include generated files in your project

For complete automation documentation, see scripts/README.md.

API Reference

BitmapText Static Class

All methods are static - no instantiation required.

Configuration (Optional)

  BitmapText.configure({
    fontDirectory: './font-assets/',   // Directory containing font assets
    canvasFactory: () => new Canvas()  // Factory function (Node.js only)
  })

fontDirectory:

canvasFactory (Node.js only):

Loading Methods

loadFont(idString, options): Promise<void>

Loads a single font (metrics + atlas). Returns a Promise that resolves when the font is loaded, or rejects on error.

  await BitmapText.loadFont('density-1-0-Arial-style-normal-weight-normal-size-18-0', {
    isFileProtocol: false,           // Optional: set true for file:// protocol
    onProgress: (loaded, total) => { // Optional: progress callback
      console.log(`${loaded}/${total} files loaded`);
    }
  });

loadFonts(idStrings, options): Promise<void>

Loads multiple fonts in parallel. Returns a Promise that resolves when all fonts are loaded, or rejects on error.

  await BitmapText.loadFonts([
    'density-1-0-Arial-style-normal-weight-normal-size-18-0',
    'density-1-0-Arial-style-normal-weight-normal-size-19-0'
  ], { onProgress: callback });

Font-Invariant Character Auto-Redirect:

When rendering text, font-invariant characters (☺☹♠♡♦♣│─├└▶▼▲◀✔✘≠↗) automatically use the BitmapTextInvariant font regardless of the specified base font. Modern emojis (😊😀😃😢) are also aliased to their symbol equivalents. To enable this:

  1. Load both your text font AND BitmapTextInvariant at the same size
  2. Use font-invariant characters or emojis anywhere in your text
  3. BitmapText automatically resolves aliases and switches to font-invariant font

Example:

  await BitmapText.loadFonts([
    'density-1-0-Arial-style-normal-weight-normal-size-19-0',
    'density-1-0-BitmapTextInvariant-style-normal-weight-normal-size-19-0'
  ]);

  // "Hello 😊" renders Hello in Arial, 😊 as ☺ in BitmapTextInvariant
  BitmapText.drawTextFromAtlas(ctx, "Hello 😊", 10, 50, fontProps);

Rendering Methods

measureText(text, fontProperties, textProperties)

Returns measurement data:

  {
    metrics: {
      width: number,                      // CSS pixels - total text width
      actualBoundingBoxLeft: number,      // CSS pixels - left extent from text position
      actualBoundingBoxRight: number,     // CSS pixels - right extent from text position
      actualBoundingBoxAscent: number,    // CSS pixels - ascent above baseline
      actualBoundingBoxDescent: number,   // CSS pixels - descent below baseline (negative)
      fontBoundingBoxAscent: number,      // CSS pixels - font ascent
      fontBoundingBoxDescent: number      // CSS pixels - font descent (negative)
    } | null,  // null if font metrics not available
    status: {
      code: StatusCode,           // 0=SUCCESS, 1=NO_METRICS, 2=PARTIAL_METRICS
      missingChars?: Set          // Missing characters (PARTIAL_METRICS only)
    }
  }

Note: All measurements are in CSS pixels. To convert to physical pixels: physicalPixels = cssPixels × fontProperties.pixelDensity

drawTextFromAtlas(ctx, text, x_CssPx, y_CssPx, fontProperties, textProperties)

Renders text and returns status:

  {
    rendered: boolean,            // Whether rendering occurred
    status: {
      code: StatusCode,           // 0=SUCCESS, 1=NO_METRICS, 2=PARTIAL_METRICS, 3=NO_ATLAS, 4=PARTIAL_ATLAS
      missingChars?: Set,         // Missing metric characters (PARTIAL_METRICS)
      missingAtlasChars?: Set,    // Missing atlas characters (PARTIAL_ATLAS)
      placeholdersUsed?: boolean  // Whether placeholders were used
    }
  }

Parameters:

Query Methods

hasMetrics(idString: string): boolean - Check if metrics are loaded for a specific font

hasAtlas(idString: string): boolean - Check if atlas is loaded for a specific font

hasFont(idString: string): boolean - Check if both metrics and atlas are loaded

  const isLoaded = BitmapText.hasFont('density-1-0-Arial-style-normal-weight-normal-size-18-0');
  if (!isLoaded) {
    await BitmapText.loadFont('density-1-0-Arial-style-normal-weight-normal-size-18-0');
  }

Unloading Methods

unloadMetrics(idString: string): void - Remove metrics from memory for a specific font

unloadAtlas(idString: string): void - Remove atlas from memory for a specific font

unloadFont(idString: string): void - Remove both metrics and atlas from memory

unloadFonts(idStrings: string[]): void - Remove multiple fonts from memory

unloadAllFonts(): void - Remove all fonts (metrics and atlases) from memory

unloadAllAtlases(): void - Remove all atlases from memory (keeps metrics)

  // Unload a specific font to free memory
  BitmapText.unloadFont('density-1-0-Arial-style-normal-weight-normal-size-18-0');

  // Unload all atlases (keeps metrics for measurement)
  BitmapText.unloadAllAtlases();

Registration Methods (Called by Font Assets)

registerMetrics(idString, compactedData): Register font metrics registerAtlas(idString, base64Data): Register atlas image

These are called automatically when font asset files are loaded.

StatusCode Module

The StatusCode module provides status codes and helper functions for handling rendering results.

Using Production Bundle (Recommended):

  <!-- StatusCode is included in the bundle, no special import needed -->
  <script src="dist/bitmaptext.min.js"></script>

  <script>
    // StatusCode is automatically available globally
    if (result.status.code === StatusCode.SUCCESS) {
      console.log('Rendering successful');
    }
  </script>

Using Individual Source Files (Development):

When loading individual source files in browsers with script tags, StatusCode.js must be loaded before BitmapText.js because BitmapText depends on the StatusCode constants.

  <!-- Load StatusCode first -->
  <script src="src/runtime/StatusCode.js"></script>

  <!-- Then load other runtime classes -->
  <script src="src/runtime/FontProperties.js"></script>
  <script src="src/runtime/TextProperties.js"></script>
  <!-- ... other files ... -->
  <script src="src/runtime/BitmapText.js"></script>

Using ES Modules:

  // In ES modules, imports are automatically hoisted
  import { StatusCode, isSuccess, getStatusDescription } from './src/runtime/StatusCode.js';
  import { BitmapText } from './src/runtime/BitmapText.js';

StatusCode Constants

  StatusCode.SUCCESS = 0        // Everything worked perfectly
  StatusCode.NO_METRICS = 1     // No font metrics available
  StatusCode.PARTIAL_METRICS = 2 // Some characters missing metrics
  StatusCode.NO_ATLAS = 3       // No atlas available (using placeholders)
  StatusCode.PARTIAL_ATLAS = 4  // Some characters missing from atlas

Helper Functions

  isSuccess(status)             // Returns true if status indicates success
  isCompleteFailure(status)     // Returns true if rendering completely failed
  isPartialSuccess(status)      // Returns true if partial rendering occurred
  getStatusDescription(status)  // Returns human-readable status description

Usage Examples

Basic Usage (ignoring status):

  const { metrics } = BitmapText.measureText(text, fontProps);
  const { rendered } = BitmapText.drawTextFromAtlas(ctx, text, x, y, fontProps);

With Status Checking:

  const result = BitmapText.measureText(text, fontProps);
  if (result.status.code === StatusCode.SUCCESS) {
    console.log('Width:', result.metrics.width);
  } else if (result.status.code === StatusCode.PARTIAL_METRICS) {
    console.warn('Missing characters:', [...result.status.missingChars]);
  }

  const drawResult = BitmapText.drawTextFromAtlas(ctx, text, x, y, fontProps);
  if (drawResult.status.code === StatusCode.PARTIAL_ATLAS) {
    console.warn('Using placeholders for:', [...drawResult.status.missingAtlasChars]);
  }

FontProperties Class

Immutable font configuration class with pre-computed keys for performance.

Constructor

  new FontProperties(pixelDensity, fontFamily, fontStyle, fontWeight, fontSize)

Parameters

Properties

TextProperties Class

Immutable text rendering configuration class with pre-computed keys for performance.

Constructor

new TextProperties(options = {})

Parameters (all optional with defaults)

Properties

Factory Methods

Instance Methods

FontSetGenerator Class

The FontSetGenerator provides memory-efficient generation of font configuration sets from JSON specifications. A general-purpose utility for automated testing, asset building, sample generation, and any scenario requiring systematic font exploration.

Use Cases:

Key Features:

Complete format specification: See docs/FONT_SET_FORMAT.md

Constructor

const generator = new FontSetGenerator(spec);

Parameters:

JSON Format

{
  "fontSets": [
    {
      "name": "Optional descriptive name",
      "density": [1.0, 2.0],
      "families": ["Arial", "Georgia"],
      "styles": ["normal", "italic"],
      "weights": ["normal", "bold", [400, 700, 100]],
      "sizes": [[12, 24, 0.5], 48]
    }
  ]
}

Field Types:

Range Format: Three-element array [start, stop, step]

Cross-Product: Each set generates: density_count × families_count × styles_count × weights_count × sizes_count configurations

Multi-Set Union: Multiple font sets combine via union (not cross-product between sets)

Methods

getCount()

Returns total number of font configurations without generating them (memory-efficient).

const count = generator.getCount();
console.log(`Will generate ${count} font configurations`);

iterator()

Returns ES6-compatible iterator that yields FontProperties instances one at a time.

for (const fontProps of generator.iterator()) {
  console.log(fontProps.idString);
  // Use fontProps for asset building, testing, sample generation, etc.
}

forEach(callback)

Convenience method for iteration with progress tracking.

generator.forEach((fontProps, index, total) => {
  console.log(`[${index + 1}/${total}] ${fontProps.idString}`);
  // Process fontProps
});

getSetsInfo()

Returns array of set metadata without generating instances.

const info = generator.getSetsInfo();
// [{ name: "Arial Standard", count: 48 }, { name: "Set 2", count: 120 }]

Usage Example: Simple Size Range

const spec = {
  fontSets: [
    {
      name: "Arial Size Testing",
      density: [1.0],
      families: ["Arial"],
      styles: ["normal"],
      weights: ["normal"],
      sizes: [[12, 24, 0.5]]  // 12, 12.5, 13, ..., 24
    }
  ]
};

const generator = new FontSetGenerator(spec);
console.log(`Total: ${generator.getCount()}`);  // 25

for (const fontProps of generator.iterator()) {
  // Each fontProps is a validated FontProperties instance
  await BitmapText.loadFont(fontProps.idString);
  // Test rendering, generate assets, create samples, etc.
}

Usage Example: Multi-Set with Different Requirements

const spec = {
  fontSets: [
    {
      name: "UI Fonts",
      density: [1.0, 2.0],
      families: ["Arial", "Helvetica"],
      styles: ["normal", "italic"],
      weights: ["normal", "bold"],
      sizes: [[12, 18, 2]]  // 12, 14, 16, 18
    },
    {
      name: "Monospace Fonts",
      density: [1.0],
      families: ["Courier New"],
      styles: ["normal"],
      weights: ["normal"],
      sizes: [10, 12, 14, 16]
    }
  ]
};

const generator = new FontSetGenerator(spec);
const info = generator.getSetsInfo();
// [{ name: "UI Fonts", count: 32 }, { name: "Monospace Fonts", count: 4 }]

console.log(`Total configurations: ${generator.getCount()}`);  // 36

// Process each set's fonts
for (const fontProps of generator.iterator()) {
  console.log(fontProps.idString);
}

Usage Example: Weight Ranges

const spec = {
  fontSets: [
    {
      name: "Arial Weight Spectrum",
      density: [1.0],
      families: ["Arial"],
      styles: ["normal"],
      weights: [[100, 900, 100]],  // 100, 200, 300, ..., 900
      sizes: [16]
    }
  ]
};

const generator = new FontSetGenerator(spec);
console.log(`Testing ${generator.getCount()} weight variations`);  // 9

generator.forEach((fontProps, index, total) => {
  console.log(`[${index + 1}/${total}] ${fontProps.idString}`);
});

Memory Efficiency

FontSetGenerator is designed for large-scale font generation:

Example: Generating 10,000 font configurations uses memory proportional to the number of unique values in each property array, not 10,000 FontProperties instances.

Validation

The generator validates specifications and throws descriptive errors:

try {
  const generator = new FontSetGenerator(invalidSpec);
} catch (error) {
  console.error(error.message);
  // "Font set specification must contain 'fontSets' array"
  // "Set 1: Missing required field 'families'"
  // "Invalid range: start (24) > stop (12)"
}

See docs/FONT_SET_FORMAT.md for:

Internal Store Classes

These classes are used internally by BitmapText and also available for font-assets-builder:

AtlasDataStore

Manages atlas images - used by font-assets-builder, internal to BitmapText static class.

FontMetricsStore

Manages font metrics - used by font-assets-builder, internal to BitmapText static class.

Note: End users of the static BitmapText API don’t need to interact with these classes directly.

Build Instructions

Development Setup

  # Clone repository
  git clone [repository-url]

  # Serve locally (required for CORS)
  python -m http.server
  # or
  npx http-server

  # Open in browser
  http://localhost:8000/public/font-assets-builder.html

Building Font Data

  1. Configure specs in src/specs/default-specs.js or via UI
  2. Use Font Builder to generate atlases
  3. Compressed data saved to font-assets/

Testing and Examples

Minimal Demo Open public/hello-world-demo.html for a simple “Hello World” example showing basic usage.

Multi-Size Demo Open public/hello-world-multi-size.html to see text rendered at multiple font sizes (18, 18.5, 19), demonstrating the complexity of loading multiple bitmap font configurations.

Small Text Rendering Demo Open public/small-text-rendering-demo.html to see small font size interpolation in action. Demonstrates sizes 0px through 9px, where sizes < 9px automatically interpolate metrics from 9px and render as placeholder rectangles. Shows both visual rendering and measurement accuracy across all sizes. See also public/small-text-rendering-demo-bundled.html for the production bundle version.

Baseline & Alignment Demo Open public/baseline-alignment-demo.html for an interactive demonstration of all baseline and alignment combinations, with side-by-side comparison of BitmapText vs native Canvas rendering. Includes controls for font selection, size, pixel density, and text samples.

Node.js Usage

  # Build all demos and runtime bundles, then run all Node.js demos (RECOMMENDED)
  npm run demo

  # Or step-by-step:
  # 1. Build runtime bundles
  npm run build

  # 2. Build all Node.js demos (standalone + bundled)
  npm run build-node-demos

  # 3. Run all Node.js demos
  npm run run-node-demos

  # Individual demos:
  node examples/node/dist/hello-world.bundle.js           # Single-size demo
  node examples/node/dist/hello-world-multi-size.bundle.js # Multi-size demo
  node examples/node/dist/small-sizes.bundle.js            # Small sizes interpolation

Demo descriptions:

All demos are self-contained scripts with no npm dependencies, built from modular source files. See examples/node/README.md for details.

Full Test Suite Open public/test-renderer.html to run visual tests and hash verification.

Tests verify:

Automated Browser Testing Capture screenshots of browser rendering using Playwright:

  node scripts/screenshot-with-playwright.js
  node scripts/screenshot-with-playwright.js --url public/baseline-alignment-demo.html --output baseline.png

See docs/PLAYWRIGHT_AUTOMATION.md and scripts/README.md for details.

Performance Benchmarks

Comprehensive performance testing suite with two benchmark types:

1. Rendering Benchmarks (drawTextFromAtlas performance)

Browser tests use three-phase progressive FPS testing:

Node.js tests use adaptive timing:

  ./perf/node/run-rendering-benchmarks.sh

2. Measurement Benchmarks (measureText performance)

Browser tests measure text dimension calculation speed:

Node.js tests verify linear O(n) scaling:

  ./perf/node/run-measurement-benchmarks.sh

What’s Measured:

See perf/README.md for complete documentation, methodology, and interpretation guide.

Troubleshooting

CORS Issues

QOI Format and Pipeline

Rendering Issues

Node.js Issues

Performance Issues

Image Formats

BitmapText.js uses different image formats optimized for each platform:

Browser (WebP):

Node.js (QOI):

Export (QOI):

Browser Support

Modern browsers with WebP (lossless) support:

Minimum requirement: Safari 14 for WebP support

Project Structure

  /
  ├── src/               # Source code
  │   ├── runtime/       # Runtime library classes
  │   ├── builder/       # Font assets building classes
  │   ├── platform/      # Platform-specific loaders
  │   ├── node/          # Node.js demo source code
  │   ├── utils/         # Utility functions
  │   ├── ui/            # UI components
  │   └── specs/         # Font specifications
  ├── public/            # HTML entry points
  ├── font-assets/       # Generated font assets
  ├── examples/          # Example applications
  │   └── node/         # Node.js demo applications
  ├── test/              # Test utilities and data
  ├── tools/             # Development tools
  ├── lib/               # Third-party libraries
  ├── docs/              # Documentation
  └── scripts/           # Automation and build scripts

Architecture

See docs/ARCHITECTURE.md for detailed system design information.

License

See LICENSE file.