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:
21
icons/PHILOSOPHY.md
Normal file
21
icons/PHILOSOPHY.md
Normal 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
BIN
icons/contact-sheet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 326 KiB |
628
icons/generate.py
Normal file
628
icons/generate.py
Normal 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
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
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
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
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
BIN
icons/icon-5-split.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 249 KiB |
Reference in New Issue
Block a user