#!/usr/bin/env python3 """Generate Archipelago GRUB boot background — 80s pixel cyberpunk aesthetic. Outputs a 1024x768 PNG with: - Near-black background with subtle radial gradient - Scanline overlay (CRT effect) - Pixel-art "A" logo (from Archipelago SVG) rendered in neon orange - Neon glow effect around the logo - Retro grid lines at the bottom (Tron-style horizon) - Subtle vignette Uses only PIL (Pillow) — no external dependencies. """ import struct import zlib import math import sys import os W, H = 1024, 768 # Archipelago brand colors BG_DARK = (5, 5, 10) # Near-black with blue tint BG_MID = (10, 10, 18) # Slightly lighter center ORANGE = (251, 146, 60) # #fb923c — primary accent ORANGE_DIM = (180, 100, 30) # Dimmed orange for glow CYAN = (60, 200, 220) # Cyberpunk accent MAGENTA = (180, 60, 180) # Cyberpunk accent 2 GRID_COLOR = (30, 60, 80) # Subtle teal grid SCANLINE = (0, 0, 0) # Black scanlines # The pixel-art "a" (lowercase) from the Archipelago SVG favicon # Matched to the actual SVG path — it's a blocky pixel-art lowercase "a" LOGO_A = [ [0, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 1], [0, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 1], [0, 1, 1, 1, 1, 1], ] # Pixel-art text: "archipelago" — 5-pixel-high bitmap font (lowercase) PIXEL_CHARS = { 'a': [[0,1,1,0],[0,0,0,1],[0,1,1,1],[1,0,0,1],[0,1,1,1]], 'r': [[1,0,1,1],[1,1,0,0],[1,0,0,0],[1,0,0,0],[1,0,0,0]], 'c': [[0,1,1,0],[1,0,0,0],[1,0,0,0],[1,0,0,0],[0,1,1,0]], 'h': [[1,0,0,0],[1,0,0,0],[1,1,1,0],[1,0,0,1],[1,0,0,1]], 'i': [[0,1],[0,0],[0,1],[0,1],[0,1]], 'p': [[1,1,1,0],[1,0,0,1],[1,1,1,0],[1,0,0,0],[1,0,0,0]], 'e': [[0,1,1,0],[1,0,0,1],[1,1,1,0],[1,0,0,0],[0,1,1,0]], 'l': [[1,0],[1,0],[1,0],[1,0],[1,1]], 'g': [[0,1,1,1],[1,0,0,1],[0,1,1,1],[0,0,0,1],[1,1,1,0]], 'o': [[0,1,1,0],[1,0,0,1],[1,0,0,1],[1,0,0,1],[0,1,1,0]], ' ': [[0,0],[0,0],[0,0],[0,0],[0,0]], } LOGO_TEXT = "archipelago" def lerp_color(c1, c2, t): """Linearly interpolate between two RGB colors.""" return tuple(int(a + (b - a) * t) for a, b in zip(c1, c2)) def distance(x1, y1, x2, y2): return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) def create_png(width, height, pixels): """Create a PNG file from raw RGB pixel data without PIL.""" def make_chunk(chunk_type, data): chunk = chunk_type + data return struct.pack('>I', len(data)) + chunk + struct.pack('>I', zlib.crc32(chunk) & 0xFFFFFFFF) # PNG signature signature = b'\x89PNG\r\n\x1a\n' # IHDR ihdr_data = struct.pack('>IIBBBBB', width, height, 8, 2, 0, 0, 0) # 8-bit RGB ihdr = make_chunk(b'IHDR', ihdr_data) # IDAT — raw pixel data with filter byte per row raw_data = bytearray() for y in range(height): raw_data.append(0) # filter: none offset = y * width * 3 raw_data.extend(pixels[offset:offset + width * 3]) compressed = zlib.compress(bytes(raw_data), 9) idat = make_chunk(b'IDAT', compressed) # IEND iend = make_chunk(b'IEND', b'') return signature + ihdr + idat + iend def generate(): # Create pixel buffer (RGB, row-major) pixels = bytearray(W * H * 3) cx, cy = W // 2, H // 2 - 40 # Center, slightly above middle # --- Background: radial gradient --- max_dist = distance(0, 0, cx, cy) for y in range(H): for x in range(W): d = distance(x, y, cx, cy) / max_dist d = min(d, 1.0) bg = lerp_color(BG_MID, BG_DARK, d * d) # Quadratic falloff # Vignette — darken edges vx = abs(x - cx) / (W / 2) vy = abs(y - cy) / (H / 2) vignette = max(0, 1.0 - (vx * vx + vy * vy) * 0.4) r = int(bg[0] * vignette) g = int(bg[1] * vignette) b = int(bg[2] * vignette) idx = (y * W + x) * 3 pixels[idx] = max(0, min(255, r)) pixels[idx + 1] = max(0, min(255, g)) pixels[idx + 2] = max(0, min(255, b)) # --- Retro grid (bottom third) --- horizon_y = H * 2 // 3 for y in range(horizon_y, H): depth = (y - horizon_y) / (H - horizon_y) # 0 at horizon, 1 at bottom # Horizontal grid lines — spacing decreases with perspective grid_spacing = max(4, int(40 * (1.0 - depth * 0.8))) is_hline = ((y - horizon_y) % grid_spacing) < 1 for x in range(W): # Vertical grid lines — converge toward center spread = 0.3 + depth * 0.7 # Lines spread more toward bottom grid_x = (x - cx) / (spread * W / 2) * 12 is_vline = abs(grid_x - round(grid_x)) < 0.04 if is_hline or is_vline: alpha = 0.15 + depth * 0.25 # Brighter closer to viewer idx = (y * W + x) * 3 for c in range(3): old = pixels[idx + c] pixels[idx + c] = min(255, int(old + GRID_COLOR[c] * alpha)) # --- Horizon glow line --- for x in range(W): dx = abs(x - cx) / (W / 2) intensity = max(0, 1.0 - dx * 1.5) * 0.4 for dy in range(-2, 3): y = horizon_y + dy if 0 <= y < H: falloff = 1.0 - abs(dy) / 3.0 idx = (y * W + x) * 3 pixels[idx] = min(255, int(pixels[idx] + CYAN[0] * intensity * falloff)) pixels[idx + 1] = min(255, int(pixels[idx + 1] + CYAN[1] * intensity * falloff)) pixels[idx + 2] = min(255, int(pixels[idx + 2] + CYAN[2] * intensity * falloff)) # --- Pixel-art "A" logo --- logo_rows = len(LOGO_A) logo_cols = len(LOGO_A[0]) pixel_size = 14 logo_w = logo_cols * pixel_size logo_h = logo_rows * pixel_size logo_x = cx - logo_w // 2 logo_y = 80 # Glow behind logo glow_radius = 90 for y in range(max(0, logo_y - glow_radius), min(H, logo_y + logo_h + glow_radius)): for x in range(max(0, logo_x - glow_radius), min(W, logo_x + logo_w + glow_radius)): # Distance to logo bounding box dx = max(0, logo_x - x, x - (logo_x + logo_w)) dy = max(0, logo_y - y, y - (logo_y + logo_h)) d = math.sqrt(dx * dx + dy * dy) if d < glow_radius: alpha = (1.0 - d / glow_radius) ** 2 * 0.15 idx = (y * W + x) * 3 pixels[idx] = min(255, int(pixels[idx] + ORANGE[0] * alpha)) pixels[idx + 1] = min(255, int(pixels[idx + 1] + ORANGE[1] * alpha)) pixels[idx + 2] = min(255, int(pixels[idx + 2] + ORANGE[2] * alpha)) # Draw logo pixels with 3D depth/shadow effect shadow_offset = 3 # Pixel offset for 3D shadow for row in range(logo_rows): for col in range(logo_cols): if LOGO_A[row][col]: px = logo_x + col * pixel_size py = logo_y + row * pixel_size # Shadow layer (dark, offset down-right) for dy in range(pixel_size - 1): for dx in range(pixel_size - 1): x = px + dx + shadow_offset y = py + dy + shadow_offset if 0 <= x < W and 0 <= y < H: idx = (y * W + x) * 3 pixels[idx] = max(0, pixels[idx] - 5) pixels[idx + 1] = min(255, pixels[idx + 1] + 15) pixels[idx + 2] = min(255, pixels[idx + 2] + 20) # Main pixel with highlight gradient (brighter at top-left) for dy in range(pixel_size - 1): for dx in range(pixel_size - 1): x, y = px + dx, py + dy if 0 <= x < W and 0 <= y < H: # Gradient: top-left bright, bottom-right darker t = (dx + dy) / (2 * pixel_size) r = int(ORANGE[0] * (1.0 - t * 0.3)) g = int(ORANGE[1] * (1.0 - t * 0.3)) b = int(ORANGE[2] * (1.0 - t * 0.3)) # Top-left highlight for 3D bevel if dx < 2 or dy < 2: r = min(255, r + 40) g = min(255, g + 30) b = min(255, b + 10) idx = (y * W + x) * 3 pixels[idx] = r pixels[idx + 1] = g pixels[idx + 2] = b # --- Pixel-art text "archipelago" below logo --- text_pixel = 4 # Smaller pixels for text text_gap = 2 # Gap between characters in pixels # Calculate total text width total_w = 0 for ch in LOGO_TEXT: char_data = PIXEL_CHARS.get(ch, PIXEL_CHARS[' ']) total_w += len(char_data[0]) * text_pixel + text_gap total_w -= text_gap # No gap after last char text_x = cx - total_w // 2 text_y = logo_y + logo_h + 20 # Below the logo cursor_x = text_x for ch in LOGO_TEXT: char_data = PIXEL_CHARS.get(ch, PIXEL_CHARS[' ']) char_h = len(char_data) char_w = len(char_data[0]) for row in range(char_h): for col in range(char_w): if char_data[row][col]: px = cursor_x + col * text_pixel py = text_y + row * text_pixel for dy in range(text_pixel - 1): for dx in range(text_pixel - 1): x, y = px + dx, py + dy if 0 <= x < W and 0 <= y < H: idx = (y * W + x) * 3 # Dimmer orange for text pixels[idx] = ORANGE_DIM[0] pixels[idx + 1] = ORANGE_DIM[1] pixels[idx + 2] = ORANGE_DIM[2] cursor_x += char_w * text_pixel + text_gap # --- Decorative neon lines flanking the text --- line_y = text_y + 5 * text_pixel + 12 line_w = total_w + 40 line_x1 = cx - line_w // 2 line_x2 = cx + line_w // 2 for x in range(line_x1, line_x2): if 0 <= x < W: # Fade at edges edge_dist = min(x - line_x1, line_x2 - x) alpha = min(1.0, edge_dist / 30.0) * 0.5 for dy in range(2): y = line_y + dy if 0 <= y < H: idx = (y * W + x) * 3 pixels[idx] = min(255, int(pixels[idx] + CYAN[0] * alpha * 0.3)) pixels[idx + 1] = min(255, int(pixels[idx + 1] + CYAN[1] * alpha * 0.3)) pixels[idx + 2] = min(255, int(pixels[idx + 2] + CYAN[2] * alpha * 0.3)) # --- "self-sovereign bitcoin infrastructure" tagline --- TAG_CHARS = { 's': [[0,1,1],[1,0,0],[0,1,0],[0,0,1],[1,1,0]], 'f': [[0,1,1],[1,0,0],[1,1,0],[1,0,0],[1,0,0]], '-': [[0,0,0],[0,0,0],[1,1,1],[0,0,0],[0,0,0]], 'v': [[1,0,1],[1,0,1],[1,0,1],[0,1,0],[0,1,0]], 'n': [[1,0,0,0],[1,1,1,0],[1,0,0,1],[1,0,0,1],[1,0,0,1]], 'b': [[1,0,0,0],[1,0,0,0],[1,1,1,0],[1,0,0,1],[1,1,1,0]], 't': [[0,1,0],[1,1,1],[0,1,0],[0,1,0],[0,0,1]], 'd': [[0,0,0,1],[0,0,0,1],[0,1,1,1],[1,0,0,1],[0,1,1,1]], 'u': [[1,0,0,1],[1,0,0,1],[1,0,0,1],[1,0,0,1],[0,1,1,1]], } # Merge with existing chars all_chars = {**PIXEL_CHARS, **TAG_CHARS} tagline = "self-sovereign bitcoin node" tag_pixel = 3 tag_gap = 2 tag_total = sum(len(all_chars.get(c, all_chars[' '])[0]) * tag_pixel + tag_gap for c in tagline) - tag_gap tag_x = cx - tag_total // 2 tag_y = line_y + 8 tag_cursor = tag_x for ch in tagline: char_data = all_chars.get(ch, all_chars[' ']) char_h = len(char_data) char_w = len(char_data[0]) for row in range(char_h): for col in range(char_w): if char_data[row][col]: px = tag_cursor + col * tag_pixel py = tag_y + row * tag_pixel for dy in range(tag_pixel - 1): for dx in range(tag_pixel - 1): x, y = px + dx, py + dy if 0 <= x < W and 0 <= y < H: idx = (y * W + x) * 3 pixels[idx] = min(255, pixels[idx] + 40) pixels[idx + 1] = min(255, pixels[idx + 1] + 50) pixels[idx + 2] = min(255, pixels[idx + 2] + 55) tag_cursor += char_w * tag_pixel + tag_gap # --- Scanlines (every other row, subtle) --- for y in range(0, H, 2): for x in range(W): idx = (y * W + x) * 3 pixels[idx] = int(pixels[idx] * 0.92) pixels[idx + 1] = int(pixels[idx + 1] * 0.92) pixels[idx + 2] = int(pixels[idx + 2] * 0.92) # --- Generate PNG --- png_data = create_png(W, H, bytes(pixels)) return png_data if __name__ == '__main__': out_path = sys.argv[1] if len(sys.argv) > 1 else 'background.png' png_data = generate() with open(out_path, 'wb') as f: f.write(png_data) print(f'Generated {out_path} ({len(png_data)} bytes)')