Generated via icons/generate.py. See PHILOSOPHY.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
629 lines
23 KiB
Python
629 lines
23 KiB
Python
"""
|
|
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.")
|