"""Nintendo Entertainment System (2C02 PPU) master palette. The NES has no fixed RGB palette -- the PPU generates an NTSC signal. We reproduce MAME's exact ``nespal_to_RGB`` (YUV->RGB) formula so the encoder matches what the emulator renders. A palette value is a 6-bit index 0-63: high nibble = luminance (0-3), low nibble = hue (0-15, with 0/13/14/15 greyscale). The byte written to PPU palette RAM IS this index, so ``PALETTE``/``GREYS`` are indexed by the hardware value. """ from __future__ import annotations import math import numpy as np from ..palette import srgb_to_lab _TINT, _HUE = 0.22, 287.0 _Kr, _Kb, _Ku, _Kv = 0.2989, 0.1145, 2.029, 1.140 _BRIGHT = [[0.50, 0.75, 1.0, 1.0], [0.29, 0.45, 0.73, 0.9], [0.0, 0.24, 0.47, 0.77]] def _rgb(intensity: int, num: int): if num == 0: sat = rad = 0.0; y = _BRIGHT[0][intensity] elif num == 13: sat = rad = 0.0; y = _BRIGHT[2][intensity] elif num in (14, 15): sat = rad = y = 0.0 else: sat = _TINT; rad = math.radians(num * 30 + _HUE); y = _BRIGHT[1][intensity] u, v = sat * math.cos(rad), sat * math.sin(rad) R = (y + _Kv * v) * 255.0 G = (y - (_Kb * _Ku * u + _Kr * _Kv * v) / (1 - _Kb - _Kr)) * 255.0 B = (y + _Ku * u) * 255.0 cl = lambda x: max(0, min(255, int(math.floor(x + 0.5)))) return (cl(R), cl(G), cl(B)) PALETTE = np.array([_rgb(i >> 4, i & 0x0F) for i in range(64)], dtype=np.float64) # Distinct grey ramp (R==G==B), sorted dark->light, deduped by luminance. _grey = {} for _i in range(64): r, g, b = PALETTE[_i] if r == g == b: _grey.setdefault(int(r), _i) GREYS = [_grey[k] for k in sorted(_grey)] # e.g. $0F,$1D,$2D,$10,$20 def get_palette() -> np.ndarray: return PALETTE def palette_lab() -> np.ndarray: return srgb_to_lab(PALETTE)