""" Icon generator for Screens — 5 distinct 1024x1024 iOS app icons. Philosophy: Luminous Distance (see PHILOSOPHY.md). Each icon fills the canvas edge-to-edge. iOS applies the squircle mask. """ from __future__ import annotations import math, os from PIL import Image, ImageDraw, ImageFilter, ImageChops SIZE = 1024 OUT = os.path.dirname(os.path.abspath(__file__)) # ---------------------------------------------------------------- helpers def blank(size=SIZE): return Image.new("RGBA", (size, size), (0, 0, 0, 0)) def hex_to_rgba(h, a=255): h = h.lstrip("#") return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16), a) def lerp(a, b, t): return a + (b - a) * t def lerp_color(c1, c2, t): return tuple(int(lerp(c1[i], c2[i], t)) for i in range(len(c1))) def linear_gradient(size, stops, angle_deg=90): """stops = [(t, (r,g,b,a)), ...] with t in [0,1]; angle 0=→, 90=↓""" w = h = size img = Image.new("RGBA", (w, h)) px = img.load() rad = math.radians(angle_deg) dx, dy = math.cos(rad), math.sin(rad) # Project (x,y) onto the direction vector, normalize to [0,1] cx, cy = w / 2, h / 2 # half-extent in direction ext = abs(dx) * w / 2 + abs(dy) * h / 2 for y in range(h): for x in range(w): t = ((x - cx) * dx + (y - cy) * dy) / (2 * ext) + 0.5 t = max(0.0, min(1.0, t)) # find segment for i in range(len(stops) - 1): t0, c0 = stops[i] t1, c1 = stops[i + 1] if t0 <= t <= t1: local = (t - t0) / (t1 - t0) if t1 > t0 else 0 px[x, y] = lerp_color(c0, c1, local) break else: px[x, y] = stops[-1][1] return img def radial_gradient(size, center, stops, radius=None): w = h = size if radius is None: radius = size * 0.75 img = Image.new("RGBA", (w, h)) px = img.load() cx, cy = center for y in range(h): for x in range(w): dx, dy = x - cx, y - cy d = math.sqrt(dx * dx + dy * dy) / radius d = max(0.0, min(1.0, d)) for i in range(len(stops) - 1): t0, c0 = stops[i] t1, c1 = stops[i + 1] if t0 <= d <= t1: local = (d - t0) / (t1 - t0) if t1 > t0 else 0 px[x, y] = lerp_color(c0, c1, local) break else: px[x, y] = stops[-1][1] return img def rounded_rect_mask(size, box, radius): mask = Image.new("L", size, 0) d = ImageDraw.Draw(mask) d.rounded_rectangle(box, radius=radius, fill=255) return mask def drop_shadow(shape_mask, offset=(0, 12), blur=40, opacity=120): sh = Image.new("RGBA", shape_mask.size, (0, 0, 0, 0)) sh.putalpha(shape_mask.point(lambda v: int(v * opacity / 255))) sh = sh.filter(ImageFilter.GaussianBlur(blur)) offset_mask = Image.new("RGBA", shape_mask.size, (0, 0, 0, 0)) offset_mask.paste(sh, offset, sh) return offset_mask def specular_highlight(mask, intensity=110): """Top-inner glow hinting at glass.""" w, h = mask.size grad = Image.new("L", (w, h)) for y in range(h): v = int(255 * max(0, 1 - y / (h * 0.55)) ** 2 * (intensity / 255)) for x in range(w): grad.putpixel((x, y), v) highlight = Image.new("RGBA", (w, h), (255, 255, 255, 0)) highlight.putalpha(ImageChops.multiply(grad, mask)) return highlight def paste_masked(base, layer, mask): """Paste `layer` onto `base` through `mask` (L mode).""" combined = Image.new("RGBA", base.size, (0, 0, 0, 0)) combined.paste(layer, (0, 0), mask) return Image.alpha_composite(base, combined) def noise_layer(size, amount=6): """Fine film grain for tactile quality.""" import random random.seed(42) layer = Image.new("L", (size, size)) px = layer.load() for y in range(size): for x in range(size): px[x, y] = 128 + random.randint(-amount, amount) noise = Image.new("RGBA", (size, size), (255, 255, 255, 0)) noise.putalpha(layer.point(lambda v: abs(v - 128) * 2)) return noise # ---------------------------------------------------------------- icon 1 — Portal def icon_portal(): canvas = linear_gradient(SIZE, [ (0.0, hex_to_rgba("#0B0B2A")), (0.6, hex_to_rgba("#1E1757")), (1.0, hex_to_rgba("#2A0E4D")), ], angle_deg=115) # Soft aura behind the portal aura = radial_gradient(SIZE, (SIZE / 2, SIZE / 2 - 20), [ (0.0, hex_to_rgba("#7B6DF5", 230)), (0.45, hex_to_rgba("#5B3FC0", 100)), (1.0, hex_to_rgba("#140B36", 0)), ], radius=SIZE * 0.55) canvas = Image.alpha_composite(canvas, aura) # Screen rectangle inset = 200 box = (inset, inset + 40, SIZE - inset, SIZE - inset - 40) radius = 70 # Back-lit glow glow_mask = rounded_rect_mask((SIZE, SIZE), (box[0] - 40, box[1] - 40, box[2] + 40, box[3] + 40), radius + 30) glow = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0)) glow.putalpha(glow_mask.filter(ImageFilter.GaussianBlur(36)).point(lambda v: int(v * 0.85))) glow_layer = Image.new("RGBA", (SIZE, SIZE), hex_to_rgba("#8A7BFF", 200)) glow_layer.putalpha(glow.getchannel("A")) canvas = Image.alpha_composite(canvas, glow_layer) # Screen surface gradient screen = linear_gradient(SIZE, [ (0.0, hex_to_rgba("#CBB8FF")), (0.35, hex_to_rgba("#7B63F0")), (1.0, hex_to_rgba("#2B1C6B")), ], angle_deg=112) screen_mask = rounded_rect_mask((SIZE, SIZE), box, radius) canvas = paste_masked(canvas, screen, screen_mask) # Inner bezel / rim light rim_outer = rounded_rect_mask((SIZE, SIZE), box, radius) inset_box = (box[0] + 8, box[1] + 8, box[2] - 8, box[3] - 8) rim_inner = rounded_rect_mask((SIZE, SIZE), inset_box, radius - 8) rim = ImageChops.subtract(rim_outer, rim_inner) rim_layer = Image.new("RGBA", (SIZE, SIZE), hex_to_rgba("#E4D8FF", 150)) rim_layer.putalpha(rim) canvas = Image.alpha_composite(canvas, rim_layer) # Specular sheen (top-inner highlight, clipped to screen) sheen = specular_highlight(screen_mask, intensity=90) canvas = Image.alpha_composite(canvas, sheen) # Inner scanline bloom — a soft horizontal ribbon implying content ribbon_box = (box[0] + 80, int((box[1] + box[3]) / 2) - 3, box[2] - 80, int((box[1] + box[3]) / 2) + 3) ribbon_mask = rounded_rect_mask((SIZE, SIZE), ribbon_box, 3).filter(ImageFilter.GaussianBlur(4)) ribbon_layer = Image.new("RGBA", (SIZE, SIZE), hex_to_rgba("#FFFFFF", 180)) ribbon_layer.putalpha(ribbon_mask) canvas = Image.alpha_composite(canvas, ribbon_layer) canvas.save(os.path.join(OUT, "icon-1-portal.png")) # ---------------------------------------------------------------- icon 2 — Cursor def icon_cursor(): canvas = linear_gradient(SIZE, [ (0.0, hex_to_rgba("#2E36E8")), (0.5, hex_to_rgba("#9B38E8")), (1.0, hex_to_rgba("#FF5A8F")), ], angle_deg=135) # Additional top-left glow to lift the cursor tip glow = radial_gradient(SIZE, (SIZE * 0.32, SIZE * 0.30), [ (0.0, hex_to_rgba("#FFFFFF", 160)), (0.5, hex_to_rgba("#B9B0FF", 60)), (1.0, hex_to_rgba("#3A2C9A", 0)), ], radius=SIZE * 0.6) canvas = Image.alpha_composite(canvas, glow) # Classic macOS arrow cursor — authored as polygon coordinates on a 100-unit grid cursor_norm = [ (0, 0), (0, 73), (20, 56), (32, 85), (44, 80), (33, 52), (56, 52) ] # Scale + center the cursor scale = 7.0 cursor_w = 56 * scale cursor_h = 85 * scale ox = (SIZE - cursor_w) / 2 - 20 oy = (SIZE - cursor_h) / 2 - 20 pts = [(ox + x * scale, oy + y * scale) for (x, y) in cursor_norm] # Soft shadow behind cursor shadow_mask = Image.new("L", (SIZE, SIZE), 0) ImageDraw.Draw(shadow_mask).polygon([(p[0] + 16, p[1] + 24) for p in pts], fill=255) shadow_mask = shadow_mask.filter(ImageFilter.GaussianBlur(22)) shadow_layer = Image.new("RGBA", (SIZE, SIZE), (20, 10, 60, 0)) shadow_layer.putalpha(shadow_mask.point(lambda v: int(v * 0.55))) canvas = Image.alpha_composite(canvas, shadow_layer) # Cursor outline (thin dark) and fill (white) — draw as two polygons cursor_outer = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0)) od = ImageDraw.Draw(cursor_outer) # Slight outline outset — simulate with stroked polygon # Draw outline by expanding the polygon by ~stroke/2 via line draw stroke = 18 od.polygon(pts, fill=(30, 14, 80, 255)) # Now shrink by drawing the white body slightly inset — simplest: re-use polygon, paint white with drawn stroke=0 cursor_body = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0)) bd = ImageDraw.Draw(cursor_body) # Inset polygon: compute centroid and pull points toward it by `stroke*0.35` for subtle inset cx = sum(p[0] for p in pts) / len(pts) cy = sum(p[1] for p in pts) / len(pts) inset = stroke * 0.35 inset_pts = [] for (x, y) in pts: dx, dy = x - cx, y - cy d = math.hypot(dx, dy) if d == 0: inset_pts.append((x, y)) else: inset_pts.append((x - dx / d * inset, y - dy / d * inset)) bd.polygon(inset_pts, fill=(255, 255, 255, 255)) # Cursor highlight gradient over the body body_mask = Image.new("L", (SIZE, SIZE), 0) ImageDraw.Draw(body_mask).polygon(inset_pts, fill=255) body_grad = linear_gradient(SIZE, [ (0.0, hex_to_rgba("#FFFFFF")), (1.0, hex_to_rgba("#D8D4FF")), ], angle_deg=110) cursor_body_grad = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0)) cursor_body_grad.paste(body_grad, (0, 0), body_mask) canvas = Image.alpha_composite(canvas, cursor_outer) canvas = Image.alpha_composite(canvas, cursor_body_grad) canvas.save(os.path.join(OUT, "icon-2-cursor.png")) # ---------------------------------------------------------------- icon 3 — Concentric Waves def icon_waves(): canvas = linear_gradient(SIZE, [ (0.0, hex_to_rgba("#031028")), (1.0, hex_to_rgba("#072047")), ], angle_deg=100) # Deep radial dim at the edges for vignette edge = radial_gradient(SIZE, (SIZE / 2, SIZE / 2), [ (0.0, hex_to_rgba("#000000", 0)), (0.65, hex_to_rgba("#000000", 0)), (1.0, hex_to_rgba("#000000", 120)), ], radius=SIZE * 0.75) canvas = Image.alpha_composite(canvas, edge) # Concentric rounded rectangles — 5 rings, smallest is bright, biggest is subtle rings = [ {"inset": 120, "radius": 160, "stroke": 18, "color": hex_to_rgba("#1AA3C2", 110)}, {"inset": 210, "radius": 130, "stroke": 16, "color": hex_to_rgba("#1BC6E0", 150)}, {"inset": 300, "radius": 100, "stroke": 14, "color": hex_to_rgba("#31E1F5", 190)}, {"inset": 380, "radius": 74, "stroke": 12, "color": hex_to_rgba("#7EF0FF", 230)}, ] for r in rings: box = (r["inset"], r["inset"], SIZE - r["inset"], SIZE - r["inset"]) outer = rounded_rect_mask((SIZE, SIZE), box, r["radius"]) inset_box = (box[0] + r["stroke"], box[1] + r["stroke"], box[2] - r["stroke"], box[3] - r["stroke"]) inner = rounded_rect_mask((SIZE, SIZE), inset_box, max(r["radius"] - r["stroke"], 10)) ring = ImageChops.subtract(outer, inner) # Subtle blur — outer rings softer than inner blur_amt = 0.8 + (rings.index(r)) * 0.3 ring = ring.filter(ImageFilter.GaussianBlur(blur_amt)) layer = Image.new("RGBA", (SIZE, SIZE), r["color"][:3] + (0,)) layer.putalpha(ring.point(lambda v, a=r["color"][3]: int(v * a / 255))) canvas = Image.alpha_composite(canvas, layer) # Central glowing dot dot_r = 28 dot_box = (SIZE / 2 - dot_r, SIZE / 2 - dot_r, SIZE / 2 + dot_r, SIZE / 2 + dot_r) dot_mask = Image.new("L", (SIZE, SIZE), 0) ImageDraw.Draw(dot_mask).ellipse(dot_box, fill=255) dot_mask = dot_mask.filter(ImageFilter.GaussianBlur(2)) dot_layer = Image.new("RGBA", (SIZE, SIZE), hex_to_rgba("#E8FDFF")) dot_layer.putalpha(dot_mask) canvas = Image.alpha_composite(canvas, dot_layer) # Dot bloom bloom_mask = Image.new("L", (SIZE, SIZE), 0) ImageDraw.Draw(bloom_mask).ellipse((SIZE / 2 - 90, SIZE / 2 - 90, SIZE / 2 + 90, SIZE / 2 + 90), fill=255) bloom_mask = bloom_mask.filter(ImageFilter.GaussianBlur(36)) bloom_layer = Image.new("RGBA", (SIZE, SIZE), hex_to_rgba("#7EF0FF")) bloom_layer.putalpha(bloom_mask.point(lambda v: int(v * 0.45))) canvas = Image.alpha_composite(canvas, bloom_layer) canvas.save(os.path.join(OUT, "icon-3-waves.png")) # ---------------------------------------------------------------- icon 4 — Monogram S def icon_monogram(): canvas = linear_gradient(SIZE, [ (0.0, hex_to_rgba("#0A1430")), (0.5, hex_to_rgba("#1B1450")), (1.0, hex_to_rgba("#04061A")), ], angle_deg=125) # Chromatic bloom — blue, magenta, amber — behind the mark bloom1 = radial_gradient(SIZE, (SIZE * 0.28, SIZE * 0.30), [ (0.0, hex_to_rgba("#4AA8FF", 190)), (1.0, hex_to_rgba("#080822", 0)), ], radius=SIZE * 0.55) bloom2 = radial_gradient(SIZE, (SIZE * 0.78, SIZE * 0.78), [ (0.0, hex_to_rgba("#E845A8", 170)), (1.0, hex_to_rgba("#080822", 0)), ], radius=SIZE * 0.55) canvas = Image.alpha_composite(canvas, bloom1) canvas = Image.alpha_composite(canvas, bloom2) # Two interlocking chevrons that read as an "S" # Upper chevron (like a `>` opening right) at top # Lower chevron (like a `<` opening left) at bottom — stacked with overlap stroke = 110 inset_x = 200 center_y = SIZE / 2 top_y = 260 bot_y = SIZE - 260 def chevron_points(start_x, end_x, tip_x, top, bottom, thickness): """A V-shaped chevron band from (start_x, top) → (tip_x, mid) → (end_x, top) drawn as a parallelogram-like thick stroke.""" mid = (top + bottom) / 2 return [ (start_x, top), (tip_x, bottom), (end_x, top), (end_x, top + thickness), (tip_x, bottom + thickness), # overflowed — we'll draw differently (start_x, top + thickness), ] # Rather than manual polygons, paint two thick rounded "V" strokes. upper = Image.new("L", (SIZE, SIZE), 0) lower = Image.new("L", (SIZE, SIZE), 0) ud = ImageDraw.Draw(upper) ld = ImageDraw.Draw(lower) # Upper chevron: start top-left → tip right-middle → continues back to top-right? Actually we want ">" # We'll draw the S as two mirrored arcs — easier: use thick polylines along a cubic path. # Approximate with chained line segments + round caps. def thick_polyline(draw, pts, width): for i in range(len(pts) - 1): draw.line([pts[i], pts[i + 1]], fill=255, width=width) for p in pts: r = width / 2 draw.ellipse((p[0] - r, p[1] - r, p[0] + r, p[1] + r), fill=255) # An abstract "S": top arc (left-top → right-center), bottom arc (right-center → left-bottom) top_arc = [ (inset_x + 20, top_y), (SIZE - inset_x - 80, top_y + 80), (SIZE - inset_x, center_y - 40), (SIZE - inset_x - 40, center_y), ] bot_arc = [ (SIZE - inset_x - 40, center_y), (inset_x + 40, center_y + 40), (inset_x, bot_y - 80), (inset_x + 20, bot_y), ] # Smooth via many interpolated bezier-like points def smooth(points, samples=80): # Quadratic bezier through 4 points: treat as cubic bezier control poly (p0, p1, p2, p3) = points out = [] for i in range(samples + 1): t = i / samples # cubic bezier x = (1 - t) ** 3 * p0[0] + 3 * (1 - t) ** 2 * t * p1[0] + 3 * (1 - t) * t ** 2 * p2[0] + t ** 3 * p3[0] y = (1 - t) ** 3 * p0[1] + 3 * (1 - t) ** 2 * t * p1[1] + 3 * (1 - t) * t ** 2 * p2[1] + t ** 3 * p3[1] out.append((x, y)) return out top_curve = smooth(top_arc, samples=120) bot_curve = smooth(bot_arc, samples=120) thick_polyline(ud, top_curve, stroke) thick_polyline(ld, bot_curve, stroke) combined = ImageChops.lighter(upper, lower) # Chromatic fill: horizontal-diagonal gradient inside the S s_grad = linear_gradient(SIZE, [ (0.0, hex_to_rgba("#7FB2FF")), (0.5, hex_to_rgba("#C97BFF")), (1.0, hex_to_rgba("#FF8BB8")), ], angle_deg=120) s_layer = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0)) s_layer.paste(s_grad, (0, 0), combined) # Highlight rim on top edge of the S for glass feel rim = combined.filter(ImageFilter.GaussianBlur(1)) rim_inner = combined.filter(ImageFilter.GaussianBlur(3)) edge = ImageChops.subtract(rim, rim_inner) edge_layer = Image.new("RGBA", (SIZE, SIZE), hex_to_rgba("#FFFFFF", 130)) edge_layer.putalpha(edge) # Drop shadow under the S shadow = combined.filter(ImageFilter.GaussianBlur(26)) sh_layer = Image.new("RGBA", (SIZE, SIZE), (0, 0, 20, 0)) sh_layer.putalpha(shadow.point(lambda v: int(v * 0.55))) sh_offset = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0)) sh_offset.paste(sh_layer, (0, 16), sh_layer) canvas = Image.alpha_composite(canvas, sh_offset) canvas = Image.alpha_composite(canvas, s_layer) canvas = Image.alpha_composite(canvas, edge_layer) canvas.save(os.path.join(OUT, "icon-4-monogram.png")) # ---------------------------------------------------------------- icon 5 — Split Screen def icon_split(): canvas = linear_gradient(SIZE, [ (0.0, hex_to_rgba("#1A1245")), (0.5, hex_to_rgba("#2F1D78")), (1.0, hex_to_rgba("#0D0824")), ], angle_deg=120) # Warm highlight in upper-left warm = radial_gradient(SIZE, (SIZE * 0.28, SIZE * 0.22), [ (0.0, hex_to_rgba("#F7B57A", 140)), (1.0, hex_to_rgba("#1A1245", 0)), ], radius=SIZE * 0.55) canvas = Image.alpha_composite(canvas, warm) # Cool counterpoint lower-right cool = radial_gradient(SIZE, (SIZE * 0.82, SIZE * 0.82), [ (0.0, hex_to_rgba("#4E6CFF", 180)), (1.0, hex_to_rgba("#100828", 0)), ], radius=SIZE * 0.6) canvas = Image.alpha_composite(canvas, cool) # Two tilted rectangles — "back" one tilted left, "front" one tilted right. def rotate_polygon(pts, angle_deg, cx, cy): a = math.radians(angle_deg) cos_a, sin_a = math.cos(a), math.sin(a) out = [] for (x, y) in pts: dx, dy = x - cx, y - cy nx = dx * cos_a - dy * sin_a + cx ny = dx * sin_a + dy * cos_a + cy out.append((nx, ny)) return out def draw_screen(rect_box, tilt_deg, body_grad_stops, rim_alpha=140): # rect_box: (x0,y0,x1,y1) x0, y0, x1, y1 = rect_box cx, cy = (x0 + x1) / 2, (y0 + y1) / 2 # Approximate rounded-rect by drawing on a tilted transparent layer layer = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0)) mask = Image.new("L", (SIZE, SIZE), 0) radius = 80 # Draw axis-aligned rounded rect on a working canvas sized to the screen's bounding box, then rotate. local_w = int(x1 - x0) + 200 local_h = int(y1 - y0) + 200 local_mask = Image.new("L", (local_w, local_h), 0) lx0 = 100 ly0 = 100 lx1 = local_w - 100 ly1 = local_h - 100 ImageDraw.Draw(local_mask).rounded_rectangle((lx0, ly0, lx1, ly1), radius=radius, fill=255) local_mask = local_mask.rotate(tilt_deg, resample=Image.BICUBIC, expand=False) # Place local mask into full-size mask at correct position place_x = int(cx - local_w / 2) place_y = int(cy - local_h / 2) mask.paste(local_mask, (place_x, place_y)) # Body gradient body = linear_gradient(SIZE, body_grad_stops, angle_deg=110) # Shadow before composite shadow_mask = mask.filter(ImageFilter.GaussianBlur(30)) shadow = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0)) shadow.putalpha(shadow_mask.point(lambda v: int(v * 0.55))) sh_offset = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0)) sh_offset.paste(shadow, (14, 30), shadow) body_layer = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0)) body_layer.paste(body, (0, 0), mask) # Rim / edge highlight rim_outer = mask rim_inner = mask.filter(ImageFilter.GaussianBlur(2)).point(lambda v: 255 if v > 180 else 0) rim_inner_eroded = rim_inner.filter(ImageFilter.MinFilter(9)) rim = ImageChops.subtract(rim_outer, rim_inner_eroded) rim_layer = Image.new("RGBA", (SIZE, SIZE), hex_to_rgba("#FFFFFF", rim_alpha)) rim_layer.putalpha(rim) return sh_offset, body_layer, rim_layer, mask # Back screen: bigger, tilted left back_box = (200, 250, SIZE - 240, SIZE - 200) back = draw_screen( back_box, tilt_deg=-7, body_grad_stops=[ (0.0, hex_to_rgba("#5A4FD9")), (1.0, hex_to_rgba("#1E154F")), ], rim_alpha=90 ) # Front screen: smaller, tilted right, shifted down-right front_box = (280, 330, SIZE - 160, SIZE - 160) front = draw_screen( front_box, tilt_deg=6, body_grad_stops=[ (0.0, hex_to_rgba("#F5CE9A")), (0.5, hex_to_rgba("#E086A3")), (1.0, hex_to_rgba("#6B488C")), ], rim_alpha=160 ) # Composite: back shadow → back body → back rim → front shadow → front body → front rim for bundle in (back, front): sh, body, rim, _ = bundle canvas = Image.alpha_composite(canvas, sh) canvas = Image.alpha_composite(canvas, body) canvas = Image.alpha_composite(canvas, rim) # Specular highlight on front screen _, _, _, front_mask = front sheen = specular_highlight(front_mask, intensity=120) canvas = Image.alpha_composite(canvas, sheen) canvas.save(os.path.join(OUT, "icon-5-split.png")) # ---------------------------------------------------------------- contact sheet def contact_sheet(): paths = [ "icon-1-portal.png", "icon-2-cursor.png", "icon-3-waves.png", "icon-4-monogram.png", "icon-5-split.png", ] cell = 360 gap = 30 cols = 5 rows = 1 label_h = 70 W = cols * cell + (cols + 1) * gap H = rows * cell + (rows + 1) * gap + label_h sheet = Image.new("RGB", (W, H), (24, 24, 30)) d = ImageDraw.Draw(sheet) for i, p in enumerate(paths): img = Image.open(os.path.join(OUT, p)).convert("RGBA") # Apply iOS squircle preview mask mask = Image.new("L", img.size, 0) ImageDraw.Draw(mask).rounded_rectangle((0, 0, img.size[0], img.size[1]), radius=int(img.size[0] * 0.22), fill=255) img.putalpha(mask) img = img.resize((cell, cell), Image.LANCZOS) x = gap + i * (cell + gap) y = gap sheet.paste(img, (x, y), img) label = ["1 Portal", "2 Cursor", "3 Waves", "4 Monogram", "5 Split"][i] d.text((x + cell / 2 - 30, y + cell + 20), label, fill=(240, 240, 250)) sheet.save(os.path.join(OUT, "contact-sheet.png")) if __name__ == "__main__": print("Generating icons…") icon_portal() print(" ✓ portal") icon_cursor() print(" ✓ cursor") icon_waves() print(" ✓ waves") icon_monogram() print(" ✓ monogram") icon_split() print(" ✓ split") contact_sheet() print(" ✓ contact sheet") print("Done.")