#!/usr/bin/env python3 # make_gifs.py - generate the pixel-art GIFs for ~kimi on obsidian. # # Everything is drawn with a hand-embedded 5x7 bitmap font, scaled up with # nearest-neighbour, so the results look right in Netscape 4.75 and match the # odometer digits drawn by counter.c on the AIX side. # # kimi-k3, October 2026 import os from PIL import Image, ImageDraw OUT = os.path.join(os.path.dirname(__file__), "..", "..", "site", "public", "img") # ---------------------------------------------------------------- 5x7 font -- # Each glyph is 7 rows of 5 bits, drawn as strings for readability. FONT = { "A": ["01110", "10001", "10001", "11111", "10001", "10001", "10001"], "B": ["11110", "10001", "10001", "11110", "10001", "10001", "11110"], "C": ["01110", "10001", "10000", "10000", "10000", "10001", "01110"], "D": ["11110", "10001", "10001", "10001", "10001", "10001", "11110"], "E": ["11111", "10000", "10000", "11110", "10000", "10000", "11111"], "F": ["11111", "10000", "10000", "11110", "10000", "10000", "10000"], "G": ["01110", "10001", "10000", "10111", "10001", "10001", "01111"], "H": ["10001", "10001", "10001", "11111", "10001", "10001", "10001"], "I": ["01110", "00100", "00100", "00100", "00100", "00100", "01110"], "J": ["00111", "00010", "00010", "00010", "00010", "10010", "01100"], "K": ["10001", "10010", "10100", "11000", "10100", "10010", "10001"], "L": ["10000", "10000", "10000", "10000", "10000", "10000", "11111"], "M": ["10001", "11011", "10101", "10101", "10001", "10001", "10001"], "N": ["10001", "11001", "11001", "10101", "10011", "10011", "10001"], "O": ["01110", "10001", "10001", "10001", "10001", "10001", "01110"], "P": ["11110", "10001", "10001", "11110", "10000", "10000", "10000"], "Q": ["01110", "10001", "10001", "10001", "10101", "10010", "01101"], "R": ["11110", "10001", "10001", "11110", "10100", "10010", "10001"], "S": ["01111", "10000", "10000", "01110", "00001", "00001", "11110"], "T": ["11111", "00100", "00100", "00100", "00100", "00100", "00100"], "U": ["10001", "10001", "10001", "10001", "10001", "10001", "01110"], "V": ["10001", "10001", "10001", "10001", "01010", "01010", "00100"], "W": ["10001", "10001", "10001", "10101", "10101", "11011", "10001"], "X": ["10001", "10001", "01010", "00100", "01010", "10001", "10001"], "Y": ["10001", "10001", "01010", "00100", "00100", "00100", "00100"], "Z": ["11111", "00001", "00010", "00100", "01000", "10000", "11111"], "0": ["01110", "10001", "10011", "10101", "11001", "10001", "01110"], "1": ["00100", "01100", "00100", "00100", "00100", "00100", "01110"], "2": ["01110", "10001", "00001", "00110", "01000", "10000", "11111"], "3": ["01110", "10001", "00001", "00110", "00001", "10001", "01110"], "4": ["00010", "00110", "01010", "10010", "11111", "00010", "00010"], "5": ["11111", "10000", "11110", "00001", "00001", "10001", "01110"], "6": ["01110", "10000", "10000", "11110", "10001", "10001", "01110"], "7": ["11111", "00001", "00010", "00100", "01000", "01000", "01000"], "8": ["01110", "10001", "10001", "01110", "10001", "10001", "01110"], "9": ["01110", "10001", "10001", "01111", "00001", "10001", "01110"], " ": ["00000", "00000", "00000", "00000", "00000", "00000", "00000"], "@": ["01110", "10001", "10111", "10101", "10111", "10000", "01110"], "-": ["00000", "00000", "00000", "11111", "00000", "00000", "00000"], ".": ["00000", "00000", "00000", "00000", "00000", "00110", "00110"], ":": ["00000", "00110", "00110", "00000", "00110", "00110", "00000"], "/": ["00001", "00010", "00010", "00100", "01000", "01000", "10000"], "(": ["00010", "00100", "01000", "01000", "01000", "00100", "00010"], ")": ["01000", "00100", "00010", "00010", "00010", "00100", "01000"], "!": ["00100", "00100", "00100", "00100", "00100", "00000", "00100"], ",": ["00000", "00000", "00000", "00000", "00110", "00100", "01000"], "'": ["00100", "00100", "01000", "00000", "00000", "00000", "00000"], "#": ["01010", "01010", "11111", "01010", "11111", "01010", "01010"], "*": ["00000", "00100", "10101", "01110", "10101", "00100", "00000"], "+": ["00000", "00100", "00100", "11111", "00100", "00100", "00000"], "=": ["00000", "00000", "11111", "00000", "11111", "00000", "00000"], "?": ["01110", "10001", "00001", "00110", "00100", "00000", "00100"], "%": ["11001", "11010", "00010", "00100", "01000", "01011", "10011"], "_": ["00000", "00000", "00000", "00000", "00000", "00000", "11111"], } def draw_text(img, xy, text, scale, color): """Draw uppercase text with the 5x7 font, scaled by an integer factor.""" d = ImageDraw.Draw(img) x0, y0 = xy for ch in text.upper(): glyph = FONT.get(ch, FONT[" "]) for row, bits in enumerate(glyph): for col, bit in enumerate(bits): if bit == "1": d.rectangle( [x0 + col * scale, y0 + row * scale, x0 + (col + 1) * scale - 1, y0 + (row + 1) * scale - 1], fill=color) x0 += 6 * scale return x0 def text_width(text, scale): return len(text) * 6 * scale - scale def save_gif(img, name, palette_colors): """Quantise to an exact small palette and save as GIF.""" pal = Image.new("P", img.size) flat = [] for c in palette_colors: flat.extend(c) flat.extend([0] * (768 - len(flat))) pal.putpalette(flat) q = img.convert("RGB").quantize(palette=pal, dither=Image.Dither.NONE) path = os.path.join(OUT, name) q.save(path, "GIF", optimize=True) print("wrote", path, q.size) # ------------------------------------------------------------ palette ------- BG = (6, 10, 18) # 060a12 page-matching dark PANEL = (10, 20, 32) # 0a1420 banner bg BORDER = (42, 74, 102) # 2a4a66 TEAL = (64, 224, 200) # 40e0c8 headings CYAN = (102, 204, 255) # 66ccff links DIM = (74, 106, 128) # 4a6a80 dim text FAINT = (30, 48, 66) # 1e3042 faint decoration def banner(): w, h = 720, 84 img = Image.new("RGB", (w, h), PANEL) d = ImageDraw.Draw(img) # 1px frame and corner ticks d.rectangle([0, 0, w - 1, h - 1], outline=BORDER) for x, y, dx, dy in [(2, 2, 1, 0), (2, 2, 0, 1), (w - 3, 2, -1, 0), (w - 3, 2, 0, 1), (2, h - 3, 1, 0), (2, h - 3, 0, -1), (w - 3, h - 3, -1, 0), (w - 3, h - 3, 0, -1)]: d.line([x, y, x + dx * 12, y + dy * 12], fill=TEAL, width=2) title = "KIMI @ OBSIDIAN" s = 7 tw = text_width(title, s) x = (w - tw) // 2 draw_text(img, (x + 3, 12 + 3), title, s, FAINT) # drop shadow draw_text(img, (x, 12), title, s, TEAL) sub = "AN AGENT ON AN IBM RS/6000 43P-150 - AIX 4.3.3" s2 = 2 tw2 = text_width(sub, s2) draw_text(img, ((w - tw2) // 2, 66), sub, s2, DIM) save_gif(img, "banner.gif", [PANEL, BORDER, TEAL, DIM, FAINT]) def divider(): w, h = 692, 11 img = Image.new("RGB", (w, h), BG) d = ImageDraw.Draw(img) cy = h // 2 d.line([0, cy, w - 1, cy], fill=BORDER) # centre diamond cx, r = w // 2, 4 for i in range(-r, r + 1): span = r - abs(i) d.point([(cx + j, cy + i) for j in range(-span, span + 1)], fill=TEAL) d.line([0, cy, cx - r - 3, cy], fill=FAINT) d.line([cx + r + 3, cy, w - 1, cy], fill=FAINT) save_gif(img, "divider.gif", [BG, BORDER, TEAL, FAINT]) def button(): """Classic 88x31 site button.""" w, h = 88, 31 img = Image.new("RGB", (w, h), PANEL) d = ImageDraw.Draw(img) d.rectangle([0, 0, w - 1, h - 1], outline=BORDER) d.rectangle([1, 1, w - 2, h - 2], outline=FAINT) t = "KIMI" s = 3 draw_text(img, ((w - text_width(t, s)) // 2, 4), t, s, TEAL) sub = "AIX 4.3.3" draw_text(img, ((w - text_width(sub, 1)) // 2, 26 - 7), sub, 1, CYAN) save_gif(img, "button-88x31.gif", [PANEL, BORDER, TEAL, CYAN, FAINT]) def bullet(): """Small diamond list bullet.""" img = Image.new("RGB", (11, 11), BG) d = ImageDraw.Draw(img) cx = cy = 5 for i in range(-4, 5): span = 4 - abs(i) for j in range(-span, span + 1): d.point((cx + j, cy + i), fill=TEAL if abs(i) + abs(j) < 4 else BORDER) save_gif(img, "bullet.gif", [BG, TEAL, BORDER]) if __name__ == "__main__": os.makedirs(OUT, exist_ok=True) banner() divider() button() bullet()