"""Offline packaging of Jackbox / Jukebox art for the MASM32 assembly renderer. Processes high-resolution source images into a solid rectangular cabinet ('прямоугольный шкаф'): - Front: Arched jukebox art framed in rich dark mahogany wood casing (solid 256x512 BGRA). - Side: Cropped rectangular wooden cabinet side panel with chrome accents (solid 256x512 BGRA). - Back: Matching dark walnut cabinet rear panel with ventilation slots (solid 256x512 BGRA). - UI: 640x480 BGRA track selector screen. Eliminates all grey studio backgrounds, transparent gaps, and seam holes. """ import math from pathlib import Path import struct import cv2 import numpy as np ROOT = Path(__file__).resolve().parents[2] SOURCE = ROOT / "assets/sprites/jackbox" OUT = ROOT / "assets/animations/hall" # Deep rich dark walnut / mahogany wood color (BGR) WOOD_COLOR = np.array([16, 26, 48], dtype=np.uint8) def make_solid_front(img_bgr: np.ndarray) -> np.ndarray: """Frame the arched jukebox front into a solid rectangular wooden cabinet face.""" h, w = img_bgr.shape[:2] # Detect studio grey background diff = np.maximum( np.abs(img_bgr[:, :, 0].astype(int) - img_bgr[:, :, 1].astype(int)), np.maximum(np.abs(img_bgr[:, :, 1].astype(int) - img_bgr[:, :, 2].astype(int)), np.abs(img_bgr[:, :, 0].astype(int) - img_bgr[:, :, 2].astype(int))) ) bright = np.mean(img_bgr, axis=2) is_bg = (diff < 10) & (bright >= 18) & (bright <= 65) # Seed from borders seed_mask = np.zeros((h, w), dtype=np.uint8) seed_mask[0, :] = is_bg[0, :] seed_mask[-1, :] = is_bg[-1, :] seed_mask[:, 0] = is_bg[:, 0] seed_mask[:, -1] = is_bg[:, -1] num_labels, labels = cv2.connectedComponents(is_bg.astype(np.uint8)) border_labels = np.unique(labels[seed_mask == 1]) border_labels = border_labels[border_labels != 0] is_real_bg = np.isin(labels, border_labels) # Replace studio grey background with dark mahogany wood casing front_solid = img_bgr.copy() mask = cv2.GaussianBlur(is_real_bg.astype(np.float32), (5, 5), 1.0) for c in range(3): front_solid[:, :, c] = np.clip( front_solid[:, :, c] * (1.0 - mask) + WOOD_COLOR[c] * mask, 0, 255 ).astype(np.uint8) # Resize to 256x512 BGRA, 100% opaque (no transparency gaps!) resized = cv2.resize(front_solid, (256, 512), interpolation=cv2.INTER_LANCZOS4) bgra = cv2.cvtColor(resized, cv2.COLOR_BGR2BGRA) bgra[:, :, 3] = 255 return bgra def make_solid_side(img_bgr: np.ndarray) -> np.ndarray: """Crop side image to a solid rectangular cabinet side panel.""" # Crop to actual rectangular cabinet profile: # y: 90..2910 (removes studio top margin and floor shadow) # x: 120..1345 (from front edge to back vertical edge) side_crop = img_bgr[90:2910, 120:1345] h, w = side_crop.shape[:2] # Detect studio grey in the top corner outside the curve diff = np.maximum( np.abs(side_crop[:, :, 0].astype(int) - side_crop[:, :, 1].astype(int)), np.maximum(np.abs(side_crop[:, :, 1].astype(int) - side_crop[:, :, 2].astype(int)), np.abs(side_crop[:, :, 0].astype(int) - side_crop[:, :, 2].astype(int))) ) bright = np.mean(side_crop, axis=2) is_bg = (diff < 10) & (bright >= 18) & (bright <= 65) seed_mask = np.zeros((h, w), dtype=np.uint8) seed_mask[0, :] = is_bg[0, :] seed_mask[-1, :] = is_bg[-1, :] seed_mask[:, 0] = is_bg[:, 0] seed_mask[:, -1] = is_bg[:, -1] num_labels, labels = cv2.connectedComponents(is_bg.astype(np.uint8)) border_labels = np.unique(labels[seed_mask == 1]) border_labels = border_labels[border_labels != 0] is_real_bg = np.isin(labels, border_labels) # Fill corner with dark mahogany cabinet wood side_solid = side_crop.copy() mask = cv2.GaussianBlur(is_real_bg.astype(np.float32), (5, 5), 1.0) for c in range(3): side_solid[:, :, c] = np.clip( side_solid[:, :, c] * (1.0 - mask) + WOOD_COLOR[c] * mask, 0, 255 ).astype(np.uint8) # Left of texture (x=0) is FRONT edge; right of texture (x=max) is BACK edge. # Matches along_x from x0 (front) to x1 (back against wall). resized = cv2.resize(side_solid, (256, 512), interpolation=cv2.INTER_LANCZOS4) bgra = cv2.cvtColor(resized, cv2.COLOR_BGR2BGRA) bgra[:, :, 3] = 255 return bgra def make_solid_back() -> np.ndarray: """Generate a solid dark walnut cabinet rear panel with ventilation slots.""" rear = np.zeros((512, 256, 4), dtype=np.uint8) for y in range(512): grain = int(math.sin(y * 0.2) * 4 + math.sin(y * 0.05) * 6) rear[y, :, 0] = np.clip(int(WOOD_COLOR[0]) + grain, 0, 255) # B rear[y, :, 1] = np.clip(int(WOOD_COLOR[1]) + grain + 1, 0, 255) # G rear[y, :, 2] = np.clip(int(WOOD_COLOR[2]) + grain + 3, 0, 255) # R rear[y, :, 3] = 255 # Fully opaque for y in range(120, 240, 12): rear[y:y+3, 32:224, :3] = 10 # Dark ventilation slots return rear def pack_bgra(path: Path, frames: list[np.ndarray], w: int, h: int): """Pack an array of (H, W, 4) BGRA numpy uint8 images into an ANM1 binary pack.""" raw = b"".join(f.tobytes() for f in frames) path.parent.mkdir(parents=True, exist_ok=True) header = struct.pack("<4s4I", b"ANM1", w, h, len(frames), len(raw)) path.write_bytes(header + raw) print(f"Packed {len(frames)} frame(s) ({w}x{h}) into {path.name}: {len(raw)} bytes") def main(): front_path = SOURCE / "Jackbox_front.jpg" side_path = SOURCE / "Jackbox_side.jpg" ui_path = SOURCE / "Jackbox_UI.jpg" assert front_path.exists(), f"Missing {front_path}" assert side_path.exists(), f"Missing {side_path}" assert ui_path.exists(), f"Missing {ui_path}" print("Building solid rectangular Jukebox cabinet textures...") # 1. Front Texture: solid rectangular face front_bgr = cv2.imread(str(front_path)) front_tex = make_solid_front(front_bgr) # 2. Side Texture: solid rectangular side panel side_bgr = cv2.imread(str(side_path)) side_tex = make_solid_side(side_bgr) # 3. Rear Texture: solid dark walnut panel rear_tex = make_solid_back() # Pack 3D world frames: Frame 0 = Front, Frame 1 = Side, Frame 2 = Rear pack_bgra(OUT / "jackbox-v1.bin", [front_tex, side_tex, rear_tex], 256, 512) # 4. Screen UI Pack: 640x480 BGRA ui_bgr = cv2.imread(str(ui_path)) ui_bgra = cv2.cvtColor(ui_bgr, cv2.COLOR_BGR2BGRA) ui_tex = cv2.resize(ui_bgra, (640, 480), interpolation=cv2.INTER_LANCZOS4) pack_bgra(OUT / "jackbox-ui-v1.bin", [ui_tex], 640, 480) print("Solid Jukebox asset packaging complete.") if __name__ == "__main__": main()