8bitlenser/lenser/coco/palette.py
2026-07-03 19:35:35 -07:00

69 lines
2 KiB
Python

"""TRS-80 Color Computer MC6847 VDG palette + pixel packing.
PMODE 4 (256x192) is 2-colour: black + the foreground of the selected colour set
(CSS=0 green, CSS=1 "buff" ~ off-white). We use CSS=1 (buff on black) for clean
monochrome, like Apple HGR mono.
"""
from __future__ import annotations
import numpy as np
from ..palette import srgb_to_lab
# Approximate sRGB for the MC6847 colours.
BLACK = (0, 0, 0)
BUFF = (255, 255, 255)
GREEN = (38, 194, 64)
YELLOW = (255, 240, 112)
BLUE = (40, 62, 211)
RED = (180, 38, 40)
CYAN = (52, 198, 160)
MAGENTA = (200, 70, 180)
ORANGE = (224, 116, 36)
# PMODE 4 monochrome (CSS=1): index 0 = black, 1 = buff.
MONO = np.array([BLACK, BUFF], dtype=np.float64)
# PMODE 3 (CG6) 4-colour sets, selected by the CSS bit. The 2-bit pixel value
# 0..3 indexes the set. VDG byte ($FF22) high nibble = E (A/G,GM2,GM1=1, GM0=0).
PMODE3_SETS = {
0xE0: np.array([GREEN, YELLOW, BLUE, RED], dtype=np.float64), # CSS=0
0xE8: np.array([BUFF, CYAN, MAGENTA, ORANGE], dtype=np.float64), # CSS=1
}
def mono_lab() -> np.ndarray:
return srgb_to_lab(MONO)
def pack_pmode3(val: np.ndarray) -> bytes:
"""Pack a (192,128) 0..3 array into 6144 bytes, 4 pixels/byte (2bpp),
leftmost pixel in the high bits."""
h, w = val.shape
out = bytearray(w // 4 * h)
k = 0
for y in range(h):
row = val[y]
for x in range(0, w, 4):
out[k] = ((row[x] & 3) << 6) | ((row[x + 1] & 3) << 4) | \
((row[x + 2] & 3) << 2) | (row[x + 3] & 3)
k += 1
return bytes(out)
def pack_pmode4(idx: np.ndarray) -> bytes:
"""Pack a (192,256) 0/1 array into 6144 bytes, 8 pixels/byte, bit7 leftmost,
1 = foreground (the VDG reads this straight from video RAM)."""
h, w = idx.shape
out = bytearray(w // 8 * h)
k = 0
for y in range(h):
row = idx[y]
for x in range(0, w, 8):
b = 0
for i in range(8):
b = (b << 1) | (1 if row[x + i] else 0)
out[k] = b
k += 1
return bytes(out)