Add 5 icon concepts for review

Generated via icons/generate.py. See PHILOSOPHY.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-17 14:33:13 -05:00
parent c1bed4f53b
commit 0f415ab498
8 changed files with 649 additions and 0 deletions

21
icons/PHILOSOPHY.md Normal file
View File

@@ -0,0 +1,21 @@
# Luminous Distance
A visual philosophy for objects that exist between presence and absence — interfaces that reach across space without disturbing the stillness at either end.
## Manifesto
Luminous Distance treats remote control not as mastery but as quiet correspondence. A signal leaves one room and arrives in another. Between those rooms is glass: cold, smooth, and aware of itself. The icon is a fragment of that glass — thick enough to hold a scene, thin enough to hint at what's behind it. Depth without decoration.
Color operates as atmosphere, not ornament. Palettes move in slow gradations — indigo deepening into ultramarine, cyan cooling into navy, warm magentas burning at the edges of cool blues — the way a display looks when you stand in a dark room and let your eyes adjust. Saturation is restrained where it matters and allowed to swell only at the focal point, producing a single core of light the eye can't escape. Nothing shouts. Everything glows.
Form is geometric but never brittle. Rounded rectangles hold the composition at every scale — a formal homage to the devices we touch all day. Within those frames, geometry is subdivided with the patience of draftsmanship: centimeter margins, optical centering, subtle tilts that suggest perspective without committing to 3D. Silhouettes are strong enough to read at a thumbnail's distance and rich enough to reward close inspection. Every curve is a deliberate choice, the product of painstaking correction and re-correction.
Material is drawn, not faked. Liquid Glass is not a filter; it is the literal subject — refraction, specular highlights, the faint chromatic bloom at the edge where a surface meets light. Where texture appears, it is the texture of something real: the phosphor glow of an old CRT, the subpixel grid of an LCD, the static shimmer of a cursor caught mid-blink. Each surface is meticulously crafted so that the eye can't decide whether it is looking at an icon or at an artifact photographed through a telephoto lens.
Composition is resolved through negative space. The frame is filled but never crowded; the focal element is given its own gravitational field. Axial and radial symmetries dominate because the subject is symmetric by nature — signal leaving and returning along the same line, one screen mirroring another. When symmetry is broken, it is broken precisely, by degrees measured in fractions of a point. The work should feel inevitable, as if the designer had no other choice. This is the hallmark of master-level execution — countless refinements hidden behind apparent ease.
Typography is absent. Labels are for product pages; the icon itself speaks only through form, color, and light. The mark must survive the brutal reduction of a Home Screen grid and still carry an idea. What remains after all explanation is stripped away is the whole point.
## The subtle reference
The Remote Framebuffer Protocol — the 1998 RFC on which VNC rests — describes a remote screen as a pixel rectangle updated over time. Every icon here is, at heart, a drawing of that rectangle. A rectangle seen through glass, a rectangle pointed at by a cursor, a rectangle broadcasting its state, a rectangle sliced by its own monogram, two rectangles tilted in conversation. The protocol is never named. But anyone who has spent long nights watching those updates arrive will feel its ghost.

BIN
icons/contact-sheet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

628
icons/generate.py Normal file
View File

@@ -0,0 +1,628 @@
"""
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.")

BIN
icons/icon-1-portal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

BIN
icons/icon-2-cursor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

BIN
icons/icon-3-waves.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
icons/icon-4-monogram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

BIN
icons/icon-5-split.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB