- 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>
87 lines
4.2 KiB
Python
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")
|