#!/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')