aboutsummaryrefslogtreecommitdiffstats
path: root/tools/gencolor.py
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-18 20:41:27 +0200
committeruser <user@clank>2026-07-18 20:41:27 +0200
commit9e7d5f6e5e01b7473b84bc3449d7f8e0f78e7045 (patch)
tree5dcde624f3b366cc9244e08b0d2ca4ab35b338e2 /tools/gencolor.py
parentkernel: a real timer source (64 Hz tick IRQ) + uptime(1) (diff)
downloadgbos-9e7d5f6e5e01b7473b84bc3449d7f8e0f78e7045.tar.gz
gbos-9e7d5f6e5e01b7473b84bc3449d7f8e0f78e7045.tar.xz
gbos-9e7d5f6e5e01b7473b84bc3449d7f8e0f78e7045.zip
term: 7-color terminal (per-glyph fg+bg) + ANSI escapes; colorize irc
Fano-plane palette scheme: the 7 colors map onto 7 CGB BG palettes so any color pair shares one palette; a tile's palette is picked from the set of colors its two cells need (tables generated by tools/gencolor.py). Per cell a packed (bg<<4)|fg byte lives alongside the char shadow, and the glyph blitter steers glyph/empty pixels to each cell's fg/bg color slots via plane masks. An ANSI-ish CSI parser (ESC [ .. m) drives it; usr/ansi.c demos it and the irc client now renders hashed nick colors, status dimming and a channel-activity bar.
Diffstat (limited to '')
-rw-r--r--tools/gencolor.py113
1 files changed, 113 insertions, 0 deletions
diff --git a/tools/gencolor.py b/tools/gencolor.py
new file mode 100644
index 0000000..711fa78
--- /dev/null
+++ b/tools/gencolor.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+# Generate src/color.asm: CGB palette data + the lookup tables the terminal
+# uses to render per-glyph FOREGROUND *and* BACKGROUND color.
+#
+# The 7 colors are the points of a FANO PLANE (Steiner triple system S(2,3,7)):
+# 7 palettes = 7 lines, and every PAIR of colors lies on exactly one line, so a
+# tile that needs at most two distinct (non-black) colors always has a palette
+# holding both. slot0 of every palette is black, so a black background costs no
+# color id; a colored background is drawn by giving the cell's empty pixels the
+# background color's slot instead of slot0.
+#
+# Because a tile packs two independent cells (each with fg+bg) it can need up to
+# 3 non-black colors. Every color pair is covered exactly; a 3-color tile is
+# covered only if that triple happens to be a line - otherwise BestPal picks the
+# line covering the most of them and the odd one out is approximated.
+#
+# Color index: 0=black(bg/none) 1=red 2=green 3=yellow 4=blue 5=magenta 6=cyan
+# 7=white. BG palette 1 is reserved for the on-screen keyboard.
+
+def gbc15(r, g, b):
+ return r | (g << 5) | (b << 10)
+
+COLOR_RGB = {
+ 1: (31, 0, 0), # red
+ 2: ( 0, 31, 0), # green
+ 3: (31, 31, 0), # yellow
+ 4: ( 0, 0, 31), # blue
+ 5: (31, 0, 31), # magenta
+ 6: ( 0, 31, 31), # cyan
+ 7: (31, 31, 31), # white
+}
+
+# Hardware BG palette -> its 3 colors in slot1,slot2,slot3 order (slot0=black).
+# The 7 triples form a Fano plane over colors 1..7. Palette 0 keeps white at
+# slot1 (the OSK and plain white text draw with color-id 1).
+PALETTES = {
+ 0: [7, 1, 6],
+ 2: [1, 2, 3],
+ 3: [1, 4, 5],
+ 4: [2, 4, 6],
+ 5: [2, 5, 7],
+ 6: [3, 4, 7],
+ 7: [3, 5, 6],
+}
+PAL_ORDER = [0, 2, 3, 4, 5, 6, 7]
+
+# --- verify Fano: every pair of 1..7 lies on exactly one line ------------------
+seen = {}
+for pal, line in PALETTES.items():
+ assert len(set(line)) == 3
+ for i in range(3):
+ for j in range(i + 1, 3):
+ k = frozenset((line[i], line[j]))
+ assert k not in seen, ("pair on two lines", k)
+ seen[k] = pal
+assert set(seen) == {frozenset((a, b)) for a in range(1, 8)
+ for b in range(1, 8) if a != b}
+
+def slot_of(pal, color):
+ """id (0..3) of color in palette pal. black->0; absent->1 (visible fallback)."""
+ if color == 0:
+ return 0
+ line = PALETTES.get(pal, [])
+ return (line.index(color) + 1) if color in line else 1
+
+def best_pal(mask):
+ """Palette covering the most colors present in the 7-bit set 'mask'."""
+ if mask == 0:
+ return 0
+ best, bestn = 0, -1
+ for pal in PAL_ORDER:
+ n = sum(1 for c in PALETTES[pal] if mask & (1 << (c - 1)))
+ if n > bestn:
+ best, bestn = pal, n
+ return best
+
+# --- emit ----------------------------------------------------------------------
+def pal_colors(hw):
+ if hw == 1: # OSK: white bg, black ink (inverse)
+ return [(31,31,31), (0,0,0), (0,0,0), (0,0,0)]
+ return [(0,0,0)] + [COLOR_RGB[c] for c in PALETTES[hw]]
+
+L = []
+L.append("; auto-generated by tools/gencolor.py - Fano-plane color tables")
+L.append("; (foreground + background). DO NOT EDIT.")
+L.append('SECTION "color", ROM0')
+L.append("")
+L.append("; 8 CGB BG palettes, 4 colors each (c0=black). Loaded at BCPS $80.")
+L.append("PalData::")
+for hw in range(8):
+ words = []
+ for (r, g, b) in pal_colors(hw):
+ v = gbc15(r, g, b)
+ words.append("$%02X,$%02X" % (v & 0xFF, (v >> 8) & 0xFF))
+ L.append(" db " + ",".join(words) + " ; palette %d" % hw)
+L.append("")
+L.append("; BitTab[color] = bit for that color in the 'colors present' set (black=0)")
+L.append("BitTab::")
+L.append(" db " + ",".join("$%02X" % (0 if c == 0 else (1 << (c - 1))) for c in range(8)))
+L.append("")
+L.append("; BestPal[set] = palette (0..7) covering the most colors in a 7-bit set")
+L.append("BestPal::")
+for base in range(0, 128, 16):
+ L.append(" db " + ",".join("$%02X" % best_pal(base + j) for j in range(16))
+ + " ; %d..%d" % (base, base + 15))
+L.append("")
+L.append("; SlotOf[palette*8 + color] = that color's slot/id (0..3) in the palette")
+L.append("SlotOf::")
+for pal in range(8):
+ L.append(" db " + ",".join("$%02X" % slot_of(pal, c) for c in range(8))
+ + " ; palette %d" % pal)
+open("/home/user/dev/gbos/src/color.asm", "w").write("\n".join(L) + "\n")
+print("wrote src/color.asm")