147 lines
3.9 KiB
TypeScript
147 lines
3.9 KiB
TypeScript
const CHAR_ORDER = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
|
|
interface CharInfo {
|
|
char: string;
|
|
x: number;
|
|
w: number;
|
|
}
|
|
|
|
const fontCache = new Map<string, { chars: CharInfo[]; h: number; sheet: HTMLImageElement }>();
|
|
|
|
function hexPixel(data: Uint8ClampedArray, x: number, y: number, w: number): number {
|
|
const i = (y * w + x) * 4;
|
|
return (data[i] << 24) | (data[i + 1] << 16) | (data[i + 2] << 8) | data[i + 3];
|
|
}
|
|
|
|
function isTransparent(data: Uint8ClampedArray, x: number, y: number, w: number): boolean {
|
|
return data[(y * w + x) * 4 + 3] === 0;
|
|
}
|
|
|
|
function colEmpty(data: Uint8ClampedArray, x: number, w: number, h: number): boolean {
|
|
for (let y = 0; y < h; y++) {
|
|
if (!isTransparent(data, x, y, w)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function scanCharWidths(data: Uint8ClampedArray, sw: number, sh: number): CharInfo[] {
|
|
const chars: CharInfo[] = [];
|
|
let ci = 0;
|
|
let x = 0;
|
|
|
|
while (x < sw && ci < CHAR_ORDER.length) {
|
|
// skip transparent columns
|
|
while (x < sw && colEmpty(data, x, sw, sh)) x++;
|
|
if (x >= sw) break;
|
|
|
|
const start = x;
|
|
// find end of character
|
|
while (x < sw && !colEmpty(data, x, sw, sh)) x++;
|
|
const end = x;
|
|
|
|
const w = end - start;
|
|
if (w > 0) {
|
|
chars.push({ char: CHAR_ORDER[ci], x: start, w });
|
|
ci++;
|
|
}
|
|
}
|
|
|
|
return chars;
|
|
}
|
|
|
|
export async function getFontInfo(font: string): Promise<{ chars: CharInfo[]; h: number; sheet: HTMLImageElement }> {
|
|
const cached = fontCache.get(font);
|
|
if (cached) return cached;
|
|
|
|
const img = new Image();
|
|
img.crossOrigin = "anonymous";
|
|
await new Promise<void>((resolve, reject) => {
|
|
img.onload = () => resolve();
|
|
img.onerror = () => reject(new Error(`Failed to load sprite for ${font}`));
|
|
img.src = `/assets/fonts/sheets/${font}.gif`;
|
|
});
|
|
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = img.naturalWidth;
|
|
canvas.height = img.naturalHeight;
|
|
const ctx = canvas.getContext("2d")!;
|
|
ctx.drawImage(img, 0, 0);
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
|
|
const chars = scanCharWidths(imageData.data, canvas.width, canvas.height);
|
|
|
|
// measure total height by finding first/last non-empty row
|
|
let top = canvas.height, bottom = 0;
|
|
for (let y = 0; y < canvas.height; y++) {
|
|
for (let x = 0; x < canvas.width; x++) {
|
|
if (!isTransparent(imageData.data, x, y, canvas.width)) {
|
|
top = Math.min(top, y);
|
|
bottom = Math.max(bottom, y);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
const h = bottom - top + 1;
|
|
|
|
const result = { chars, h, sheet: img };
|
|
fontCache.set(font, result);
|
|
return result;
|
|
}
|
|
|
|
export function measureText(chars: CharInfo[], h: number, text: string, spaceW: number): { totalW: number } {
|
|
const map = new Map(chars.map((c) => [c.char, c]));
|
|
let totalW = 0;
|
|
for (const ch of text.toLowerCase()) {
|
|
if (ch === " ") {
|
|
totalW += spaceW;
|
|
} else {
|
|
const info = map.get(ch);
|
|
totalW += info?.w ?? spaceW;
|
|
}
|
|
}
|
|
return { totalW };
|
|
}
|
|
|
|
export function renderToCanvas(
|
|
chars: CharInfo[],
|
|
h: number,
|
|
sheet: HTMLImageElement,
|
|
text: string,
|
|
spaceW: number,
|
|
dpr = 1,
|
|
): HTMLCanvasElement | null {
|
|
const map = new Map(chars.map((c) => [c.char, c]));
|
|
const safeText = text.toLowerCase();
|
|
|
|
let totalW = 0;
|
|
for (const ch of safeText) {
|
|
totalW += ch === " " ? spaceW : (map.get(ch)?.w ?? spaceW);
|
|
}
|
|
|
|
if (totalW === 0) return null;
|
|
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = Math.round(totalW * dpr);
|
|
canvas.height = Math.round(h * dpr);
|
|
const ctx = canvas.getContext("2d");
|
|
if (!ctx) return null;
|
|
ctx.scale(dpr, dpr);
|
|
|
|
let x = 0;
|
|
for (const ch of safeText) {
|
|
if (ch === " ") {
|
|
x += spaceW;
|
|
} else {
|
|
const info = map.get(ch);
|
|
if (info) {
|
|
ctx.drawImage(sheet, info.x, 0, info.w, sheet.naturalHeight, x, 0, info.w, h);
|
|
x += info.w;
|
|
} else {
|
|
x += spaceW;
|
|
}
|
|
}
|
|
}
|
|
|
|
return canvas;
|
|
}
|