mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 20:20:19 +02:00
feat(android): add adaptive icons, dynamic shortcuts, in-process hot-swap, and exit-to-launcher
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user