'use strict'; /* * make-pixelfont.js – erzeugt src/assets/aeropixel.ttf * ----------------------------------------------------- * Eigene Pixel-Schrift für das Minecraft-Design (keine fremde Schrift, also * keine Lizenzfragen). Jede Glyphe ist ein Raster aus '#' (Pixel) und ' ': * Zeile 0 ist die Oberkante der Großbuchstaben, Zeile 6 sitzt auf der * Grundlinie, Zeilen 7–8 sind Unterlängen (g, j, p, q, y, Komma). * Fehlende Zeichen fallen im Launcher automatisch auf Segoe UI zurück. * * Aufruf: node scripts/make-pixelfont.js */ const fs = require('fs'); const path = require('path'); const P = 125; // ein Pixel in Font-Einheiten (8 Pixel = 1 em) const EM = 1000; const ASC = 1000; // 7 Zeilen Großbuchstaben + 1 Zeile Luft const DESC = 250; // 2 Zeilen Unterlänge const G = { A: [' ### ', '# #', '# #', '#####', '# #', '# #', '# #'], B: ['#### ', '# #', '# #', '#### ', '# #', '# #', '#### '], C: [' ### ', '# #', '# ', '# ', '# ', '# #', ' ### '], D: ['#### ', '# #', '# #', '# #', '# #', '# #', '#### '], E: ['#####', '# ', '# ', '#### ', '# ', '# ', '#####'], F: ['#####', '# ', '# ', '#### ', '# ', '# ', '# '], G: [' ####', '# ', '# ', '# ##', '# #', '# #', ' ####'], H: ['# #', '# #', '# #', '#####', '# #', '# #', '# #'], I: ['###', ' # ', ' # ', ' # ', ' # ', ' # ', '###'], J: [' #', ' #', ' #', ' #', ' #', '# #', ' ### '], K: ['# #', '# # ', '# # ', '## ', '# # ', '# # ', '# #'], L: ['# ', '# ', '# ', '# ', '# ', '# ', '#####'], M: ['# #', '## ##', '# # #', '# #', '# #', '# #', '# #'], N: ['# #', '## #', '# # #', '# ##', '# #', '# #', '# #'], O: [' ### ', '# #', '# #', '# #', '# #', '# #', ' ### '], P: ['#### ', '# #', '# #', '#### ', '# ', '# ', '# '], Q: [' ### ', '# #', '# #', '# #', '# #', '# # ', ' ## #'], R: ['#### ', '# #', '# #', '#### ', '# # ', '# #', '# #'], S: [' ####', '# ', '# ', ' ### ', ' #', ' #', '#### '], T: ['#####', ' # ', ' # ', ' # ', ' # ', ' # ', ' # '], U: ['# #', '# #', '# #', '# #', '# #', '# #', ' ### '], V: ['# #', '# #', '# #', '# #', '# #', ' # # ', ' # '], W: ['# #', '# #', '# #', '# #', '# # #', '## ##', '# #'], X: ['# #', '# #', ' # # ', ' # ', ' # # ', '# #', '# #'], Y: ['# #', '# #', ' # # ', ' # ', ' # ', ' # ', ' # '], Z: ['#####', ' #', ' # ', ' # ', ' # ', '# ', '#####'], a: [' ', ' ', ' ### ', ' #', ' ####', '# #', ' ####'], b: ['# ', '# ', '# ## ', '## #', '# #', '# #', '#### '], c: [' ', ' ', ' ### ', '# #', '# ', '# #', ' ### '], d: [' #', ' #', ' ## #', '# ##', '# #', '# #', ' ####'], e: [' ', ' ', ' ### ', '# #', '#####', '# ', ' ####'], f: [' ##', ' # ', '####', ' # ', ' # ', ' # ', ' # '], g: [' ', ' ', ' ####', '# #', '# #', '# #', ' ####', ' #', ' ### '], h: ['# ', '# ', '# ## ', '## #', '# #', '# #', '# #'], i: ['#', ' ', '#', '#', '#', '#', '#'], j: [' #', ' ', ' #', ' #', ' #', ' #', ' #', '# #', ' ## '], k: ['# ', '# ', '# #', '# # ', '## ', '# # ', '# #'], l: ['# ', '# ', '# ', '# ', '# ', '# ', ' #'], m: [' ', ' ', '## # ', '# # #', '# # #', '# #', '# #'], n: [' ', ' ', '#### ', '# #', '# #', '# #', '# #'], o: [' ', ' ', ' ### ', '# #', '# #', '# #', ' ### '], p: [' ', ' ', '# ## ', '## #', '# #', '# #', '#### ', '# ', '# '], q: [' ', ' ', ' ## #', '# ##', '# #', '# #', ' ####', ' #', ' #'], r: [' ', ' ', '# ## ', '## #', '# ', '# ', '# '], s: [' ', ' ', ' ####', '# ', ' ### ', ' #', '#### '], t: [' # ', ' # ', '###', ' # ', ' # ', ' # ', ' #'], u: [' ', ' ', '# #', '# #', '# #', '# #', ' ####'], v: [' ', ' ', '# #', '# #', '# #', ' # # ', ' # '], w: [' ', ' ', '# #', '# #', '# # #', '# # #', ' ####'], x: [' ', ' ', '# #', ' # # ', ' # ', ' # # ', '# #'], y: [' ', ' ', '# #', '# #', '# #', '# #', ' ####', ' #', '#### '], z: [' ', ' ', '#####', ' # ', ' # ', ' # ', '#####'], 0: [' ### ', '# #', '# ##', '# # #', '## #', '# #', ' ### '], 1: [' # ', ' ## ', ' # ', ' # ', ' # ', ' # ', '#####'], 2: [' ### ', '# #', ' #', ' ## ', ' # ', '# ', '#####'], 3: [' ### ', '# #', ' #', ' ## ', ' #', '# #', ' ### '], 4: [' ##', ' # #', ' # #', '# #', '#####', ' #', ' #'], 5: ['#####', '# ', '#### ', ' #', ' #', '# #', ' ### '], 6: [' ## ', ' # ', '# ', '#### ', '# #', '# #', ' ### '], 7: ['#####', '# #', ' #', ' # ', ' # ', ' # ', ' # '], 8: [' ### ', '# #', '# #', ' ### ', '# #', '# #', ' ### '], 9: [' ### ', '# #', '# #', ' ####', ' #', ' # ', ' ## '], 'Ä': ['# #', ' ### ', '# #', '# #', '#####', '# #', '# #'], 'Ö': ['# #', ' ### ', '# #', '# #', '# #', '# #', ' ### '], 'Ü': ['# #', ' ', '# #', '# #', '# #', '# #', ' ### '], 'ä': [' # # ', ' ', ' ### ', ' #', ' ####', '# #', ' ####'], 'ö': [' # # ', ' ', ' ### ', '# #', '# #', '# #', ' ### '], 'ü': [' # # ', ' ', '# #', '# #', '# #', '# #', ' ####'], 'ß': [' ### ', '# #', '# # ', '# # ', '# # ', '# #', '# ## '], 'é': [' # ', ' # ', ' ### ', '# #', '#####', '# ', ' ####'], '.': [' ', ' ', ' ', ' ', ' ', ' ', '#'], ',': [' ', ' ', ' ', ' ', ' ', ' ', '#', '#'], ':': [' ', ' ', '#', ' ', ' ', ' ', '#'], ';': [' ', ' ', '#', ' ', ' ', ' ', '#', '#'], '!': ['#', '#', '#', '#', '#', ' ', '#'], '?': [' ### ', '# #', ' #', ' # ', ' # ', ' ', ' # '], '-': [' ', ' ', ' ', '####', ' ', ' ', ' '], '–': [' ', ' ', ' ', '#####', ' ', ' ', ' '], '_': [' ', ' ', ' ', ' ', ' ', ' ', ' ', '#####'], '(': [' #', ' # ', '# ', '# ', '# ', ' # ', ' #'], ')': ['# ', ' # ', ' #', ' #', ' #', ' # ', '# '], '[': ['##', '# ', '# ', '# ', '# ', '# ', '##'], ']': ['##', ' #', ' #', ' #', ' #', ' #', '##'], '/': [' #', ' # ', ' # ', ' # ', ' # ', ' # ', '# '], '\\': ['# ', ' # ', ' # ', ' # ', ' # ', ' # ', ' #'], '+': [' ', ' # ', ' # ', '#####', ' # ', ' # ', ' '], '=': [' ', ' ', '#####', ' ', '#####', ' ', ' '], '*': [' ', ' ', '# # #', ' ### ', '# # #', ' ', ' '], '%': ['# #', ' #', ' # ', ' # ', ' # ', '# ', '# #'], '&': [' ## ', '# # ', ' ## ', ' ## #', '# # ', '# # ', ' ## #'], '#': [' # # ', '#####', ' # # ', ' # # ', ' # # ', '#####', ' # # '], '<': [' #', ' # ', ' # ', '# ', ' # ', ' # ', ' #'], '>': ['# ', ' # ', ' # ', ' #', ' # ', ' # ', '# '], "'": ['#', '#'], '"': ['# #', '# #'], '|': ['#', '#', '#', '#', '#', '#', '#', '#', '#'], '·': [' ', ' ', ' ', '#', ' ', ' ', ' '], '…': [' ', ' ', ' ', ' ', ' ', ' ', '# # #'], '@': [' ### ', '# #', '# ###', '# # #', '# ###', '# ', ' ####'], }; // ---------- Glyphen in Umrisse umwandeln (waagerechte Pixel-Läufe als Rechtecke) ---------- function umrisse(zeilen) { const konturen = []; zeilen.forEach((zeile, r) => { const yOben = (7 - r) * P; const yUnten = yOben - P; let x = 0; while (x < zeile.length) { if (zeile[x] !== '#') { x++; continue; } let ende = x; while (ende < zeile.length && zeile[ende] === '#') ende++; // im Uhrzeigersinn (TrueType-Außenkontur) konturen.push([[x * P, yUnten], [x * P, yOben], [ende * P, yOben], [ende * P, yUnten]]); x = ende; } }); return konturen; } const glyphen = [{ name: '.notdef', code: null, breite: 4, konturen: [[[0, 0], [0, 875], [500, 875], [500, 0]], [[125, 125], [375, 125], [375, 750], [125, 750]]] }, { name: 'space', code: 32, breite: 3, konturen: [] }]; for (const [zeichen, zeilen] of Object.entries(G)) { const breite = Math.max(...zeilen.map((z) => z.length)); glyphen.push({ code: zeichen.codePointAt(0), breite, konturen: umrisse(zeilen) }); } const sortiert = [glyphen[0], glyphen[1], ...glyphen.slice(2).sort((a, b) => a.code - b.code)]; // ---------- Hilfen zum Schreiben ---------- class Puffer { constructor() { this.teile = []; this.laenge = 0; } u8(v) { const b = Buffer.alloc(1); b.writeUInt8(v); return this.roh(b); } u16(v) { const b = Buffer.alloc(2); b.writeUInt16BE(v & 0xffff); return this.roh(b); } i16(v) { const b = Buffer.alloc(2); b.writeInt16BE(v); return this.roh(b); } u32(v) { const b = Buffer.alloc(4); b.writeUInt32BE(v >>> 0); return this.roh(b); } i64(v) { const b = Buffer.alloc(8); b.writeBigInt64BE(BigInt(v)); return this.roh(b); } roh(b) { this.teile.push(b); this.laenge += b.length; return this; } fertig() { return Buffer.concat(this.teile); } } const auf4 = (b) => (b.length % 4 ? Buffer.concat([b, Buffer.alloc(4 - (b.length % 4))]) : b); function pruefsumme(b) { const p = auf4(b); let s = 0; for (let i = 0; i < p.length; i += 4) s = (s + p.readUInt32BE(i)) >>> 0; return s; } // ---------- glyf + loca + hmtx ---------- const glyf = new Puffer(); const loca = []; const hmtx = new Puffer(); let maxPunkte = 0, maxKonturen = 0; let gxMin = 0, gyMin = 0, gxMax = 0, gyMax = 0; let maxVorschub = 0, minLsb = 1e9, minRsb = 1e9, maxXAusdehnung = 0; for (const g of sortiert) { loca.push(glyf.laenge); const vorschub = (g.breite + 1) * P; maxVorschub = Math.max(maxVorschub, vorschub); if (!g.konturen.length) { hmtx.u16(vorschub).i16(0); continue; } const punkte = g.konturen.flat(); const xs = punkte.map((p) => p[0]), ys = punkte.map((p) => p[1]); const xMin = Math.min(...xs), xMax = Math.max(...xs), yMin = Math.min(...ys), yMax = Math.max(...ys); gxMin = Math.min(gxMin, xMin); gyMin = Math.min(gyMin, yMin); gxMax = Math.max(gxMax, xMax); gyMax = Math.max(gyMax, yMax); minLsb = Math.min(minLsb, xMin); minRsb = Math.min(minRsb, vorschub - xMax); maxXAusdehnung = Math.max(maxXAusdehnung, xMax); maxPunkte = Math.max(maxPunkte, punkte.length); maxKonturen = Math.max(maxKonturen, g.konturen.length); const b = new Puffer(); b.i16(g.konturen.length).i16(xMin).i16(yMin).i16(xMax).i16(yMax); let ende = -1; for (const k of g.konturen) { ende += k.length; b.u16(ende); } b.u16(0); // keine Hinting-Anweisungen for (let i = 0; i < punkte.length; i++) b.u8(0x01); // alle Punkte auf der Kurve, Koordinaten als int16 let px = 0, py = 0; for (const [x] of punkte) { b.i16(x - px); px = x; } for (const [, y] of punkte) { b.i16(y - py); py = y; } glyf.roh(auf4(b.fertig())); hmtx.u16(vorschub).i16(xMin); } loca.push(glyf.laenge); const anzahl = sortiert.length; const locaB = new Puffer(); for (const o of loca) locaB.u32(o); // ---------- cmap (Format 4, ein Segment je Zeichen) ---------- const abbildung = sortiert.map((g, i) => [g.code, i]).filter(([c]) => c !== null).sort((a, b) => a[0] - b[0]); const segmente = abbildung.map(([c, i]) => ({ start: c, ende: c, delta: (i - c) & 0xffff })); segmente.push({ start: 0xffff, ende: 0xffff, delta: 1 }); const segX2 = segmente.length * 2; const suchbereich = 2 * 2 ** Math.floor(Math.log2(segmente.length)); const sub = new Puffer(); sub.u16(4).u16(16 + segmente.length * 8).u16(0).u16(segX2).u16(suchbereich) .u16(Math.log2(suchbereich / 2)).u16(segX2 - suchbereich); for (const s of segmente) sub.u16(s.ende); sub.u16(0); for (const s of segmente) sub.u16(s.start); for (const s of segmente) sub.u16(s.delta); for (const s of segmente) sub.u16(0); const cmap = new Puffer().u16(0).u16(1).u16(3).u16(1).u32(12).roh(sub.fertig()); // ---------- head / hhea / maxp / OS/2 / name / post ---------- const zeit = Math.floor(Date.UTC(2026, 9, 3) / 1000) + 2082844800; // Sekunden seit 1904 const head = new Puffer().u32(0x00010000).u32(0x00010000).u32(0).u32(0x5f0f3cf5).u16(0x000b).u16(EM) .i64(zeit).i64(zeit).i16(gxMin).i16(gyMin).i16(gxMax).i16(gyMax).u16(0).u16(8).i16(2).i16(1).i16(0); const hhea = new Puffer().u32(0x00010000).i16(ASC).i16(-DESC).i16(0).u16(maxVorschub).i16(minLsb).i16(minRsb) .i16(maxXAusdehnung).i16(1).i16(0).i16(0).i16(0).i16(0).i16(0).i16(0).i16(0).u16(anzahl); const maxp = new Puffer().u32(0x00010000).u16(anzahl).u16(maxPunkte).u16(maxKonturen).u16(0).u16(0) .u16(2).u16(0).u16(0).u16(0).u16(0).u16(0).u16(0).u16(0).u16(0); const codes = abbildung.map(([c]) => c); const os2 = new Puffer().u16(4).i16(5 * P).u16(400).u16(5).u16(0) .i16(650).i16(600).i16(0).i16(75).i16(650).i16(600).i16(0).i16(350).i16(P).i16(3 * P) .i16(0).roh(Buffer.alloc(10)).u32(3).u32(0).u32(0).u32(0).roh(Buffer.from('AERO')) .u16(0x40).u16(Math.min(...codes)).u16(Math.min(0xffff, Math.max(...codes))) .i16(ASC).i16(-DESC).i16(0).u16(ASC).u16(DESC).u32(1).u32(0).i16(5 * P).i16(7 * P).u16(0).u16(32).u16(1); const namen = { 1: 'AeroPixel', 2: 'Regular', 3: 'AeroPixel Regular', 4: 'AeroPixel', 5: 'Version 1.0', 6: 'AeroPixel-Regular' }; const nameDaten = Object.entries(namen).map(([id, text]) => [Number(id), Buffer.from(text, 'utf16le').swap16()]); const name = new Puffer().u16(0).u16(nameDaten.length).u16(6 + nameDaten.length * 12); let versatz = 0; for (const [id, b] of nameDaten) { name.u16(3).u16(1).u16(0x409).u16(id).u16(b.length).u16(versatz); versatz += b.length; } for (const [, b] of nameDaten) name.roh(b); const post = new Puffer().u32(0x00030000).u32(0).i16(-P).i16(P).u32(0).u32(0).u32(0).u32(0).u32(0); // ---------- zusammensetzen ---------- const tabellen = [ ['OS/2', os2], ['cmap', cmap], ['glyf', glyf], ['head', head], ['hhea', hhea], ['hmtx', hmtx], ['loca', locaB], ['maxp', maxp], ['name', name], ['post', post], ].map(([tag, p]) => [tag, p.fertig()]); const n = tabellen.length; const sb = 16 * 2 ** Math.floor(Math.log2(n)); const kopf = new Puffer().u32(0x00010000).u16(n).u16(sb).u16(Math.log2(sb / 16)).u16(n * 16 - sb); let offset = 12 + n * 16; const verzeichnis = new Puffer(); for (const [tag, daten] of tabellen) { verzeichnis.roh(Buffer.from(tag, 'latin1')).u32(pruefsumme(daten)).u32(offset).u32(daten.length); offset += auf4(daten).length; } let schrift = Buffer.concat([kopf.fertig(), verzeichnis.fertig(), ...tabellen.map(([, d]) => auf4(d))]); // checkSumAdjustment im head-Eintrag setzen const headOffset = 12 + n * 16 + tabellen.slice(0, 3).reduce((s, [, d]) => s + auf4(d).length, 0); schrift.writeUInt32BE((0xb1b0afba - pruefsumme(schrift)) >>> 0, headOffset + 8); const ziel = path.join(__dirname, '..', 'src', 'assets', 'aeropixel.ttf'); fs.writeFileSync(ziel, schrift); console.log(`${ziel}: ${anzahl} Glyphen, ${schrift.length} Bytes`);