From 495f76aaad8fb7a3d3225b2969f8a6cf56718a27 Mon Sep 17 00:00:00 2001 From: flop Date: Fri, 22 May 2026 18:31:54 +0200 Subject: [PATCH] fix: font rendering --- ts/src/browser.ts | 2 +- ts/src/font/index.ts | 8 +++++++- ts/src/font/u8g2.ts | 29 +++++++++++++++-------------- ts/src/renderer.ts | 38 ++++++++++++++++++++++++++------------ 4 files changed, 49 insertions(+), 28 deletions(-) diff --git a/ts/src/browser.ts b/ts/src/browser.ts index ca4a7c7..eaa9529 100644 --- a/ts/src/browser.ts +++ b/ts/src/browser.ts @@ -88,7 +88,7 @@ const EL_TYPES: MonoDisplay.ElementType[] = Object.keys(EL_FIELDS).map( (x) => MonoDisplay.StringToElementType[x as MonoDisplay.ElementTypeName] ); -const DEFAULT_FONTS = ["Font 1", "Font 2", "Font 3", "Font 4", "Font 5"]; +const DEFAULT_FONTS = Object.keys(MonoDisplay.MonoDisplayRenderer.builtinFonts); // --- Confirm dialog ---------------------------------------------------------- function showConfirm( diff --git a/ts/src/font/index.ts b/ts/src/font/index.ts index 9e6efa6..d60edcf 100644 --- a/ts/src/font/index.ts +++ b/ts/src/font/index.ts @@ -1,7 +1,9 @@ +import { u8g2_font_5x7_mf_u8g2font } from "./fonts/u8g2_font_5x7_mf_u8g2font"; import { u8g2_font_5x7_tf_u8g2font } from "./fonts/u8g2_font_5x7_tf.u8g2font"; import { u8g2_font_HelvetiPixel_tr_u8g2font } from "./fonts/u8g2_font_HelvetiPixel_tr_u8g2font"; import { u8g2_font_micropixel_tf_u8g2font } from "./fonts/u8g2_font_micropixel_tf_u8g2font"; import { u8g2_font_micropixel_tr_u8g2font } from "./fonts/u8g2_font_micropixel_tr_u8g2font"; +import { u8g2_font_NokiaSmallPlain_tf_u8g2font } from "./fonts/u8g2_font_NokiaSmallPlain_tf_u8g2font"; import { u8g2_font_smolfont_tf_u8g2font } from "./fonts/u8g2_font_smolfont_tf_u8g2font"; import { u8g2_font_spleen12x24_me_u8g2font } from "./fonts/u8g2_font_spleen12x24_me_u8g2font"; import { MonoDisplayFont } from "./types"; @@ -10,9 +12,13 @@ import { MonoDisplayFont } from "./types"; export * from "./types"; export * from "./u8g2"; + +export const u8g2_font_NokiaSmallPlain_tf = new MonoDisplayFont(u8g2_font_NokiaSmallPlain_tf_u8g2font); +export const u8g2_font_5x7_mf = new MonoDisplayFont(u8g2_font_5x7_mf_u8g2font); + export const u8g2_font_5x7_tf = new MonoDisplayFont(u8g2_font_5x7_tf_u8g2font); export const u8g2_font_HelvetiPixel_tr = new MonoDisplayFont(u8g2_font_HelvetiPixel_tr_u8g2font); export const u8g2_font_spleen12x24_me = new MonoDisplayFont(u8g2_font_spleen12x24_me_u8g2font); export const u8g2_font_smolfont_tf = new MonoDisplayFont(u8g2_font_smolfont_tf_u8g2font); export const u8g2_font_micropixel_tf = new MonoDisplayFont(u8g2_font_micropixel_tf_u8g2font); -export const u8g2_font_micropixel_tr = new MonoDisplayFont(u8g2_font_micropixel_tr_u8g2font); \ No newline at end of file +export const u8g2_font_micropixel_tr = new MonoDisplayFont(u8g2_font_micropixel_tr_u8g2font); diff --git a/ts/src/font/u8g2.ts b/ts/src/font/u8g2.ts index a0e6659..8adf212 100644 --- a/ts/src/font/u8g2.ts +++ b/ts/src/font/u8g2.ts @@ -7,7 +7,7 @@ const HDR_SIZE = 23; // --------------------------------------------------------------------------- -// BitReader — LSB-first, matching u8g2_font_decode_get_unsigned_bits exactly: +// BitReader - LSB-first, matching u8g2_font_decode_get_unsigned_bits exactly: // val = *ptr >> bit_pos // if (bit_pos + cnt >= 8): val |= *(ptr+1) << (8 - bit_pos); ptr++ // val &= (1 << cnt) - 1 @@ -67,7 +67,7 @@ function parseHeader(font: Uint8Array) { } // --------------------------------------------------------------------------- -// RLE decoder — matches fd_decode / u8g2_font_decode_glyph in the C source. +// RLE decoder - matches fd_decode / u8g2_font_decode_glyph in the C source. // // The bitstream is a sequence of (zeros-run, ones-run) pairs, repeated. // Each pair: @@ -106,7 +106,7 @@ function decodeRLE( // Glyph result // --------------------------------------------------------------------------- export interface U8G2GlyphData { - cols: Uint8Array; // column bytes (bit 0 = top row), length = w + cols: Uint8Array | Uint32Array; // column words (bit 0 = top row), length = w w: number; h: number; dx: number; // horizontal advance @@ -115,7 +115,7 @@ export interface U8G2GlyphData { // --------------------------------------------------------------------------- // Decode the glyph bitstream at byteOffset (after the record header bytes). -// Field order from C source (u8g2_font.c lines 590–625): +// Field order from C source (u8g2_font.c lines 590-625): // glyph_width, glyph_height, x (signed), y (signed), delta_x (signed) // --------------------------------------------------------------------------- function decodeGlyphAt( @@ -127,20 +127,21 @@ function decodeGlyphAt( const w = br.read(hdr.bitsPerW); const h = br.read(hdr.bitsPerH); - /* x */ br.readSigned(hdr.bitsPerX); // x bearing — consumed but not needed + /* x */ br.readSigned(hdr.bitsPerX); // x bearing - consumed but not needed const yB = br.readSigned(hdr.bitsPerY); const dx = br.readSigned(hdr.bitsPerDx); if (w === 0 || h === 0) { - // Blank glyph (e.g. space) — advance is stored in dx + // Blank glyph (e.g. space) - advance is stored in dx return { cols: new Uint8Array(1), w: 1, h: 0, dx: Math.max(dx, 1), yOff: 0 }; } // Decode row-major horizontal bitmap const bits = decodeRLE(br, w, h, hdr.m0, hdr.m1); - // Convert row-major → column bytes (bit 0 = top row) for rasterizeText - const cols = new Uint8Array(w); + // Convert row-major → column words (bit 0 = top row) for rasterizeText + // Must be Uint32Array - glyphs can be up to 32px tall; Uint8Array truncates rows 8+ + const cols = new Uint32Array(w); for (let row = 0; row < h; row++) { for (let col = 0; col < w; col++) { if (bits[row * w + col]) cols[col] |= (1 << row); @@ -161,12 +162,12 @@ function decodeGlyphAt( // Record layout (cp < 0x0100): // [1 byte] encoding // [1 byte] jump = byte distance from START of this record to next record -// [bitstream …] +// [bitstream ...] // // Record layout (cp >= 0x0100): // [2 bytes BE] encoding // [1 byte] jump (same semantics) -// [bitstream …] +// [bitstream ...] // // Unicode section is preceded by a lookup table (added in v2.23): // pairs of [uint16 BE delta | uint16 BE last-encoding], terminated by last==0xFFFF @@ -178,7 +179,7 @@ export function u8g2Glyph(cp: number, font: Uint8Array): U8G2GlyphData | null { try { if (cp >= 0x0100) { - // ── Unicode block ──────────────────────────────────────────────────── + // -- Unicode block ---------------------------------------------------- const tableBase = hdr.startUnicode; if (tableBase + 4 > font.length) return null; @@ -214,7 +215,7 @@ export function u8g2Glyph(cp: number, font: Uint8Array): U8G2GlyphData | null { return null; } else { - // ── ASCII block (cp < 0x0100) ──────────────────────────────────────── + // -- ASCII block (cp < 0x0100) ---------------------------------------- // Jump-start from the nearest block offset to avoid scanning from byte 0. let recPos: number; if (cp >= 0x61) recPos = hdr.startLower; @@ -265,7 +266,7 @@ export function rasterizeText( const pixels = new Uint8Array(totalW * cellHeight); // Centre the full font (ascent + |descent|) vertically in the cell. - // header byte 14 is descent of 'g' — stored as a negative signed value. + // header byte 14 is descent of 'g' - stored as a negative signed value. const descent = font.FONT_DATA[14]! > 127 ? font.FONT_DATA[14]! - 256 : font.FONT_DATA[14]!; const fontH = hdr.ascentA - descent; // total font height in pixels const topPad = Math.floor((cellHeight - fontH) / 2); @@ -290,4 +291,4 @@ export function rasterizeText( } return { pixels, width: totalW, height: cellHeight }; -} \ No newline at end of file +} diff --git a/ts/src/renderer.ts b/ts/src/renderer.ts index bb06c78..d8a34c9 100644 --- a/ts/src/renderer.ts +++ b/ts/src/renderer.ts @@ -16,7 +16,17 @@ import { type MonoFormatVScroll } from "./types.js"; import { type MonoDisplayDriverOptions } from "./driver"; -import { rasterizeText, MonoDisplayFont, u8g2_font_5x7_tf, u8g2_font_HelvetiPixel_tr, u8g2_font_spleen12x24_me, u8g2_font_smolfont_tf, u8g2_font_micropixel_tf, u8g2_font_micropixel_tr } from "./font"; +import { + rasterizeText, MonoDisplayFont, + u8g2_font_5x7_tf, + u8g2_font_HelvetiPixel_tr, + u8g2_font_spleen12x24_me, + u8g2_font_smolfont_tf, + u8g2_font_micropixel_tf, + u8g2_font_micropixel_tr, + u8g2_font_NokiaSmallPlain_tf, + u8g2_font_5x7_mf +} from "./font"; /** * Renders a MonoFormatFile onto an HTMLCanvasElement. * Uses setInterval at 1000/fps ms per tick. All element state (animation @@ -38,21 +48,22 @@ export class MonoDisplayRenderer { private vScrollPos: Map = new Map(); private customFonts: MonoDisplayFont[] = []; private textCache: Map = new Map(); - private builtinFonts: MonoDisplayFont[]; + public static readonly builtinFonts: Record = { + "NokiaSmallPlain_tf": u8g2_font_NokiaSmallPlain_tf, + "5x7_mf": u8g2_font_5x7_mf, + // "micropixel_tf": u8g2_font_micropixel_tf, + // "micropixel_tr": u8g2_font_micropixel_tr, + // "smolfont_tf": u8g2_font_smolfont_tf, + // "spleen12x24_me": u8g2_font_spleen12x24_me, + // "spleen12x24_me": u8g2_font_spleen12x24_me, + // "5x7_tf": u8g2_font_5x7_tf, + }; constructor(canvas: HTMLCanvasElement, opts: Required) { const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Cannot get 2D canvas context"); this.ctx = ctx; this.opts = opts; - this.builtinFonts = [ - u8g2_font_micropixel_tf, - u8g2_font_micropixel_tr, - u8g2_font_smolfont_tf, - u8g2_font_spleen12x24_me, - u8g2_font_HelvetiPixel_tr, - u8g2_font_5x7_tf, - ]; } stop(): void { @@ -239,8 +250,11 @@ export class MonoDisplayRenderer { const key = `${text}|${fontIndex}|${height}`; const cached = this.textCache.get(key); if (cached) return cached; - const customFont = fontIndex >= 0x8000 ? this.customFonts[fontIndex - 0x8000] : this.builtinFonts[fontIndex]; - const img = rasterizeText(text, height, customFont); + fontIndex = Math.min(fontIndex, Object.keys(MonoDisplayRenderer.builtinFonts).length - 1); + const builtinFont: MonoDisplayFont = Object.values(MonoDisplayRenderer.builtinFonts).at(fontIndex) || u8g2_font_NokiaSmallPlain_tf; + const customFont: MonoDisplayFont | undefined = this.customFonts[fontIndex - 0x8000]; + const selectedFont: MonoDisplayFont = fontIndex >= 0x8000 ? (customFont || builtinFont) : builtinFont; + const img = rasterizeText(text, height, selectedFont); this.textCache.set(key, img); return img; }