From db022ffc366c8e81cd1120aa0190661e43d5dc32 Mon Sep 17 00:00:00 2001 From: user Date: Thu, 16 Jul 2026 14:53:25 +0200 Subject: font: full mixed-case + punctuation glyph set (not uppercase-only) Replace the small-caps placeholder font with a real baseline-aligned 4x8 set covering all 96 printable ASCII: distinct lowercase with true ascenders (b d f h k l t) and descenders (g j p q y), digits, and punctuation. Metrics: caps/ascenders rows 1-5, x-height rows 2-5, baseline row 5, descenders into rows 6-7. Glyphs are ~3px wide in the 4px cell -- cramped but legible; the tile mechanism itself imposes no character-set limit. genfont.py now takes per-glyph (top, rows) so glyphs sit on the baseline. Demo strings in term_test updated to show mixed case + punctuation. Verified by screenshot: descenders visibly drop below the baseline. --- tools/genfont.py | 201 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 106 insertions(+), 95 deletions(-) (limited to 'tools') diff --git a/tools/genfont.py b/tools/genfont.py index 7fe7689..2f94881 100644 --- a/tools/genfont.py +++ b/tools/genfont.py @@ -1,105 +1,116 @@ #!/usr/bin/env python3 -# Generate a 4x8 font (3x5 glyphs, left-aligned in a 4px cell) as RGBDS asm. +# Generate a 4x8 font as RGBDS asm. Glyphs are up to 3px wide (in a 4px cell, +# 1px right gap) and use the full 8px height with a real baseline at row 5: +# caps/ascenders rows 1..5 x-height (lowercase) rows 2..5 +# descenders reach rows 6..7 row 0 = top gap / line spacing # Output: 96 glyphs (ASCII 32..127), 8 bytes each; each byte = one 4-px row, -# bit3=leftmost pixel of the cell, bits 3..1 = the 3px glyph, bit0 = gap. +# bit3 = leftmost pixel of the cell, bits 3..1 = the glyph, bit0 = gap. G = {} -def d(ch, rows): # rows: 5 strings of width 3 ('#'/' ') - G[ch] = rows -# space -d(' ', [" "," "," "," "," "]) -# letters (uppercase; lowercase mapped to these) -d('A',["## ","# #","###","# #","# #"]) -d('B',["## ","# #","## ","# #","## "]) -d('C',[" ##","# ","# ","# "," ##"]) -d('D',["## ","# #","# #","# #","## "]) -d('E',["###","# ","## ","# ","###"]) -d('F',["###","# ","## ","# ","# "]) -d('G',[" ##","# ","# #","# #"," ##"]) -d('H',["# #","# #","###","# #","# #"]) -d('I',["###"," # "," # "," # ","###"]) -d('J',[" #"," #"," #","# #"," # "]) -d('K',["# #","# #","## ","# #","# #"]) -d('L',["# ","# ","# ","# ","###"]) -d('M',["# #","###","###","# #","# #"]) -d('N',["# #","###","###","###","# #"]) -d('O',[" # ","# #","# #","# #"," # "]) -d('P',["## ","# #","## ","# ","# "]) -d('Q',[" # ","# #","# #"," # "," #"]) -d('R',["## ","# #","## ","# #","# #"]) -d('S',[" ##","# "," # "," #","## "]) -d('T',["###"," # "," # "," # "," # "]) -d('U',["# #","# #","# #","# #"," ##"]) -d('V',["# #","# #","# #"," # "," # "]) -d('W',["# #","# #","###","###","# #"]) -d('X',["# #","# #"," # ","# #","# #"]) -d('Y',["# #","# #"," # "," # "," # "]) -d('Z',["###"," #"," # ","# ","###"]) -# digits -d('0',[" # ","# #","# #","# #"," # "]) -d('1',[" # ","## "," # "," # ","###"]) -d('2',["## "," #"," # ","# ","###"]) -d('3',["## "," #"," # "," #","## "]) -d('4',["# #","# #","###"," #"," #"]) -d('5',["###","# ","## "," #","## "]) -d('6',[" ##","# ","## ","# #"," # "]) -d('7',["###"," #"," # "," # "," # "]) -d('8',[" # ","# #"," # ","# #"," # "]) -d('9',[" # ","# #"," ##"," #","## "]) -# punctuation -d('.',[" "," "," "," "," # "]) -d(',',[" "," "," "," # ","# "]) -d(':',[" "," # "," "," # "," "]) -d(';',[" "," # "," "," # ","# "]) -d('!',[" # "," # "," # "," "," # "]) -d('?',["## "," #"," # "," "," # "]) -d('-',[" "," ","###"," "," "]) -d('+',[" "," # ","###"," # "," "]) -d('=',[" ","###"," ","###"," "]) -d('*',["# #"," # ","###"," # ","# #"]) -d('/',[" #"," #"," # ","# ","# "]) -d('\\',["# ","# "," # "," #"," #"]) -d('(',[" #"," # "," # "," # "," #"]) -d(')',["# "," # "," # "," # ","# "]) -d('[',["## ","# ","# ","# ","## "]) -d(']',[" ##"," #"," #"," #"," ##"]) -d('<',[" #"," # ","# "," # "," #"]) -d('>',["# "," # "," #"," # ","# "]) -d('#',["# #","###","# #","###","# #"]) -d('$',[" # "," ##","## "," ##","## "]) # rough -d('%',["# #"," #"," # ","# ","# #"]) -d('&',[" # ","# #"," # ","# #"," ##"]) -d("'",[" # "," # "," "," "," "]) -d('"',["# #","# #"," "," "," "]) -d('@',[" # ","# #","###","# "," ##"]) -d('_',[" "," "," "," ","###"]) -d('~',[" "," ##","## "," "," "]) -d('^',[" # ","# #"," "," "," "]) -d('|',[" # "," # "," # "," # "," # "]) -d('{',[" #"," # ","## "," # "," #"]) -d('}',["# "," # "," ##"," # ","# "]) -d('`',["# "," # "," "," "," "]) +def d(ch, top, rows): # rows: list of 3-char strings ('#'=on) + G[ch] = (top, rows) -def glyph_bytes(rows): - out=[] - # 5 glyph rows in the top; cell is 4px wide, glyph 3px left-aligned (bit3..1) - for r in range(8): - if r < 5: - s = rows[r] - v = 0 - for i,px in enumerate(s): # i=0 leftmost -> bit3 - if px=='#': v |= (1 << (3-i)) - out.append(v) - else: - out.append(0) +# ---- uppercase (top=1, 5 tall) ---- +CAPS = { + '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':["###","..#",".#.","#..","###"], +} +for c,r in CAPS.items(): d(c,1,r) + +# ---- digits (top=1, 5 tall) ---- +DIG = { + '0':[".#.","#.#","#.#","#.#",".#."], '1':[".#.","##.",".#.",".#.","###"], + '2':["##.","..#",".#.","#..","###"], '3':["##.","..#",".#.","..#","##."], + '4':["#.#","#.#","###","..#","..#"], '5':["###","#..","##.","..#","##."], + '6':[".##","#..","##.","#.#",".#."], '7':["###","..#",".#.",".#.",".#."], + '8':[".#.","#.#",".#.","#.#",".#."], '9':[".#.","#.#",".##","..#","##."], +} +for c,r in DIG.items(): d(c,1,r) + +# ---- lowercase: x-height rows 2..5, baseline row 5 ---- +# plain x-height (top=2, 4 tall) +XH = { + 'a':["##.",".##","#.#",".##"], 'c':[".##","#..","#..",".##"], + 'e':[".#.","#.#","##.",".##"], 'm':["#.#","###","###","#.#"], + 'n':["##.","#.#","#.#","#.#"], 'o':[".#.","#.#","#.#",".#."], + 'r':["#.#","##.","#..","#.."], 's':[".##","##.",".##","##."], + 'u':["#.#","#.#","#.#",".##"], 'v':["#.#","#.#","#.#",".#."], + 'w':["#.#","###","###","#.#"], 'x':["#.#",".#.",".#.","#.#"], + 'z':["###","..#",".#.","###"], +} +for c,r in XH.items(): d(c,2,r) +# ascenders (top=1, 5 tall) +ASC = { + 'b':["#..","#..","##.","#.#","##."], 'd':["..#","..#",".##","#.#",".##"], + 'f':[".##",".#.","###",".#.",".#."], 'h':["#..","#..","##.","#.#","#.#"], + 'k':["#..","#.#","##.","#.#","#.#"], 'l':["##.",".#.",".#.",".#.",".##"], + 't':[".#.","###",".#.",".#.",".##"], 'i':[".#.","...",".#.",".#.",".#."], +} +for c,r in ASC.items(): d(c,1,r) +# descenders (top=2, body 4 + descender -> rows 2..7) +DESC = { + 'g':[".##","#.#",".##","..#","##."], 'p':["##.","#.#","##.","#..","#.."], + 'q':[".##","#.#",".##","..#","..#"], 'y':["#.#","#.#",".##","..#","##."], + 'j':["..#","...","..#","..#","##."], +} +for c,r in DESC.items(): d(c,2,r) + +# ---- punctuation ---- +P = { + '.':(4,[".#."]), ',':(4,[".#.","#.."]), ':':(2,[".#.","...",".#."]), + ';':(2,[".#.","...",".#.","#.."]), '!':(1,[".#.",".#.",".#.","...",".#."]), + '?':(1,["##.","..#",".#.","...",".#."]), '-':(3,["###"]), + '+':(2,[".#.","###",".#."]), '=':(2,["###","...","###"]), + '*':(1,["#.#",".#.","###",".#.","#.#"]), '/':(1,["..#","..#",".#.","#..","#.."]), + '\\':(1,["#..","#..",".#.","..#","..#"]), '(':(1,["..#",".#.",".#.",".#.","..#"]), + ')':(1,["#..",".#.",".#.",".#.","#.."]), '[':(1,["##.","#..","#..","#..","##."]), + ']':(1,[".##","..#","..#","..#",".##"]), '<':(1,["..#",".#.","#..",".#.","..#"]), + '>':(1,["#..",".#.","..#",".#.","#.."]), '#':(1,["#.#","###","#.#","###","#.#"]), + '$':(1,[".##","#.#",".#.","#.#","##."]), '%':(1,["#.#","..#",".#.","#..","#.#"]), + '&':(1,[".#.","#.#",".#.","#.#",".##"]), "'":(1,[".#.",".#."]), + '"':(1,["#.#","#.#"]), '@':(1,[".#.","#.#","###","#..",".##"]), + '_':(7,["###"]), '~':(3,[".##","##."]), '^':(1,[".#.","#.#"]), + '|':(1,[".#.",".#.",".#.",".#.",".#."]), '`':(1,["#..",".#."]), + '{':(1,["..#",".#.","##.",".#.","..#"]), '}':(1,["#..",".#.",".##",".#.","#.."]), + ' ':(0,[]), +} +for c,(t,r) in P.items(): d(c,t,r) + +def bits(s): + v=0 + for i,px in enumerate(s[:3]): + if px=='#': v |= (1 << (3-i)) + return v + +def glyph_bytes(top, rows): + out=[0]*8 + for k,s in enumerate(rows): + r = top + k + if 0 <= r < 8: out[r] = bits(s) return out -lines=["; auto-generated by tools/genfont.py - 4x8 font (3x5 glyphs), 96 glyphs", +lines=["; auto-generated by tools/genfont.py - 4x8 mixed-case font, 96 glyphs", "SECTION \"font\", ROM0", "FontData::"] +missing=[] for code in range(32,128): ch = chr(code) - key = ch.upper() if ch.isalpha() else ch - rows = G.get(key, G.get(ch, ["###","# #","# #","# #","###"])) # box for unknown - b = glyph_bytes(rows) - lines.append(" db " + ",".join("$%02X"%x for x in b) + " ; '%s'"%(ch if ch!='\\' and code!=127 else '?')) + if ch in G: + top,rows = G[ch] + else: + top,rows = 1,["###","#.#","#.#","#.#","###"] # box for undefined + if code!=127: missing.append(ch) + b = glyph_bytes(top,rows) + label = ch if ch not in '\\' else '\\\\' + lines.append(" db " + ",".join("$%02X"%x for x in b) + " ; '%s'"%(label if 32