101 lines
4.8 KiB
Python
101 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
# Membuat ikon PWA (assets/pwa/*.png) dari potret yang sudah ada di repo, tanpa generator gambar:
|
|
# crop kotak dari assets/menu/browo-select.webp, ubah ukuran, tempel di atas latar gradasi tema game.
|
|
# Jalankan dari mana saja: python3 tools/make_pwa_icons.py (hanya butuh Pillow)
|
|
import os
|
|
from PIL import Image, ImageChops, ImageDraw, ImageFilter
|
|
|
|
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')
|
|
SRC = os.path.join(ROOT, 'assets', 'menu', 'browo-select.webp')
|
|
OUT = os.path.join(ROOT, 'assets', 'pwa')
|
|
TOP, BOTTOM, GOLD = (31, 91, 102), (11, 27, 37), (230, 199, 143) # warna tema: teal gelap, biru malam, emas
|
|
CROP = (50, 0, 690, 640) # topi, wajah, dua tinju, dada (sprite 740x964)
|
|
HEAD = 330 # tinggi bagian kepala pada hasil crop (topi + wajah), dipakai untuk zona aman maskable
|
|
|
|
|
|
def background(n):
|
|
bg = Image.composite(Image.new('RGB', (n, n), BOTTOM), Image.new('RGB', (n, n), TOP), Image.linear_gradient('L').resize((n, n)))
|
|
size = int(n * 1.15) # cahaya emas lembut di belakang kepala
|
|
glow = Image.radial_gradient('L').resize((size, size)).point(lambda v: max(0, 181 - v) * 95 // 181) # nol tepat di tepi lingkaran: tanpa pita
|
|
mask = Image.new('L', (n, n), 0)
|
|
mask.paste(glow, ((n - size) // 2, int(n * .36) - size // 2))
|
|
bg.paste(Image.new('RGB', (n, n), GOLD), mask=mask)
|
|
return bg
|
|
|
|
|
|
def sprite():
|
|
return Image.open(SRC).convert('RGBA').crop(CROP)
|
|
|
|
|
|
def inside_circle(alpha, size, center, radius):
|
|
# True bila semua piksel sprite (alfa > 40) berada dalam lingkaran: dipakai untuk zona aman maskable
|
|
circle = Image.new('L', size, 0)
|
|
ImageDraw.Draw(circle).ellipse((center[0] - radius, center[1] - radius, center[0] + radius, center[1] + radius), fill=255)
|
|
return ImageChops.subtract(alpha.point(lambda v: 255 if v > 40 else 0), circle).getbbox() is None
|
|
|
|
|
|
def place(canvas, spr, scale, cx, bottom=None, cy=None):
|
|
w, h = round(spr.width * scale), round(spr.height * scale)
|
|
s = spr.resize((w, h), Image.LANCZOS)
|
|
x = round(cx - w / 2)
|
|
y = round(bottom - h) if bottom is not None else round(cy - h / 2)
|
|
layer = Image.new('RGBA', canvas.size, (0, 0, 0, 0))
|
|
layer.paste(s, (x, y))
|
|
shadow = Image.new('RGBA', canvas.size, (0, 0, 0, 0))
|
|
shadow.putalpha(layer.getchannel('A').filter(ImageFilter.GaussianBlur(canvas.width / 64)).point(lambda v: v * 6 // 10))
|
|
canvas.alpha_composite(shadow, (0, round(canvas.width / 100)))
|
|
canvas.alpha_composite(layer)
|
|
|
|
|
|
def icon_any(n):
|
|
# Kotak membulat dengan garis emas tipis; sudut luar transparan (peramban desktop tidak memotongnya)
|
|
canvas = background(n).convert('RGBA')
|
|
spr = sprite()
|
|
place(canvas, spr, n * .9 / spr.width, n / 2, bottom=n * .985)
|
|
big = n * 4 # bingkai dan topeng digambar 4x lalu diperkecil supaya tepinya halus
|
|
frame = Image.new('RGBA', (big, big), (0, 0, 0, 0))
|
|
ImageDraw.Draw(frame).rounded_rectangle((big * .012, big * .012, big * .988, big * .988), radius=big * .2, outline=GOLD + (235,), width=max(8, big // 60))
|
|
canvas.alpha_composite(frame.resize((n, n), Image.LANCZOS))
|
|
mask = Image.new('L', (big, big), 0)
|
|
ImageDraw.Draw(mask).rounded_rectangle((0, 0, big - 1, big - 1), radius=big * .205, fill=255)
|
|
canvas.putalpha(ImageChops.multiply(canvas.getchannel('A'), mask.resize((n, n), Image.LANCZOS)))
|
|
return canvas
|
|
|
|
|
|
def icon_bleed(n):
|
|
# Penuh tanpa sudut transparan (apple-touch-icon; iOS membulatkan sendiri)
|
|
canvas = background(n).convert('RGBA')
|
|
spr = sprite()
|
|
place(canvas, spr, n * 1.02 / spr.width, n / 2, bottom=n * 1.02)
|
|
return canvas.convert('RGB')
|
|
|
|
|
|
def icon_maskable(n):
|
|
# Topi dan wajah (bagian atas sprite) harus muat dalam lingkaran aman (radius 40% sisi) agar tidak terpotong
|
|
# topeng ikon adaptif; bahu dan tinju boleh terpotong. Sprite menempel ke dasar supaya tidak terlihat melayang.
|
|
canvas = background(n).convert('RGBA')
|
|
spr = sprite()
|
|
c = (n / 2, n / 2)
|
|
lo, hi = 0, n / spr.width * 1.2 # skala: piksel ikon per piksel sprite; cari yang terbesar yang masih muat
|
|
for _ in range(14):
|
|
k = (lo + hi) / 2
|
|
w, h = round(spr.width * k), round(spr.height * k)
|
|
probe = Image.new('L', (n, n), 0)
|
|
probe.paste(spr.resize((w, h), Image.LANCZOS).getchannel('A').crop((0, 0, w, round(HEAD * k))), (round(c[0] - w / 2), n - h))
|
|
if inside_circle(probe, (n, n), c, n * .395): lo = k
|
|
else: hi = k
|
|
place(canvas, spr, lo, c[0], bottom=n)
|
|
return canvas.convert('RGB')
|
|
|
|
|
|
def save(img, name):
|
|
img.save(os.path.join(OUT, name), optimize=True)
|
|
print(name, img.size, os.path.getsize(os.path.join(OUT, name)) // 1024, 'KB')
|
|
|
|
|
|
os.makedirs(OUT, exist_ok=True)
|
|
for n in (192, 512):
|
|
save(icon_any(n), f'icon-{n}.png')
|
|
save(icon_maskable(n), f'icon-maskable-{n}.png')
|
|
save(icon_bleed(180), 'apple-touch-icon.png')
|