SyncWave/tools/make_icons.py
TySmi 8919596ac2 New app icon and launch screen from "SyncWave Sv2.png"
- Adaptive icon: the glossy purple S logo, whole image at 94% so the S sits
  inside the 33dp safe zone (it reached 34.6dp untouched), edge blended into
  the new #0E0B14 background that matches the image's border.
- Themed (monochrome) icon: the S as a solid silhouette.
- Launch screen: only the S on #0E0B14, so the round splash crop shows no
  edge of the textured square.
- tools/make_icons.py regenerates all of these from the source PNG.

Verified on the emulator: icon in the app drawer and the launch screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:58:03 -04:00

87 lines
4.2 KiB
Python

"""Builds the SyncWave launcher icon, themed icon and splash image from the source logo.
Usage: py tools/make_icons.py "SyncWave Sv2.png" app/src/main/res <preview dir>
Writes mipmap-*/ic_launcher_foreground.png, mipmap-*/ic_launcher_monochrome.png,
drawable-*/splash_icon.png, and icon_preview.png in the preview dir.
"""
import sys, os
import numpy as np
from PIL import Image, ImageDraw, ImageFilter
src_path, res_dir, preview_dir = sys.argv[1], sys.argv[2], sys.argv[3]
BG = (14, 11, 20) # #0E0B14, average of the logo's outer border
SCALE = 0.94 # S reaches 34.6dp of 54 -> 32.5dp, inside the 33dp safe zone
N = 2048
src = Image.open(src_path).convert("RGB")
assert src.size == (N, N), src.size
a = np.asarray(src).astype(np.float32)
# --- Mask of the S: colourful/bright pixels; then everything not connected to the border
# background counts as S (fills darker spots inside the glass).
mx, mn = a.max(2), a.min(2)
core = ((mx - mn) > 60) & (mx > 70)
m = Image.fromarray(np.where(core, 255, 0).astype(np.uint8))
m = m.filter(ImageFilter.MaxFilter(9)).filter(ImageFilter.MinFilter(9)) # close small gaps
ImageDraw.floodfill(m, (0, 0), 128) # outside -> 128
s_mask = Image.fromarray(np.where(np.asarray(m) == 128, 0, 255).astype(np.uint8))
s_mask = s_mask.filter(ImageFilter.GaussianBlur(2.5)) # soft anti-aliased edge
# --- 1. Foreground: whole logo at 94% on BG, with its outer edge blended into BG.
def scaled_on_bg(img, alpha=None):
size = round(N * SCALE)
small = img.resize((size, size), Image.LANCZOS)
canvas = Image.new("RGB", (N, N), BG)
off = (N - size) // 2
if alpha is None:
# Feather the outer 3% of the image into the background.
ramp = np.ones((size, size), np.float32)
f = int(size * 0.03)
lin = np.linspace(0, 1, f, dtype=np.float32)
for i in range(f):
ramp[i, :] = np.minimum(ramp[i, :], lin[i]); ramp[-1 - i, :] = np.minimum(ramp[-1 - i, :], lin[i])
ramp[:, i] = np.minimum(ramp[:, i], lin[i]); ramp[:, -1 - i] = np.minimum(ramp[:, -1 - i], lin[i])
alpha = Image.fromarray((ramp * 255).astype(np.uint8))
else:
alpha = alpha.resize((size, size), Image.LANCZOS)
canvas.paste(small, (off, off), alpha)
return canvas
foreground = scaled_on_bg(src)
# --- 2. Monochrome: white S silhouette (Android tints it).
size = round(N * SCALE); off = (N - size) // 2
mono = Image.new("RGBA", (N, N), (255, 255, 255, 0))
mono_alpha = Image.new("L", (N, N), 0)
mono_alpha.paste(s_mask.resize((size, size), Image.LANCZOS), (off, off))
mono.putalpha(mono_alpha)
# --- 3. Splash: only the glossy S (no textured square) on BG.
splash = scaled_on_bg(src, alpha=s_mask)
densities = {"mdpi": 108, "hdpi": 162, "xhdpi": 216, "xxhdpi": 324, "xxxhdpi": 432}
for d, px in densities.items():
foreground.resize((px, px), Image.LANCZOS).save(os.path.join(res_dir, f"mipmap-{d}", "ic_launcher_foreground.png"), optimize=True)
mono.resize((px, px), Image.LANCZOS).save(os.path.join(res_dir, f"mipmap-{d}", "ic_launcher_monochrome.png"), optimize=True)
splash.resize((px, px), Image.LANCZOS).save(os.path.join(res_dir, f"drawable-{d}", "splash_icon.png"), optimize=True)
# --- Previews: icon in a circle mask (the 72dp visible area), themed icon, splash in its circle.
def circle_preview(img, visible_dp, bg=(255, 255, 255)):
px = 432
im = img.convert("RGBA").resize((px, px), Image.LANCZOS)
r = px * visible_dp / 108 / 2
out = Image.new("RGBA", (px, px), bg + (255,))
circ = Image.new("L", (px * 4, px * 4), 0)
ImageDraw.Draw(circ).ellipse([(px * 2 - r * 4), (px * 2 - r * 4), (px * 2 + r * 4), (px * 2 + r * 4)], fill=255)
out.paste(im, (0, 0), circ.resize((px, px), Image.LANCZOS))
return out
themed_bg = Image.new("RGB", (N, N), (40, 34, 60))
themed_fg = Image.new("RGB", (N, N), (205, 190, 255))
themed = Image.composite(themed_fg, themed_bg, mono_alpha)
row = [circle_preview(foreground, 72), circle_preview(foreground, 66), circle_preview(themed, 72), circle_preview(splash, 108 * 192 / 288, bg=BG)]
sheet = Image.new("RGB", (432 * 4 + 60, 472), (255, 255, 255))
for i, p in enumerate(row):
sheet.paste(p, (i * 452, 20), p)
sheet.save(os.path.join(preview_dir, "icon_preview.png"))
print("done")