Files
gen1recomp/tools/generate_android_icons.py
T

197 lines
7.5 KiB
Python

#!/usr/bin/env python3
"""
Generates Android Adaptive Icon (based on cleaned love.png Pokéball + Gen1Recomp emblem)
and 3D Cartridge Shortcut assets for all density buckets (mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi).
"""
import os
from collections import deque
import numpy as np
from PIL import Image, ImageDraw, ImageFilter
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
RES_DIR = os.path.join(ROOT, "mobile", "android", "app", "src", "main", "res")
DENSITIES = {
"mdpi": {"shortcut": 48, "adaptive": 108},
"hdpi": {"shortcut": 72, "adaptive": 162},
"xhdpi": {"shortcut": 96, "adaptive": 216},
"xxhdpi": {"shortcut": 144, "adaptive": 324},
"xxxhdpi": {"shortcut": 192, "adaptive": 432},
}
SHELL_COLORS = {
"red": {"main": (230, 45, 55), "dark": (175, 25, 35), "light": (255, 90, 100)},
"blue": {"main": (35, 125, 235), "dark": (20, 85, 175), "light": (80, 165, 255)},
"yellow": {"main": (255, 205, 10), "dark": (210, 160, 0), "light": (255, 230, 80)},
"gold": {"main": (225, 170, 40), "dark": (170, 120, 20), "light": (245, 200, 80)},
}
def extract_cleaned_love_emblem():
"""Extracts the Pokéball/Gen1Recomp emblem from love.png with transparent bg and deepened blacks."""
src_path = os.path.join(RES_DIR, "drawable-xxxhdpi", "love.png")
if not os.path.exists(src_path):
src_path = os.path.join(RES_DIR, "drawable-xxhdpi", "love.png")
src = Image.open(src_path).convert("RGBA")
arr = np.array(src, dtype=np.uint8)
h, w = arr.shape[:2]
visited = np.zeros((h, w), dtype=bool)
bg_mask = np.zeros((h, w), dtype=bool)
queue = deque()
for x in range(w):
queue.append((0, x)); queue.append((h-1, x))
for y in range(h):
queue.append((y, 0)); queue.append((y, w-1))
bg_ref = np.array([255, 237, 254], dtype=float)
while queue:
y, x = queue.popleft()
if visited[y, x]: continue
visited[y, x] = True
color = arr[y, x, :3].astype(float)
if np.max(np.abs(color - bg_ref)) < 28:
bg_mask[y, x] = True
for dy, dx in [(-1,0), (1,0), (0,-1), (0,1)]:
ny, nx = y + dy, x + dx
if 0 <= ny < h and 0 <= nx < w and not visited[ny, nx]:
queue.append((ny, nx))
out_arr = arr.copy().astype(float)
out_arr[bg_mask, 3] = 0
# Deepen the soft blacks/outlines for crispness:
fg_mask = ~bg_mask
rgb = out_arr[fg_mask, :3]
lum = 0.299 * rgb[:, 0] + 0.587 * rgb[:, 1] + 0.114 * rgb[:, 2]
for i in range(len(rgb)):
l = lum[i]
if l < 110:
factor = (l / 110.0) ** 1.8
rgb[i, 0] = max(0, rgb[i, 0] * factor * 0.7)
rgb[i, 1] = max(0, rgb[i, 1] * factor * 0.7)
rgb[i, 2] = max(0, rgb[i, 2] * factor * 0.8)
out_arr[fg_mask, :3] = np.clip(rgb, 0, 255)
return Image.fromarray(out_arr.astype(np.uint8))
CLEANED_EMBLEM = extract_cleaned_love_emblem()
def create_adaptive_foreground(size):
emblem = CLEANED_EMBLEM.copy()
target_size = int(size * 0.78)
emblem.thumbnail((target_size, target_size), Image.Resampling.LANCZOS)
canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
x = (size - emblem.width) // 2
y = (size - emblem.height) // 2
canvas.paste(emblem, (x, y), emblem)
return canvas
def create_adaptive_monochrome(size):
fg = create_adaptive_foreground(size)
arr = np.array(fg, dtype=float)
lum = 0.299 * arr[:, :, 0] + 0.587 * arr[:, :, 1] + 0.114 * arr[:, :, 2]
alpha = arr[:, :, 3]
mono_alpha = np.zeros_like(alpha)
valid = alpha > 20
mono_alpha[valid & (lum > 70)] = 255
mono_alpha[valid & (lum <= 70)] = 0
mono_img = np.zeros((size, size, 4), dtype=np.uint8)
mono_img[:, :, 0] = 255
mono_img[:, :, 1] = 255
mono_img[:, :, 2] = 255
mono_img[:, :, 3] = mono_alpha.astype(np.uint8)
return Image.fromarray(mono_img)
def render_3d_cartridge(version, size):
colors = SHELL_COLORS[version]
S = size * 4
# Transparent canvas so the cartridge sits directly on launcher's white circle plate
canvas = Image.new("RGBA", (S, S), (0, 0, 0, 0))
draw = ImageDraw.Draw(canvas)
# Tight padding to maximize cartridge size
pad_x = int(S * 0.05)
pad_y = int(S * 0.03)
cw = S - pad_x * 2
ch = S - pad_y * 2
# Soft drop shadow
shadow = Image.new("RGBA", (S, S), (0, 0, 0, 0))
sdraw = ImageDraw.Draw(shadow)
sdraw.rounded_rectangle([pad_x + 8, pad_y + 14, pad_x + cw + 8, pad_y + ch + 14],
radius=int(S * 0.05), fill=(0, 0, 0, 90))
shadow = shadow.filter(ImageFilter.GaussianBlur(int(S * 0.03)))
canvas.paste(shadow, (0, 0), shadow)
radius = int(S * 0.045)
depth = int(S * 0.03)
draw.rounded_rectangle([pad_x, pad_y + depth, pad_x + cw, pad_y + ch + depth],
radius=radius, fill=colors["dark"])
draw.rounded_rectangle([pad_x, pad_y, pad_x + cw, pad_y + ch],
radius=radius, fill=colors["main"])
notch_w = int(cw * 0.65)
notch_h = int(ch * 0.07)
notch_x = pad_x + (cw - notch_w) // 2
notch_y = pad_y + int(ch * 0.035)
draw.rounded_rectangle([notch_x, notch_y, notch_x + notch_w, notch_y + notch_h],
radius=int(notch_h * 0.4), fill=colors["dark"])
draw.rounded_rectangle([notch_x, notch_y - 2, notch_x + notch_w, notch_y + notch_h - 2],
radius=int(notch_h * 0.4), fill=colors["light"])
label_margin_x = int(cw * 0.09)
label_top_y = pad_y + int(ch * 0.20)
label_w = cw - label_margin_x * 2
label_h = int(ch * 0.70)
label_x = pad_x + label_margin_x
draw.rounded_rectangle([label_x - 3, label_top_y - 3, label_x + label_w + 3, label_top_y + label_h + 3],
radius=int(radius * 0.7), fill=colors["dark"])
label_path = os.path.join(ROOT, "assets", "labels", f"{version}.png")
if os.path.exists(label_path):
label_img = Image.open(label_path).convert("RGBA")
label_img = label_img.resize((label_w, label_h), Image.Resampling.LANCZOS)
mask = Image.new("L", (label_w, label_h), 0)
mdraw = ImageDraw.Draw(mask)
mdraw.rounded_rectangle([0, 0, label_w, label_h], radius=int(radius * 0.5), fill=255)
canvas.paste(label_img, (label_x, label_top_y), mask)
else:
draw.rounded_rectangle([label_x, label_top_y, label_x + label_w, label_top_y + label_h],
radius=int(radius * 0.5), fill=(240, 240, 240, 255))
draw.rounded_rectangle([pad_x, pad_y, pad_x + cw, pad_y + ch],
radius=radius, outline=colors["light"], width=max(2, int(S * 0.008)))
return canvas.resize((size, size), Image.Resampling.LANCZOS)
def main():
os.makedirs(os.path.join(RES_DIR, "values"), exist_ok=True)
os.makedirs(os.path.join(RES_DIR, "mipmap-anydpi-v26"), exist_ok=True)
for density, sizes in DENSITIES.items():
drawable_dir = os.path.join(RES_DIR, f"drawable-{density}")
os.makedirs(drawable_dir, exist_ok=True)
fg = create_adaptive_foreground(sizes["adaptive"])
fg.save(os.path.join(drawable_dir, "ic_launcher_foreground.png"), "PNG")
for ver in ("red", "blue", "yellow", "gold"):
cart = render_3d_cartridge(ver, sizes["shortcut"])
cart.save(os.path.join(drawable_dir, f"ic_shortcut_{ver}.png"), "PNG")
print(f"Generated assets for drawable-{density}")
if __name__ == "__main__":
main()