First public commit.
This commit is contained in:
parent
2a48f52979
commit
4bac9d83ed
288 changed files with 18417 additions and 1076 deletions
96
lenser/atari/palette.py
Normal file
96
lenser/atari/palette.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
"""Atari 8-bit (GTIA) colour palette.
|
||||
|
||||
The GTIA produces 256 colour-register values: the high nibble is the hue (0 =
|
||||
grey, 1..15 = colours around the NTSC wheel) and the low nibble is the luminance.
|
||||
We generate an NTSC palette with a standard YIQ formula, but if the atari800
|
||||
emulator's palette file is present we load that instead so the preview matches
|
||||
exactly what the emulator displays.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..palette import srgb_to_lab # reuse the CIELAB conversion
|
||||
|
||||
# Candidate locations for atari800's bundled NTSC palette (768 raw RGB bytes).
|
||||
_PAL_FILES = [
|
||||
"/usr/share/atari800/Palettes/Real.act",
|
||||
"/usr/share/atari800/default.pal",
|
||||
"/usr/local/share/atari800/default.pal",
|
||||
]
|
||||
|
||||
|
||||
def _generate_ntsc() -> np.ndarray:
|
||||
"""Generate a 256x3 (uint8) NTSC palette via a YIQ approximation."""
|
||||
pal = np.zeros((256, 3), dtype=np.float64)
|
||||
# Calibration roughly matching the common Atari NTSC look.
|
||||
sat = 0.30
|
||||
hue0 = -58.0 # phase of hue 1, degrees
|
||||
for reg in range(256):
|
||||
hue = (reg >> 4) & 0x0F
|
||||
lum = reg & 0x0F
|
||||
y = lum / 15.0
|
||||
if hue == 0:
|
||||
i = q = 0.0
|
||||
else:
|
||||
angle = math.radians(hue0 + (hue - 1) * (360.0 / 15.0))
|
||||
i = sat * math.cos(angle)
|
||||
q = sat * math.sin(angle)
|
||||
r = y + 0.956 * i + 0.621 * q
|
||||
g = y - 0.272 * i - 0.647 * q
|
||||
b = y - 1.106 * i + 1.703 * q
|
||||
pal[reg] = [r, g, b]
|
||||
return np.clip(pal * 255.0 + 0.5, 0, 255).astype(np.uint8).astype(np.float64)
|
||||
|
||||
|
||||
def _load_pal_file() -> np.ndarray | None:
|
||||
for path in _PAL_FILES:
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
if len(data) >= 768:
|
||||
return np.frombuffer(data[:768], dtype=np.uint8).reshape(256, 3).astype(np.float64)
|
||||
except OSError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
_CACHE: dict[str, np.ndarray] = {}
|
||||
|
||||
|
||||
def get_palette(name: str = "ntsc") -> np.ndarray:
|
||||
"""Return the 256x3 sRGB palette (float64, 0..255)."""
|
||||
if "rgb" not in _CACHE:
|
||||
_CACHE["rgb"] = _load_pal_file()
|
||||
if _CACHE["rgb"] is None:
|
||||
_CACHE["rgb"] = _generate_ntsc()
|
||||
return _CACHE["rgb"]
|
||||
|
||||
|
||||
def palette_lab(name: str = "ntsc") -> np.ndarray:
|
||||
"""Return the 256 palette colours in CIELAB (256x3)."""
|
||||
if "lab" not in _CACHE:
|
||||
_CACHE["lab"] = srgb_to_lab(get_palette(name))
|
||||
return _CACHE["lab"]
|
||||
|
||||
|
||||
def hue_ramp(hue: int) -> list[int]:
|
||||
"""The 16 register values of one hue (luminance 0..15) -- for GR.9 / mono."""
|
||||
return [((hue & 0x0F) << 4) | lum for lum in range(16)]
|
||||
|
||||
|
||||
def nearest_hue(rgb) -> int:
|
||||
"""Atari hue (0..15) whose mid-luminance colour best matches ``rgb``."""
|
||||
from ..palette import srgb_to_lab
|
||||
lab = srgb_to_lab(np.asarray(rgb, dtype=np.float64))
|
||||
pl = palette_lab()
|
||||
best, best_d = 0, float("inf")
|
||||
for h in range(16):
|
||||
d = float(np.sum((pl[(h << 4) | 8] - lab) ** 2))
|
||||
if d < best_d:
|
||||
best, best_d = h, d
|
||||
return best
|
||||
Loading…
Add table
Add a link
Reference in a new issue