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

81 lines
3.2 KiB
Python

"""Atari 2600 TIA (NTSC) palette + playfield bit packing.
The TIA colour byte is (hue << 4) | (lum << 1): 16 hues x 8 luminances = 128
colours (bit 0 unused, so register values are even 0..254). We generate the
NTSC palette with a YIQ formula -- close enough to be recognisable; the encoder
matches against it in CIELAB.
"""
from __future__ import annotations
import numpy as np
from ..palette import srgb_to_lab
TIA_NTSC = [
(0,0,0), (44,44,44), (83,83,83), (119,119,119),
(154,154,154), (188,188,188), (222,222,222), (255,255,255),
(33,11,0), (73,53,0), (109,90,0), (145,126,0),
(179,161,44), (213,195,83), (246,229,119), (255,255,154),
(60,0,0), (97,35,0), (133,74,0), (168,110,26),
(202,146,66), (235,180,103), (255,214,139), (255,247,173),
(74,0,0), (111,18,0), (146,58,29), (181,96,68),
(215,132,105), (248,167,141), (255,201,175), (255,234,209),
(75,0,0), (112,5,43), (147,48,81), (182,86,118),
(215,122,153), (249,157,187), (255,191,221), (255,225,254),
(62,0,56), (99,1,93), (135,45,129), (170,83,164),
(204,119,198), (237,154,232), (255,189,255), (255,222,255),
(36,0,97), (75,7,133), (112,49,168), (147,87,202),
(182,123,235), (215,159,255), (248,193,255), (255,226,255),
(0,0,118), (43,21,153), (82,61,187), (118,99,221),
(153,134,254), (188,169,255), (221,203,255), (254,236,255),
(0,0,117), (10,38,152), (51,77,187), (89,113,220),
(125,149,253), (160,183,255), (195,217,255), (228,250,255),
(0,15,94), (0,56,130), (25,93,165), (65,129,199),
(102,164,233), (138,198,255), (173,232,255), (206,255,255),
(0,32,53), (0,71,91), (10,108,127), (52,143,162),
(90,178,196), (126,212,229), (161,245,255), (195,255,255),
(0,41,0), (0,80,38), (12,116,77), (53,152,114),
(91,186,149), (127,220,183), (162,253,217), (196,255,250),
(0,44,0), (0,82,0), (29,118,24), (69,154,64),
(106,188,102), (141,221,137), (176,255,172), (210,255,206),
(0,38,0), (14,77,0), (56,114,0), (93,149,24),
(129,183,64), (164,217,101), (198,250,137), (232,255,171),
(7,24,0), (50,64,0), (88,102,0), (124,137,0),
(159,172,43), (193,206,82), (226,239,118), (255,255,153),
(42,5,0), (80,48,0), (117,86,0), (152,122,6),
(186,157,48), (220,191,86), (253,225,122), (255,255,158),
]
PALETTE = np.array(TIA_NTSC, dtype=np.float64) # (128,3) sRGB, calibrated from MAME
def color_byte(index: int) -> int:
"""TIA register value (even 0..254) for palette index hue*8+lum."""
hue, lum = divmod(index, 8)
return (hue << 4) | (lum << 1)
def palette_lab() -> np.ndarray:
return srgb_to_lab(PALETTE)
# ---- playfield packing -----------------------------------------------------
# The 20 playfield pixels (left to right) map to the PF registers in this order:
# px 0-3 PF0 bits 4,5,6,7
# px 4-11 PF1 bits 7,6,5,4,3,2,1,0
# px 12-19 PF2 bits 0,1,2,3,4,5,6,7
def pack20(bits) -> tuple[int, int, int]:
"""20 pixel on/off values (leftmost first) -> (PF0, PF1, PF2) bytes."""
pf0 = pf1 = pf2 = 0
for i in range(4):
if bits[i]:
pf0 |= 1 << (4 + i)
for i in range(8):
if bits[4 + i]:
pf1 |= 1 << (7 - i)
for i in range(8):
if bits[12 + i]:
pf2 |= 1 << i
return pf0, pf1, pf2