fix: font rendering

typescript_changes
flop 3 weeks ago
parent ef63b862ae
commit cca5be7e23
  1. 2
      ts/src/browser.ts
  2. 8
      ts/src/font/index.ts
  3. 29
      ts/src/font/u8g2.ts
  4. 38
      ts/src/renderer.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(

@ -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);
export const u8g2_font_micropixel_tr = new MonoDisplayFont(u8g2_font_micropixel_tr_u8g2font);

@ -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 590625):
// 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 };
}
}

@ -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<number, number> = new Map();
private customFonts: MonoDisplayFont[] = [];
private textCache: Map<string, MonoFormatPixelImage> = new Map();
private builtinFonts: MonoDisplayFont[];
public static readonly builtinFonts: Record<string, MonoDisplayFont> = {
"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<MonoDisplayDriverOptions>) {
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;
}

Loading…
Cancel
Save