Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
165 lines
5.3 KiB
Python
165 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
GOD Frame iOS AppIcon generator (ADR-0027 Phase 8.6).
|
|
|
|
Native pixel art at 64x64 (Tor sprite on hex-accented bg #2D3142),
|
|
upscaled NEAREST 16x to 1024x1024 master, then Lanczos-resized to every
|
|
filename referenced by AppIcon.appiconset/Contents.json.
|
|
|
|
Outputs:
|
|
- god_frame/assets/icon/icon_64.png (native pixel art)
|
|
- god_frame/assets/icon/icon_1024.png (master, RGB no alpha)
|
|
- god_frame/ios/Runner/Assets.xcassets/AppIcon.appiconset/<size>.png (all)
|
|
"""
|
|
from PIL import Image
|
|
import json
|
|
import os
|
|
import re
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
SPRITE_PATH = os.path.join(ROOT, "docs/16neo/sprites/pes-tor-happy.png")
|
|
ICON_DIR = os.path.join(ROOT, "god_frame/assets/icon")
|
|
APPICON_DIR = os.path.join(
|
|
ROOT, "god_frame/ios/Runner/Assets.xcassets/AppIcon.appiconset"
|
|
)
|
|
CONTENTS_JSON = os.path.join(APPICON_DIR, "Contents.json")
|
|
os.makedirs(ICON_DIR, exist_ok=True)
|
|
|
|
# Palette — matches splash where it overlaps; bg per architect brief #2D3142.
|
|
P = {
|
|
"bg": (45, 49, 66), # #2D3142 — base
|
|
"bg_light": (66, 71, 92), # lighter inner panel
|
|
"bg_dark": (28, 32, 46), # outer rim
|
|
"accent": (192, 132, 252), # #c084fc HLTRN purple
|
|
"accent_dim": (107, 107, 141), # dim purple
|
|
"gold": (212, 160, 23), # #d4a017
|
|
"white": (245, 245, 250),
|
|
"lightsky": (168, 218, 220),
|
|
}
|
|
|
|
W, H = 64, 64
|
|
img = Image.new("RGB", (W, H), P["bg"])
|
|
px = img.load()
|
|
|
|
|
|
def setpx(x, y, c):
|
|
if 0 <= x < W and 0 <= y < H:
|
|
px[x, y] = c
|
|
|
|
|
|
def hline(x1, x2, y, c):
|
|
for x in range(x1, x2 + 1):
|
|
setpx(x, y, c)
|
|
|
|
|
|
def vline(x, y1, y2, c):
|
|
for y in range(y1, y2 + 1):
|
|
setpx(x, y, c)
|
|
|
|
|
|
# ---- Outer rim (dark) ----
|
|
for y in range(H):
|
|
for x in range(W):
|
|
if x < 1 or x > W - 2 or y < 1 or y > H - 2:
|
|
setpx(x, y, P["bg_dark"])
|
|
|
|
# ---- Inner panel (slight lighter rectangle, hex-cornered) ----
|
|
# 4px corner cuts to echo hex theme
|
|
for y in range(2, H - 2):
|
|
for x in range(2, W - 2):
|
|
# cut the 4 corners diagonally for a hex-stop feel
|
|
d_tl = (2 - x) + (2 - y)
|
|
d_tr = (x - (W - 3)) + (2 - y)
|
|
d_bl = (2 - x) + (y - (H - 3))
|
|
d_br = (x - (W - 3)) + (y - (H - 3))
|
|
if max(d_tl, d_tr, d_bl, d_br) > 1:
|
|
continue
|
|
setpx(x, y, P["bg_light"])
|
|
|
|
# ---- Hex frame: 4 short purple ticks at the panel corners ----
|
|
def hex_corner(cx, cy, dx, dy, color):
|
|
# 3-pixel L mark pointing inward
|
|
setpx(cx, cy, color)
|
|
setpx(cx + dx, cy, color)
|
|
setpx(cx, cy + dy, color)
|
|
|
|
|
|
hex_corner(3, 3, 2, 2, P["accent"])
|
|
hex_corner(W - 4, 3, -2, 2, P["accent"])
|
|
hex_corner(3, H - 4, 2, -2, P["accent"])
|
|
hex_corner(W - 4, H - 4, -2, -2, P["accent"])
|
|
|
|
# ---- Mid-purple shadow halo behind Tor (soft dot grid) ----
|
|
cx, cy = 32, 32
|
|
for y in range(cy - 14, cy + 14):
|
|
for x in range(cx - 14, cx + 14):
|
|
if 0 <= x < W and 0 <= y < H:
|
|
dx = x - cx
|
|
dy = y - cy
|
|
r2 = dx * dx + dy * dy
|
|
if 80 < r2 < 170 and (x + y) % 2 == 0:
|
|
setpx(x, y, P["accent_dim"])
|
|
|
|
# ---- Place Tor sprite cropped to bbox, fitted to ~48x44 ----
|
|
tor = Image.open(SPRITE_PATH).convert("RGBA")
|
|
bbox = tor.getbbox() # (4, 0, 60, 52) for happy sprite
|
|
tor_crop = tor.crop(bbox)
|
|
# Target ~46 wide, preserve aspect
|
|
target_w = 46
|
|
ratio = target_w / tor_crop.width
|
|
target_h = max(1, round(tor_crop.height * ratio))
|
|
tor_small = tor_crop.resize((target_w, target_h), Image.NEAREST)
|
|
tx = (W - target_w) // 2
|
|
ty = (H - target_h) // 2 + 1 # nudge down 1px to balance
|
|
tor_px = tor_small.load()
|
|
for yy in range(target_h):
|
|
for xx in range(target_w):
|
|
r, g, b, a = tor_px[xx, yy]
|
|
if a > 30:
|
|
setpx(tx + xx, ty + yy, (r, g, b))
|
|
|
|
# ---- Tiny gold sparkle in top-right (echoes splash HLTRN gold) ----
|
|
sx, sy = W - 10, 9
|
|
setpx(sx, sy, P["gold"])
|
|
setpx(sx - 1, sy, P["accent_dim"])
|
|
setpx(sx + 1, sy, P["accent_dim"])
|
|
setpx(sx, sy - 1, P["accent_dim"])
|
|
setpx(sx, sy + 1, P["accent_dim"])
|
|
|
|
# ---- Save native + master ----
|
|
native_path = os.path.join(ICON_DIR, "icon_64.png")
|
|
img.save(native_path)
|
|
print(f"native saved: {native_path} size={img.size}")
|
|
|
|
master = img.resize((1024, 1024), Image.NEAREST)
|
|
master_path = os.path.join(ICON_DIR, "icon_1024.png")
|
|
master.save(master_path)
|
|
print(f"master saved: {master_path} size={master.size}")
|
|
|
|
|
|
# ---- Resize master into every filename referenced by Contents.json ----
|
|
with open(CONTENTS_JSON) as f:
|
|
contents = json.load(f)
|
|
|
|
filenames = sorted({entry["filename"] for entry in contents["images"]})
|
|
print(f"\nWriting {len(filenames)} icon files into {APPICON_DIR}\n")
|
|
|
|
for name in filenames:
|
|
m = re.match(r"^(\d+)\.png$", name)
|
|
if not m:
|
|
print(f" skip: {name} (cannot parse size)")
|
|
continue
|
|
size = int(m.group(1))
|
|
if size == 1024:
|
|
out = master # already 1024 NEAREST upscale — pixel-perfect
|
|
else:
|
|
# Lanczos for downscale (smooth at small sizes — needed at 16/20/29)
|
|
out = master.resize((size, size), Image.LANCZOS)
|
|
# Force RGB (no alpha — App Store rejects icons with alpha channel)
|
|
if out.mode != "RGB":
|
|
out = out.convert("RGB")
|
|
out_path = os.path.join(APPICON_DIR, name)
|
|
out.save(out_path)
|
|
print(f" {name:>10} {size}x{size}")
|
|
|
|
print(f"\n✅ {len(filenames)} icon files written.")
|