"""Interlace mode: two multicolor frames shown on alternating fields (50Hz each). The eye averages the two frames, so each pixel can show the blend of its colour in frame A and frame B -- up to ~136 distinct apparent colours (16 base + 120 pairs). Frame A is an ordinary multicolor conversion; frame B targets the *residual* (2*target - A) so that (A+B)/2 reconstructs the original. Both frames share the global background and the colour-RAM colour per cell (the only VIC state the viewer cannot cheaply swap per frame), and differ in bitmap + screen RAM. Memory layout of the appended data (loads from $2000), matched to viewer/interlace.s: $2000 bitmap A 8000 (bank 0, VIC reads here) $3F40 screen A 1000 (copied to $0400) $4400 screen B 1000 (bank 1 video matrix, in place) $6000 bitmap B 8000 (bank 1, VIC reads here) $8000 colour RAM 1000 (shared, copied to $D800) $83E8 background 1 """ from __future__ import annotations from itertools import combinations import numpy as np from .. import dither, palette as pal from . import base WIDTH, HEIGHT = 160, 200 CELL_W, CELL_H = 4, 8 PIXEL_ASPECT = 2.0 DATA_ADDR = 0x2000 N_COLS, N_ROWS = 40, 25 N_CELLS = N_COLS * N_ROWS def convert(img_rgb, palette_name="colodore", dither_mode="bayer", intensive=False): plab = pal.palette_lab(palette_name) prgb = pal.get_palette(palette_name) img_lab = pal.srgb_to_lab(img_rgb) # ---- frame A: ordinary multicolor (bg + 3 free per cell) ---- cellsA, _, _ = base.cells_lab(img_lab, CELL_W, CELL_H) distA = base.cell_distance(cellsA, plab) if intensive: bg, setsA, _ = base.optimize_background(distA, n_free=3) else: bg = base.best_global_color(img_lab, plab) avail = [i for i in range(16) if i != bg] setsA, _ = base.select_cell_sets(distA, avail, n_free=3, fixed=(bg,)) # colour-RAM colour (shared by both frames) = third free colour of A. c11 = setsA[:, 3].astype(np.int64) allowedA = base.per_pixel_allowed(setsA, N_ROWS, N_COLS, CELL_W, CELL_H, HEIGHT, WIDTH) idxA = dither.quantize(img_lab, allowedA, plab, dither_mode).astype(np.uint8) # ---- frame B: match residual 2*target - A in linear light ---- lin_target = pal.srgb_to_linear(img_rgb) lin_A = pal.srgb_to_linear(prgb[idxA]) resid = np.clip(2.0 * lin_target - lin_A, 0.0, 1.0) resid_srgb = pal.linear_to_srgb(resid) resid_lab = pal.srgb_to_lab(resid_srgb) setsB = _solve_frameB(resid_lab, plab, bg, c11) allowedB = base.per_pixel_allowed(setsB, N_ROWS, N_COLS, CELL_W, CELL_H, HEIGHT, WIDTH) idxB = dither.quantize(resid_lab, allowedB, plab, dither_mode).astype(np.uint8) # ---- blended preview (linear average -> sRGB, widened to display aspect) ---- blend_lin = (pal.srgb_to_linear(prgb[idxA]) + pal.srgb_to_linear(prgb[idxB])) / 2.0 blend = pal.linear_to_srgb(blend_lin) preview = np.repeat(blend, int(round(PIXEL_ASPECT)), axis=1) blend_lab = pal.srgb_to_lab(blend) error = float(np.sqrt(np.sum((blend_lab - img_lab) ** 2, axis=-1)).mean()) data = _encode(idxA, idxB, setsA, setsB, bg, c11) return base.Conversion( mode="interlace", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT, index_image=idxA, data=data, data_addr=DATA_ADDR, viewer="interlace", preview_rgb=preview, error=error, meta={"palette": palette_name, "dither": dither_mode, "background": int(bg)}, ) def _solve_frameB(resid_lab, plab, bg, c11): """Per cell, pick the 2 free colours for frame B given shared {bg, c11[cell]}.""" cells, _, _ = base.cells_lab(resid_lab, CELL_W, CELL_H) dist = base.cell_distance(cells, plab) # (n, P, 16) dbg = dist[:, :, bg] # (n, P) dc11 = np.take_along_axis(dist, c11[:, None, None], axis=2)[:, :, 0] sbase = np.minimum(dbg, dc11) n = dist.shape[0] best = np.full(n, np.inf) b1 = np.zeros(n, dtype=np.int64) b2 = np.zeros(n, dtype=np.int64) for x, y in combinations(range(16), 2): e = np.minimum(np.minimum(sbase, dist[:, :, x]), dist[:, :, y]).sum(axis=1) better = e < best best = np.where(better, e, best) b1 = np.where(better, x, b1) b2 = np.where(better, y, b2) bg_arr = np.full(n, bg, dtype=np.int64) return np.stack([bg_arr, b1, b2, c11], axis=1) def _pack_frame(index_image, screen_assign, colram_assign, bg, get_lut): """Build (bitmap, screen) for one frame. ``get_lut`` maps cell index -> dict.""" bitmap = np.zeros(8000, dtype=np.uint8) screen = np.zeros(1000, dtype=np.uint8) for cr in range(N_ROWS): for cc in range(N_COLS): ci = cr * N_COLS + cc hi, lo, lut = get_lut(ci) screen[ci] = ((hi & 0x0F) << 4) | (lo & 0x0F) base_addr = cr * 320 + cc * 8 block = index_image[cr * 8:cr * 8 + 8, cc * 4:cc * 4 + 4] for r in range(8): byte = 0 for x in range(4): byte = (byte << 2) | lut.get(int(block[r, x]), 0b00) bitmap[base_addr + r] = byte return bitmap, screen def _encode(idxA, idxB, setsA, setsB, bg, c11): def lutA(ci): cc11 = int(c11[ci]) a01, a10 = int(setsA[ci, 1]), int(setsA[ci, 2]) return a01, a10, {int(bg): 0b00, a01: 0b01, a10: 0b10, cc11: 0b11} def lutB(ci): cc11 = int(c11[ci]) b01, b10 = int(setsB[ci, 1]), int(setsB[ci, 2]) return b01, b10, {int(bg): 0b00, b01: 0b01, b10: 0b10, cc11: 0b11} bitmapA, screenA = _pack_frame(idxA, None, None, bg, lutA) bitmapB, screenB = _pack_frame(idxB, None, None, bg, lutB) colram = (c11 & 0x0F).astype(np.uint8) block = bytearray() block += bytes(bitmapA) # $2000 block += bytes(screenA) # $3F40 block += bytes(0x4400 - (0x3F40 + 1000)) # pad to $4400 block += bytes(screenB) # $4400 block += bytes(0x6000 - (0x4400 + 1000)) # pad to $6000 block += bytes(bitmapB) # $6000 block += bytes(0x8000 - (0x6000 + 8000)) # pad to $8000 block += bytes(colram) # $8000 block += bytes([int(bg) & 0x0F]) # $83E8 return bytes(block)