#!/usr/bin/env python3 """ gen_icon.py — the AVE27 app icon, drawn with a signed-distance renderer and written as a raw PNG (no image library). The mark: a stack of five temporal layers — an SVC pyramid, widest at the base. The top layer is displaced up and out past the stack's bounding edge: the layer count that walks past what the driver sized. Deep slate field, steel layers, the escaping layer in hot ember with the halo of the write landing somewhere it was never allocated. """ import math import os import struct import zlib OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "Resources", "Assets.xcassets", "AppIcon.appiconset") os.makedirs(OUT, exist_ok=True) SIZE = 1024 def srgb(c): return max(0, min(255, int(round(c)))) def mix(a, b, t): return tuple(a[i] + (b[i] - a[i]) * t for i in range(3)) def smoothstep(e0, e1, x): t = max(0.0, min(1.0, (x - e0) / (e1 - e0))) return t * t * (3 - 2 * t) def sd_round_rect(px, py, cx, cy, hw, hh, r): qx = abs(px - cx) - (hw - r) qy = abs(py - cy) - (hh - r) outside = math.hypot(max(qx, 0.0), max(qy, 0.0)) inside = min(max(qx, qy), 0.0) return outside + inside - r # ---------------------------------------------------------------- palette BG_TOP = (0.086, 0.094, 0.118) BG_BOT = (0.024, 0.026, 0.038) LAYER_COLD = (0.36, 0.44, 0.58) LAYER_WARM = (0.52, 0.62, 0.78) EMBER = (1.00, 0.44, 0.14) EMBER_HOT = (1.00, 0.76, 0.38) # stack geometry: five layers, widths grow downward, top one escapes STACK_CY = SIZE * 0.555 LAYER_H = SIZE * 0.082 LAYER_GAP = SIZE * 0.028 BASE_HALF = SIZE * 0.30 TOP_HALF = SIZE * 0.115 LAYERS = 5 # the escape: top layer displaced up-right, past the stack's right edge ESC_DX = SIZE * 0.145 ESC_DY = -SIZE * 0.085 def render(): out = bytearray() half = SIZE / 2 radius = SIZE * 0.2237 tau = math.tau cos, sin, hypot = math.cos, math.sin, math.hypot # precompute layer rects: index 0 = base .. LAYERS-1 = top step = LAYER_H + LAYER_GAP total = LAYERS * LAYER_H + (LAYERS - 1) * LAYER_GAP rects = [] for i in range(LAYERS): t = i / (LAYERS - 1) hw = BASE_HALF + (TOP_HALF - BASE_HALF) * t cy = STACK_CY + total / 2 - LAYER_H / 2 - i * step cx = half r = LAYER_H * 0.32 rects.append((cx, cy, hw, LAYER_H / 2, r, t)) esc_rect = None if LAYERS >= 1: cx, cy, hw, hh, r, t = rects[-1] esc_rect = (cx + ESC_DX, cy + ESC_DY, hw, hh, r, t) esc_cx, esc_cy = esc_rect[0], esc_rect[1] halo_r = SIZE * 0.20 for y in range(SIZE): out.append(0) py = y + 0.5 for x in range(SIZE): px = x + 0.5 # background g = py / SIZE col = mix(BG_TOP, BG_BOT, g) d = hypot(px - half, py - SIZE * 0.47) lift = 1.0 - smoothstep(0.0, SIZE * 0.66, d) col = tuple(min(1.0, col[i] + 0.05 * lift) for i in range(3)) # faint baseline under the stack sd_base = abs(py - (STACK_CY + LAYER_H * 2.6)) - SIZE * 0.004 if abs(px - half) < BASE_HALF * 1.25: cov = 1.0 - smoothstep(-1.0, 1.0, sd_base) col = mix(col, LAYER_COLD, cov * 0.22) # the stack: steel gradient, cold at base, warmer toward the top for (cx, cy, hw, hh, r, t) in rects[:-1]: sd = sd_round_rect(px, py, cx, cy, hw, hh, r) if sd < 1.2: cov = 1.0 - smoothstep(-1.2, 1.2, sd) layer_col = mix(LAYER_COLD, LAYER_WARM, t) col = mix(col, layer_col, cov * 0.9) # the escaping layer: ember, halo behind it d_esc = hypot(px - esc_cx, py - esc_cy) if d_esc < halo_r: halo = 1.0 - smoothstep(0.0, halo_r, d_esc) hk = 0.22 * halo * halo col = tuple(min(1.0, col[i] + EMBER[i] * hk) for i in range(3)) (cx, cy, hw, hh, r, t) = esc_rect sd = sd_round_rect(px, py, cx, cy, hw, hh, r) if sd < 1.2: cov = 1.0 - smoothstep(-1.2, 1.2, sd) # hot edge on the leading (upper-right) side heat = smoothstep(0.0, 1.0, (px - cx + hw) / (2 * hw)) * 0.7 col = mix(col, mix(EMBER, EMBER_HOT, heat), cov) # the trail: where the escaping layer used to sit — an ember outline (cx, cy, hw, hh, r, t) = rects[-1] sd_trail = abs(sd_round_rect(px, py, cx, cy, hw, hh, r)) - SIZE * 0.006 if sd_trail < 1.2: cov = (1.0 - smoothstep(-1.2, 1.2, sd_trail)) * 0.8 col = mix(col, EMBER, cov) # rounded-rect alpha sd_r = sd_round_rect(px, py, half, half, half - 0.5, half - 0.5, radius) a = 1.0 - smoothstep(-0.75, 0.75, sd_r) out += bytes([srgb(min(1.0, col[0]) * 255), srgb(min(1.0, col[1]) * 255), srgb(min(1.0, col[2]) * 255), srgb(a * 255)]) return bytes(out) def write_png(path, width, height, rgba): def chunk(tag, data): c = struct.pack(">I", len(data)) + tag + data c += struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF) return c ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0) raw = zlib.compress(rgba, 9) png = (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", raw) + chunk(b"IEND", b"")) with open(path, "wb") as f: f.write(png) def main(): data = render() icon = os.path.join(OUT, "AppIcon-1024.png") write_png(icon, SIZE, SIZE, data) print("wrote", icon, os.path.getsize(icon), "bytes") if __name__ == "__main__": main()