import os, math
from PIL import Image, ImageDraw
ROOT = "/home/claude/icon-pack"
BG = "#4A1B0C"
SAND = "#FAC775"
ECHO = "#EF9F27"
SPARK = "#FAECE7"
CREST = [
("M", (0.0, 77.2)),
("L", (18.0, 77.2)),
("C", (25.2, 77.2, 28.4, 63.6, 36.0, 63.6)),
("C", (43.6, 63.6, 46.8, 80.4, 54.0, 80.4)),
("C", (61.2, 80.4, 64.8, 62.0, 72.0, 62.0)),
("C", (79.2, 62.0, 84.0, 75.6, 90.0, 75.6)),
("L", (108.0, 75.6)),
]
SPARKLE = [
("M", (54.0, 23.6)),
("Q", (57.6, 33.2, 67.6, 37.2)),
("Q", (57.6, 41.2, 54.0, 50.8)),
("Q", (50.4, 41.2, 40.4, 37.2)),
("Q", (50.4, 33.2, 54.0, 23.6)),
]
ECHO_NEAR_DY, ECHO_NEAR_W, ECHO_NEAR_A = 9.6, 2.8, 0.70
ECHO_FAR_DY, ECHO_FAR_W, ECHO_FAR_A = 18.4, 1.8, 0.40
def shift(segs, dy):
out = []
for cmd, v in segs:
out.append((cmd, tuple(v[i] - (dy if i % 2 else 0) for i in range(len(v)))))
return out
def to_path_data(segs, close=False, tail=None):
parts = []
for cmd, v in segs:
nums = ",".join(f"{n:g}" for n in v)
if cmd == "M":
parts.append(f"M{nums}")
elif cmd == "L":
parts.append(f"L{nums}")
elif cmd == "C":
parts.append(f"C{nums}")
elif cmd == "Q":
parts.append(f"Q{nums}")
if tail:
parts.append(tail)
if close:
parts.append("Z")
return " ".join(parts)
def sample(segs, steps=48):
pts, cur = [], None
for cmd, v in segs:
if cmd == "M":
cur = (v[0], v[1]); pts.append(cur)
elif cmd == "L":
cur = (v[0], v[1]); pts.append(cur)
elif cmd == "C":
p0 = cur; c1 = (v[0], v[1]); c2 = (v[2], v[3]); p1 = (v[4], v[5])
for i in range(1, steps + 1):
t = i / steps; u = 1 - t
x = u**3*p0[0] + 3*u*u*t*c1[0] + 3*u*t*t*c2[0] + t**3*p1[0]
y = u**3*p0[1] + 3*u*u*t*c1[1] + 3*u*t*t*c2[1] + t**3*p1[1]
pts.append((x, y))
cur = p1
elif cmd == "Q":
p0 = cur; c = (v[0], v[1]); p1 = (v[2], v[3])
for i in range(1, steps + 1):
t = i / steps; u = 1 - t
x = u*u*p0[0] + 2*u*t*c[0] + t*t*p1[0]
y = u*u*p0[1] + 2*u*t*c[1] + t*t*p1[1]
pts.append((x, y))
cur = p1
return pts
VECTOR_HEAD = ('\n')
def body(sand_color, echo_color, spark_color):
far = to_path_data(shift(CREST, ECHO_FAR_DY))
near = to_path_data(shift(CREST, ECHO_NEAR_DY))
dune = to_path_data(CREST, close=True, tail="L108,108 L0,108")
spark = to_path_data(SPARKLE, close=True)
return (
f' \n'
f' \n'
f' \n'
f' \n'
)
def write(path, text):
full = os.path.join(ROOT, path)
os.makedirs(os.path.dirname(full), exist_ok=True)
with open(full, "w", encoding="utf-8") as f:
f.write(text)
write("res/drawable/ic_launcher_background.xml",
VECTOR_HEAD + f' \n\n')
write("res/drawable/ic_launcher_foreground.xml",
VECTOR_HEAD + body(SAND, ECHO, SPARK) + "\n")
write("res/drawable/ic_launcher_monochrome.xml",
VECTOR_HEAD + body("#FFFFFF", "#FFFFFF", "#FFFFFF") + "\n")
adaptive = ('\n'
'\n'
' \n'
' \n'
' \n'
'\n')
write("res/mipmap-anydpi-v26/ic_launcher.xml", adaptive)
write("res/mipmap-anydpi-v26/ic_launcher_round.xml", adaptive)
write("res/values/ic_launcher_colors.xml",
'\n\n'
f' {BG}\n\n')
SS = 4 # supersampling
def render(canvas_px, bleed=True):
"""Render the 108x108 artwork at canvas_px, supersampled."""
S = canvas_px * SS
k = S / 108.0
img = Image.new("RGBA", (S, S), BG)
d = ImageDraw.Draw(img, "RGBA")
def px(pts):
return [(x * k, y * k) for x, y in pts]
def rgba(hexs, alpha):
h = hexs.lstrip("#")
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4)) + (int(alpha * 255),)
for dy, w, a in ((ECHO_FAR_DY, ECHO_FAR_W, ECHO_FAR_A),
(ECHO_NEAR_DY, ECHO_NEAR_W, ECHO_NEAR_A)):
layer = Image.new("RGBA", (S, S), (0, 0, 0, 0))
ld = ImageDraw.Draw(layer, "RGBA")
ld.line(px(sample(shift(CREST, dy))), fill=rgba(ECHO, a),
width=max(1, int(round(w * k))), joint="curve")
img.alpha_composite(layer)
dune = sample(CREST) + [(108.0, 108.0), (0.0, 108.0)]
d.polygon(px(dune), fill=rgba(SAND, 1.0))
d.polygon(px(sample(SPARKLE)), fill=rgba(SPARK, 1.0))
return img.resize((canvas_px, canvas_px), Image.LANCZOS)
def visible_crop(canvas_px):
"""Crop the central 72/108 region — the guaranteed-visible tile."""
img = render(canvas_px)
m = int(round(canvas_px * 18 / 108))
return img.crop((m, m, canvas_px - m, canvas_px - m))
def rounded(img, ratio=0.2333):
s = img.size[0]
up = img.resize((s * SS, s * SS), Image.LANCZOS)
mask = Image.new("L", (s * SS, s * SS), 0)
ImageDraw.Draw(mask).rounded_rectangle([0, 0, s * SS - 1, s * SS - 1],
radius=int(s * SS * ratio), fill=255)
up.putalpha(mask)
return up.resize((s, s), Image.LANCZOS)
def circular(img):
s = img.size[0]
up = img.resize((s * SS, s * SS), Image.LANCZOS)
mask = Image.new("L", (s * SS, s * SS), 0)
ImageDraw.Draw(mask).ellipse([0, 0, s * SS - 1, s * SS - 1], fill=255)
up.putalpha(mask)
return up.resize((s, s), Image.LANCZOS)
def tile(size):
"""Render the visible tile directly at `size` px, oversampled for quality."""
return visible_crop(int(round(size * 108 / 72)))
DPI = {"mdpi": 48, "hdpi": 72, "xhdpi": 96, "xxhdpi": 144, "xxxhdpi": 192}
for name, s in DPI.items():
base = tile(s)
p = os.path.join(ROOT, f"res/mipmap-{name}")
os.makedirs(p, exist_ok=True)
rounded(base).save(os.path.join(p, "ic_launcher.png"))
circular(base).save(os.path.join(p, "ic_launcher_round.png"))
os.makedirs(os.path.join(ROOT, "play-store"), exist_ok=True)
tile(512).convert("RGB").save(os.path.join(ROOT, "play-store/ic_launcher-playstore.png"))
# preview sheet
sizes = [192, 96, 72, 48]
W, H = 560, 260
sheet = Image.new("RGBA", (W, H), (255, 255, 255, 255))
x = 24
for s in sizes:
t = rounded(tile(s))
sheet.alpha_composite(t, (x, 24 + (192 - s) // 2))
x += s + 24
sheet.convert("RGB").save(os.path.join(ROOT, "preview.png"))
print("done")