Apply .gitattributes normalization to convert all CRLF line endings inherited from Windows-origin source files to Unix LF. 175 files, zero content changes.
125 lines
3.8 KiB
Python
125 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Pre-render splash art as ANSI half-block text for instant display.
|
|
|
|
Converts source images (PNG/JPG/GIF) into .ans files using Unicode half-block
|
|
characters with 24-bit ANSI color. This eliminates the runtime Pillow decode
|
|
and textual-image protocol detection, making splash display instant.
|
|
|
|
Each terminal cell renders two vertical pixels using the upper-half-block
|
|
character (U+2580 '▀') with fg=top_pixel, bg=bottom_pixel for 2x vertical
|
|
resolution.
|
|
|
|
Usage:
|
|
python scripts/prebake_splash.py [--width 80]
|
|
"""
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
from PIL import Image
|
|
|
|
ASSETS_DIR = Path(__file__).resolve().parent.parent / "src" / "skywalker_tui" / "assets" / "splash"
|
|
|
|
# Source images to pre-bake
|
|
SOURCE_FILES = [
|
|
"seti-satellite.png",
|
|
"dialtone.jpg",
|
|
"so-far-away.gif",
|
|
"prodigy-out-of-space.jpg",
|
|
"space-docker.gif",
|
|
]
|
|
|
|
|
|
def image_to_ansi(img_path: Path, width: int = 80) -> str:
|
|
"""Convert an image to ANSI half-block art.
|
|
|
|
Uses ▀ (upper half block) with fg=top pixel, bg=bottom pixel to get
|
|
2x vertical resolution. Emits 24-bit ANSI color escape sequences,
|
|
optimized to only change fg/bg when the color actually changes.
|
|
"""
|
|
img = Image.open(img_path)
|
|
|
|
# Use first frame for animated GIFs
|
|
if hasattr(img, "n_frames") and img.n_frames > 1:
|
|
img.seek(0)
|
|
|
|
img = img.convert("RGBA")
|
|
|
|
# Resize maintaining aspect ratio, height rounded to even for half-blocks
|
|
ratio = img.height / img.width
|
|
pixel_height = int(width * ratio)
|
|
pixel_height += pixel_height % 2 # ensure even
|
|
img = img.resize((width, pixel_height), Image.LANCZOS)
|
|
|
|
lines = []
|
|
for y in range(0, pixel_height, 2):
|
|
chunks = []
|
|
prev_fg = None
|
|
prev_bg = None
|
|
|
|
for x in range(width):
|
|
tr, tg, tb, ta = img.getpixel((x, y))
|
|
if y + 1 < pixel_height:
|
|
br, bg, bb, ba = img.getpixel((x, y + 1))
|
|
else:
|
|
br, bg, bb, ba = 0, 0, 0, 0
|
|
|
|
# Treat near-transparent pixels as black
|
|
if ta < 128:
|
|
tr, tg, tb = 0, 0, 0
|
|
if ba < 128:
|
|
br, bg, bb = 0, 0, 0
|
|
|
|
fg = (tr, tg, tb)
|
|
bk = (br, bg, bb)
|
|
|
|
# Only emit escape codes when color changes
|
|
codes = []
|
|
if fg != prev_fg:
|
|
codes.append(f"\033[38;2;{fg[0]};{fg[1]};{fg[2]}m")
|
|
prev_fg = fg
|
|
if bk != prev_bg:
|
|
codes.append(f"\033[48;2;{bk[0]};{bk[1]};{bk[2]}m")
|
|
prev_bg = bk
|
|
|
|
chunks.append("".join(codes) + "\u2580")
|
|
|
|
lines.append("".join(chunks) + "\033[0m")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Pre-bake splash art as ANSI half-block text")
|
|
parser.add_argument("--width", type=int, default=80, help="Output width in columns (default: 80)")
|
|
args = parser.parse_args()
|
|
|
|
if not ASSETS_DIR.exists():
|
|
print(f"Assets directory not found: {ASSETS_DIR}")
|
|
return
|
|
|
|
for filename in SOURCE_FILES:
|
|
src = ASSETS_DIR / filename
|
|
if not src.exists():
|
|
print(f" SKIP {filename} (not found)")
|
|
continue
|
|
|
|
stem = src.stem
|
|
dst = ASSETS_DIR / f"{stem}.ans"
|
|
|
|
print(f" BAKE {filename} -> {stem}.ans ({args.width} cols) ...", end=" ", flush=True)
|
|
ansi_text = image_to_ansi(src, width=args.width)
|
|
dst.write_text(ansi_text)
|
|
|
|
# Stats
|
|
src_kb = src.stat().st_size / 1024
|
|
dst_kb = dst.stat().st_size / 1024
|
|
line_count = ansi_text.count("\n") + 1
|
|
print(f"{src_kb:.1f}KB -> {dst_kb:.1f}KB ({line_count} lines)")
|
|
|
|
print("\nDone. Pre-baked .ans files are ready for instant splash display.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|