271 lines
15 KiB
JavaScript
271 lines
15 KiB
JavaScript
'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`);
|