First public commit.

This commit is contained in:
The Dust Council 2026-07-03 19:35:35 -07:00
parent 2a48f52979
commit 4bac9d83ed
288 changed files with 18417 additions and 1076 deletions

View file

@ -0,0 +1,16 @@
"""TI-99/4A conversion dispatch."""
from __future__ import annotations
from ... import imageprep
from . import gm2, mono
_MODULES = {"gm2": gm2, "mono": mono}
MODES = list(_MODULES.keys())
def convert_image(path_or_img, mode="gm2", palette_name="tms9918",
dither_mode="floyd", intensive=False, prep_opt=None, base_color=None):
prep_opt = prep_opt or imageprep.PrepOptions()
module = _MODULES.get(mode, gm2)
img_rgb = imageprep.prepare(path_or_img, module.WIDTH, module.HEIGHT,
module.PIXEL_ASPECT, prep_opt, border_rgb=(0, 0, 0))
return module.convert(img_rgb, palette_name, dither_mode, intensive, base_color=base_color)

View file

@ -0,0 +1,72 @@
"""TI-99/4A Graphics Mode 2: 256x192, 2 colours per 8x8 cell (15-colour palette).
Like C64 hires but on the TMS9918A. Produces the bitmap pattern table (6144 B)
and one colour byte per cell (768 B); the cartridge viewer expands each cell's
colour across its 8 rows of the VDP colour table.
"""
from __future__ import annotations
import numpy as np
from ... import dither, palette as c64pal
from ...convert import base
from .. import palette as tpal
WIDTH, HEIGHT = 256, 192
CELL_W, CELL_H = 8, 8
PIXEL_ASPECT = 1.0
N_COLS, N_ROWS = 32, 24
def convert(img_rgb, palette_name="tms9918", dither_mode="floyd",
intensive=False, base_color=None):
plab = tpal.palette_lab()
prgb = tpal.get_palette().astype(np.uint8)
img_lab = c64pal.srgb_to_lab(img_rgb)
cells, rows, cols = base.cells_lab(img_lab, CELL_W, CELL_H)
# Dither-aware colour selection for error-diffusion modes -- each cell's two
# colours are chosen so the segment between them brackets the cell, letting
# dithering blend to the true shade (far smoother than nearest-colour, which
# bands). Ordered/none keep plain nearest-colour selection.
if dither_mode in base.DIFFUSION_DITHERS:
sets, _ = base.select_cell_sets_dither(cells, plab, tpal.USABLE, n_free=2)
else:
dist = base.cell_distance(cells, plab)
sets, _ = base.select_cell_sets(dist, tpal.USABLE, n_free=2)
allowed = base.per_pixel_allowed(sets, rows, cols, CELL_W, CELL_H, HEIGHT, WIDTH)
idx = dither.quantize(img_lab, allowed, plab, dither_mode).astype(np.uint8)
pattern, colors = _encode(idx, sets, rows, cols)
data = bytes(pattern) + bytes(colors) # 6144 + 768
return base.Conversion(
mode="gm2", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
index_image=idx.astype(np.uint16), data=data, data_addr=0,
viewer="gm2", preview_rgb=prgb[idx],
error=base.perceptual_error(idx, img_lab, plab),
meta={"palette": "tms9918", "dither": dither_mode},
)
def _encode(idx, sets, rows, cols):
pattern = np.zeros(6144, dtype=np.uint8) # 768 cells x 8 rows
colors = np.zeros(768, dtype=np.uint8) # 1 colour byte per cell
for cr in range(rows):
for cc in range(cols):
ci = cr * cols + cc
c0, c1 = int(sets[ci, 0]), int(sets[ci, 1])
# brighter colour = foreground (bit 1); store fg in high nibble.
bg, fg = (c0, c1)
colors[ci] = ((fg & 0x0F) << 4) | (bg & 0x0F)
block = idx[cr * 8:cr * 8 + 8, cc * 8:cc * 8 + 8]
base = ci * 8
for r in range(8):
row = block[r]
byte = 0
for x in range(8):
byte = (byte << 1) | (1 if row[x] == fg else 0)
pattern[base + r] = byte
return pattern, colors

View file

@ -0,0 +1,44 @@
"""TI-99/4A (TMS9918A) monochrome / tinted-mono mode.
Same 256x192, 2-colours-per-cell format as gm2, but matched by *luminance* to a
grey ramp (black -> grey -> white) so every cell is neutral -- no colour clash,
maximum perceived detail, a clean greyscale photo. Pick a base colour for a
tinted monochrome instead. Reuses the gm2 byte packing and viewer.
"""
from __future__ import annotations
import numpy as np
from ... import palette as c64pal
from ...convert import base
from .. import palette as tpal
from . import gm2
WIDTH, HEIGHT = gm2.WIDTH, gm2.HEIGHT
CELL_W, CELL_H = gm2.CELL_W, gm2.CELL_H
PIXEL_ASPECT = gm2.PIXEL_ASPECT
# Neutral ramp: black(1), grey(14), white(15). Lighter siblings for tinting.
NEUTRAL = [1, 14, 15]
SIBLINGS = {2: 3, 3: 2, 4: 5, 5: 4, 6: 9, 8: 9, 9: 8, 12: 14, 13: 9}
def convert(img_rgb, palette_name="tms9918", dither_mode="atkinson",
intensive=False, base_color=None):
plab = tpal.palette_lab()
prgb = tpal.get_palette().astype(np.uint8)
ramp = base.luminance_ramp(plab, NEUTRAL, base_color, SIBLINGS)
idx, sets, rows, cols, err = base.mono_render(
img_rgb, plab, ramp, WIDTH, HEIGHT, CELL_W, CELL_H, dither_mode, n_free=2)
pattern, colors = gm2._encode(idx, sets, rows, cols)
data = bytes(pattern) + bytes(colors)
return base.Conversion(
mode="mono", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
index_image=idx.astype(np.uint16), data=data, data_addr=0,
viewer="gm2", preview_rgb=prgb[idx], error=err,
meta={"palette": "tms9918", "dither": dither_mode, "base_color": base_color},
)