Goon — self-hosted aggregator for adult-content scene metadata. Indexes scenes from TPDB, StashDB, and 30+ public adult tube sites. Cross-source deduplication via perceptual hash + Levenshtein distance. FastAPI backend + APScheduler worker + React Native (Expo) mobile client. FOSS, ad-free, donation-funded. See README for details.
140 lines
4.7 KiB
Python
140 lines
4.7 KiB
Python
"""Generuje ikonki Goona.
|
|
|
|
Output:
|
|
mobile/assets/icon.png — 1024x1024, full square (iOS + fallback)
|
|
mobile/assets/adaptive-icon.png — 1024x1024, foreground only (środek 66% safe)
|
|
mobile/assets/splash.png — 1024x1024, splash screen
|
|
mobile/assets/favicon.png — 48x48 (web)
|
|
|
|
Design:
|
|
- Background: dark gradient (#0E1018 → #1A1D2A) — pasuje do theme.bg/card
|
|
- Glyph: lowercase "g" w purple `#8B5CF6` (theme.accent) z accentGlow soft halo
|
|
- Adaptive icon: TYLKO glyph (Android docina canvas do koła/kwadratu z border radius)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
|
|
|
OUT = Path(__file__).resolve().parents[1] / "mobile" / "assets"
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
|
|
ACCENT = (139, 92, 246, 255) # #8B5CF6
|
|
ACCENT_GLOW = (167, 139, 250, 255) # #A78BFA
|
|
BG_TOP = (14, 16, 24, 255) # #0E1018
|
|
BG_BOT = (26, 29, 42, 255) # #1A1D2A
|
|
TRANSPARENT = (0, 0, 0, 0)
|
|
|
|
|
|
def _vertical_gradient(size: int, top: tuple, bot: tuple) -> Image.Image:
|
|
img = Image.new("RGBA", (size, size), top)
|
|
px = img.load()
|
|
for y in range(size):
|
|
t = y / (size - 1)
|
|
r = int(top[0] * (1 - t) + bot[0] * t)
|
|
g = int(top[1] * (1 - t) + bot[1] * t)
|
|
b = int(top[2] * (1 - t) + bot[2] * t)
|
|
for x in range(size):
|
|
px[x, y] = (r, g, b, 255)
|
|
return img
|
|
|
|
|
|
def _find_font(size: int) -> ImageFont.FreeTypeFont:
|
|
candidates = [
|
|
"C:/Windows/Fonts/segoeuib.ttf", # Segoe UI Bold
|
|
"C:/Windows/Fonts/Arial Bold.ttf",
|
|
"C:/Windows/Fonts/arialbd.ttf",
|
|
"C:/Windows/Fonts/calibrib.ttf",
|
|
]
|
|
for path in candidates:
|
|
try:
|
|
return ImageFont.truetype(path, size)
|
|
except (OSError, IOError):
|
|
continue
|
|
return ImageFont.load_default()
|
|
|
|
|
|
def _draw_g(canvas: Image.Image, *, size: int, glow: bool) -> None:
|
|
"""Rysuje lowercase 'g' centered, z opcjonalnym glow halo."""
|
|
glyph_size = int(size * 0.78)
|
|
font = _find_font(glyph_size)
|
|
|
|
# 'g' lowercase ma descender (idzie poniżej baseline) — używamy textbbox żeby
|
|
# poprawnie wycentrować całość pionowo.
|
|
bbox = font.getbbox("g")
|
|
text_w = bbox[2] - bbox[0]
|
|
text_h = bbox[3] - bbox[1]
|
|
x = (size - text_w) // 2 - bbox[0]
|
|
y = (size - text_h) // 2 - bbox[1]
|
|
|
|
if glow:
|
|
# Glow: rysuj tę samą literę w accentGlow z dużym blur, alpha~50%
|
|
glow_layer = Image.new("RGBA", (size, size), TRANSPARENT)
|
|
gdraw = ImageDraw.Draw(glow_layer)
|
|
gdraw.text((x, y), "g", font=font, fill=(*ACCENT_GLOW[:3], 128))
|
|
glow_layer = glow_layer.filter(ImageFilter.GaussianBlur(size * 0.04))
|
|
canvas.alpha_composite(glow_layer)
|
|
|
|
draw = ImageDraw.Draw(canvas)
|
|
draw.text((x, y), "g", font=font, fill=ACCENT)
|
|
|
|
|
|
def make_icon(size: int = 1024) -> Image.Image:
|
|
"""Pełna ikonka: gradient bg + glyph z glow."""
|
|
img = _vertical_gradient(size, BG_TOP, BG_BOT)
|
|
_draw_g(img, size=size, glow=True)
|
|
return img
|
|
|
|
|
|
def make_adaptive_foreground(size: int = 1024) -> Image.Image:
|
|
"""Adaptive icon foreground: TYLKO glyph na transparent. Android docina canvas
|
|
do circle/squircle/rounded-square — glyph musi mieścić się w 66% środkowych pikseli."""
|
|
img = Image.new("RGBA", (size, size), TRANSPARENT)
|
|
_draw_g(img, size=size, glow=True)
|
|
return img
|
|
|
|
|
|
def make_splash(size: int = 1024) -> Image.Image:
|
|
"""Splash screen — full square with gradient + centered glyph (mniejszy)."""
|
|
img = _vertical_gradient(size, BG_TOP, BG_BOT)
|
|
# mniejszy glyph dla splash (centered ~ 50%)
|
|
glyph_size = int(size * 0.5)
|
|
font = _find_font(glyph_size)
|
|
bbox = font.getbbox("g")
|
|
text_w = bbox[2] - bbox[0]
|
|
text_h = bbox[3] - bbox[1]
|
|
x = (size - text_w) // 2 - bbox[0]
|
|
y = (size - text_h) // 2 - bbox[1]
|
|
|
|
glow_layer = Image.new("RGBA", (size, size), TRANSPARENT)
|
|
gdraw = ImageDraw.Draw(glow_layer)
|
|
gdraw.text((x, y), "g", font=font, fill=(*ACCENT_GLOW[:3], 110))
|
|
glow_layer = glow_layer.filter(ImageFilter.GaussianBlur(size * 0.05))
|
|
img.alpha_composite(glow_layer)
|
|
|
|
draw = ImageDraw.Draw(img)
|
|
draw.text((x, y), "g", font=font, fill=ACCENT)
|
|
return img
|
|
|
|
|
|
def main() -> None:
|
|
icon = make_icon(1024)
|
|
icon.save(OUT / "icon.png", "PNG")
|
|
print(f"wrote {OUT / 'icon.png'}")
|
|
|
|
adaptive = make_adaptive_foreground(1024)
|
|
adaptive.save(OUT / "adaptive-icon.png", "PNG")
|
|
print(f"wrote {OUT / 'adaptive-icon.png'}")
|
|
|
|
splash = make_splash(1024)
|
|
splash.save(OUT / "splash.png", "PNG")
|
|
print(f"wrote {OUT / 'splash.png'}")
|
|
|
|
favicon = icon.resize((48, 48), Image.LANCZOS)
|
|
favicon.save(OUT / "favicon.png", "PNG")
|
|
print(f"wrote {OUT / 'favicon.png'}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|