Files
AIC-Project/Doc/design/UI/gen_ui_atlas.py
T

795 lines
33 KiB
Python

#!/usr/bin/env python3
"""Generate 2048x2048 Unity UI atlas matching Doc/design/UI_Pic_Set.md layout
and the casual soft-flat adventure style of the reference UI kit."""
from __future__ import annotations
import math
from pathlib import Path
from PIL import Image, ImageDraw, ImageFilter, ImageFont
OUT = Path(__file__).resolve().parent / "ui_atlas_2048.png"
SIZE = 2048
# --- palette (soft-flat casual / tropical adventure) ---
CREAM = (245, 232, 210, 255)
CREAM_DARK = (214, 188, 150, 255)
CREAM_EDGE = (168, 132, 92, 255)
WOOD = (176, 120, 72, 255)
WOOD_DARK = (130, 82, 46, 255)
WOOD_LIGHT = (210, 160, 105, 255)
TEXT_BRN = (92, 58, 36, 255)
WHITE = (255, 255, 255, 255)
GREEN = (110, 196, 92, 255)
GREEN_D = (72, 148, 62, 255)
GREEN_L = (150, 220, 120, 255)
BLUE = (92, 168, 230, 255)
BLUE_D = (58, 120, 190, 255)
BLUE_L = (140, 200, 245, 255)
ORANGE = (245, 168, 72, 255)
ORANGE_D = (210, 120, 40, 255)
PINK = (235, 110, 130, 255)
PINK_D = (195, 70, 95, 255)
YELLOW = (245, 210, 80, 255)
YELLOW_D = (210, 165, 40, 255)
GREY = (160, 170, 185, 255)
GREY_D = (120, 130, 145, 255)
RED = (230, 75, 70, 255)
RED_D = (180, 45, 45, 255)
DIM = (20, 20, 30, 120)
TRANSPARENT = (0, 0, 0, 0)
def clamp(v: int) -> int:
return max(0, min(255, v))
def shade(c, d: int):
return (clamp(c[0] + d), clamp(c[1] + d), clamp(c[2] + d), c[3] if len(c) > 3 else 255)
def round_rect(draw, box, r, fill, outline=None, width=2):
draw.rounded_rectangle(box, radius=r, fill=fill, outline=outline, width=width)
def soft_panel(img, box, fill, edge, r, border=4, highlight=True):
"""Draw a bubbly soft-flat panel with bottom shadow and top highlight."""
x0, y0, x1, y1 = box
layer = Image.new("RGBA", img.size, TRANSPARENT)
d = ImageDraw.Draw(layer)
# bottom shadow
round_rect(d, (x0 + 2, y0 + 4, x1 + 2, y1 + 4), r, shade(edge, -40), None, 0)
round_rect(d, (x0, y0, x1, y1), r, edge, None, 0)
round_rect(d, (x0 + border, y0 + border, x1 - border, y1 - border), max(2, r - border), fill, None, 0)
if highlight:
# soft top gloss strip
gloss = Image.new("RGBA", img.size, TRANSPARENT)
gd = ImageDraw.Draw(gloss)
mid = y0 + (y1 - y0) // 3
gd.rounded_rectangle(
(x0 + border + 2, y0 + border + 2, x1 - border - 2, mid),
radius=max(2, r - border - 2),
fill=(255, 255, 255, 55),
)
gloss = gloss.filter(ImageFilter.GaussianBlur(2))
layer = Image.alpha_composite(layer, gloss)
img.alpha_composite(layer)
def pill_btn(img, box, fill, edge, r=None):
x0, y0, x1, y1 = box
h = y1 - y0
if r is None:
r = h // 2
soft_panel(img, box, fill, edge, r, border=3, highlight=True)
def draw_icon_simple(draw, kind, cx, cy, s, color=WHITE, outline=None):
"""Tiny symbolic icons for atlas."""
o = outline or shade(color, -60)
half = s // 2
def line(*pts, w=3):
draw.line(pts, fill=o, width=w)
if kind == "x":
line(cx - half, cy - half, cx + half, cy + half, w=4)
line(cx + half, cy - half, cx - half, cy + half, w=4)
elif kind == "check":
line(cx - half, cy, cx - half // 3, cy + half, w=4)
line(cx - half // 3, cy + half, cx + half, cy - half, w=4)
elif kind == "back":
line(cx + half // 2, cy - half, cx - half, cy, w=4)
line(cx - half, cy, cx + half // 2, cy + half, w=4)
elif kind == "home":
pts = [
(cx, cy - half),
(cx + half, cy),
(cx + half * 2 // 3, cy),
(cx + half * 2 // 3, cy + half),
(cx - half * 2 // 3, cy + half),
(cx - half * 2 // 3, cy),
(cx - half, cy),
]
draw.polygon(pts, fill=color, outline=o)
elif kind == "search":
draw.ellipse((cx - half, cy - half, cx + half // 2, cy + half // 2), outline=o, width=3)
line(cx + half // 3, cy + half // 3, cx + half, cy + half, w=3)
elif kind == "clear":
draw.ellipse((cx - half, cy - half, cx + half, cy + half), fill=shade(PINK, 20), outline=o, width=2)
line(cx - half // 2, cy - half // 2, cx + half // 2, cy + half // 2, w=3)
line(cx + half // 2, cy - half // 2, cx - half // 2, cy + half // 2, w=3)
elif kind == "copy":
draw.rectangle((cx - half + 4, cy - half + 2, cx + half // 2, cy + half // 2), outline=o, width=2)
draw.rectangle((cx - half // 2, cy - half // 2, cx + half, cy + half), outline=o, width=2)
elif kind == "paste":
draw.rectangle((cx - half, cy - half // 2, cx + half, cy + half), outline=o, width=2)
draw.rectangle((cx - half // 2, cy - half, cx + half // 2, cy - half // 3), outline=o, width=2)
elif kind == "keyboard":
draw.rounded_rectangle((cx - half, cy - half // 2, cx + half, cy + half // 2), 4, outline=o, width=2)
for i in range(3):
for j in range(2):
draw.rectangle(
(cx - half + 6 + i * 10, cy - half // 2 + 6 + j * 10, cx - half + 12 + i * 10, cy - half // 2 + 12 + j * 10),
fill=o,
)
elif kind == "arrow_down":
draw.polygon(
[(cx - half, cy - half // 3), (cx + half, cy - half // 3), (cx, cy + half // 2)],
fill=o,
)
elif kind == "plus":
line(cx - half, cy, cx + half, cy, w=4)
line(cx, cy - half, cx, cy + half, w=4)
elif kind == "minus":
line(cx - half, cy, cx + half, cy, w=4)
elif kind == "lock":
draw.rounded_rectangle((cx - half, cy - half // 4, cx + half, cy + half), 4, fill=color, outline=o, width=2)
draw.arc((cx - half // 2, cy - half, cx + half // 2, cy), 0, 180, fill=o, width=3)
elif kind == "unlock":
draw.rounded_rectangle((cx - half, cy - half // 4, cx + half, cy + half), 4, fill=color, outline=o, width=2)
draw.arc((cx, cy - half, cx + half, cy), 200, 360, fill=o, width=3)
elif kind == "warning":
draw.polygon([(cx, cy - half), (cx + half, cy + half), (cx - half, cy + half)], fill=YELLOW, outline=o)
draw.ellipse((cx - 3, cy + half // 3, cx + 3, cy + half // 3 + 6), fill=o)
draw.rectangle((cx - 2, cy - half // 3, cx + 2, cy + half // 4), fill=o)
elif kind == "info":
draw.ellipse((cx - half, cy - half, cx + half, cy + half), fill=BLUE, outline=o, width=2)
draw.ellipse((cx - 3, cy - half // 2, cx + 3, cy - half // 2 + 6), fill=WHITE)
draw.rectangle((cx - 2, cy - half // 6, cx + 2, cy + half // 2), fill=WHITE)
elif kind == "server":
for i in range(3):
yy = cy - half + i * (s // 3)
draw.rounded_rectangle((cx - half, yy, cx + half, yy + s // 4), 3, fill=BLUE_L, outline=o, width=1)
elif kind == "user":
draw.ellipse((cx - half // 2, cy - half, cx + half // 2, cy), fill=color, outline=o, width=2)
draw.pieslice((cx - half, cy, cx + half, cy + half * 2), 200, 340, fill=color, outline=o)
elif kind == "chat":
draw.rounded_rectangle((cx - half, cy - half, cx + half, cy + half // 3), 8, fill=color, outline=o, width=2)
draw.polygon([(cx - half // 2, cy + half // 4), (cx - half, cy + half), (cx, cy + half // 4)], fill=color, outline=o)
elif kind == "mail":
draw.rounded_rectangle((cx - half, cy - half // 2, cx + half, cy + half // 2), 4, outline=o, width=2)
line(cx - half, cy - half // 2, cx, cy, w=2)
line(cx + half, cy - half // 2, cx, cy, w=2)
elif kind == "settings":
draw.ellipse((cx - half // 2, cy - half // 2, cx + half // 2, cy + half // 2), outline=o, width=3)
for a in range(0, 360, 45):
rad = math.radians(a)
x2 = cx + int(math.cos(rad) * half)
y2 = cy + int(math.sin(rad) * half)
line(cx, cy, x2, y2, w=3)
elif kind == "sound_on":
draw.polygon([(cx - half, cy - half // 3), (cx - half // 3, cy - half // 3), (cx + half // 3, cy - half), (cx + half // 3, cy + half), (cx - half // 3, cy + half // 3), (cx - half, cy + half // 3)], fill=color, outline=o)
draw.arc((cx, cy - half // 2, cx + half, cy + half // 2), -60, 60, fill=o, width=2)
elif kind == "sound_off":
draw_icon_simple(draw, "sound_on", cx, cy, s, color, o)
line(cx - half, cy - half, cx + half, cy + half, w=3)
elif kind == "scan":
draw.rectangle((cx - half, cy - half, cx - half // 3, cy - half // 3), outline=o, width=3)
draw.rectangle((cx + half // 3, cy - half, cx + half, cy - half // 3), outline=o, width=3)
draw.rectangle((cx - half, cy + half // 3, cx - half // 3, cy + half), outline=o, width=3)
draw.rectangle((cx + half // 3, cy + half // 3, cx + half, cy + half), outline=o, width=3)
elif kind == "refresh":
draw.arc((cx - half, cy - half, cx + half, cy + half), 40, 300, fill=o, width=3)
draw.polygon([(cx + half // 2, cy - half), (cx + half, cy - half // 3), (cx + half // 4, cy - half // 3)], fill=o)
elif kind == "download":
line(cx, cy - half, cx, cy + half // 3, w=3)
line(cx - half // 2, cy, cx, cy + half // 2, w=3)
line(cx + half // 2, cy, cx, cy + half // 2, w=3)
line(cx - half, cy + half, cx + half, cy + half, w=3)
elif kind == "upload":
line(cx, cy + half // 3, cx, cy - half, w=3)
line(cx - half // 2, cy - half // 3, cx, cy - half, w=3)
line(cx + half // 2, cy - half // 3, cx, cy - half, w=3)
line(cx - half, cy + half, cx + half, cy + half, w=3)
elif kind == "bag":
draw.rounded_rectangle((cx - half, cy - half // 3, cx + half, cy + half), 6, fill=ORANGE, outline=o, width=2)
draw.arc((cx - half // 2, cy - half, cx + half // 2, cy), 0, 180, fill=o, width=2)
elif kind == "item":
draw.rounded_rectangle((cx - half, cy - half, cx + half, cy + half), 8, fill=BLUE_L, outline=o, width=2)
draw.ellipse((cx - half // 3, cy - half // 3, cx + half // 3, cy + half // 3), fill=YELLOW)
elif kind == "sword":
line(cx, cy + half, cx, cy - half, w=4)
line(cx - half // 2, cy - half // 2, cx + half // 2, cy - half // 2, w=3)
draw.ellipse((cx - 4, cy + half - 4, cx + 4, cy + half + 4), fill=o)
elif kind == "shield":
draw.polygon(
[(cx, cy - half), (cx + half, cy - half // 3), (cx + half * 2 // 3, cy + half), (cx, cy + half // 2), (cx - half * 2 // 3, cy + half), (cx - half, cy - half // 3)],
fill=BLUE,
outline=o,
)
elif kind == "map":
draw.polygon([(cx - half, cy - half // 2), (cx - half // 3, cy - half), (cx + half // 3, cy - half // 2), (cx + half, cy - half), (cx + half, cy + half // 2), (cx + half // 3, cy + half), (cx - half // 3, cy + half // 2), (cx - half, cy + half)], fill=GREEN_L, outline=o)
elif kind == "star":
pts = []
for i in range(5):
a = math.radians(-90 + i * 72)
pts.append((cx + int(math.cos(a) * half), cy + int(math.sin(a) * half)))
a2 = math.radians(-90 + i * 72 + 36)
pts.append((cx + int(math.cos(a2) * half // 2), cy + int(math.sin(a2) * half // 2)))
draw.polygon(pts, fill=YELLOW, outline=o)
elif kind == "heart":
draw.ellipse((cx - half, cy - half // 2, cx, cy + half // 3), fill=PINK, outline=o)
draw.ellipse((cx, cy - half // 2, cx + half, cy + half // 3), fill=PINK, outline=o)
draw.polygon([(cx - half, cy), (cx + half, cy), (cx, cy + half)], fill=PINK, outline=o)
elif kind == "diamond":
draw.polygon([(cx, cy - half), (cx + half, cy), (cx, cy + half), (cx - half, cy)], fill=BLUE_L, outline=o)
elif kind == "key":
draw.ellipse((cx - half, cy - half // 2, cx - half // 6, cy + half // 2), outline=o, width=3)
line(cx - half // 4, cy, cx + half, cy, w=3)
line(cx + half // 2, cy, cx + half // 2, cy + half // 2, w=3)
elif kind == "clock":
draw.ellipse((cx - half, cy - half, cx + half, cy + half), outline=o, width=2)
line(cx, cy, cx, cy - half // 2, w=2)
line(cx, cy, cx + half // 2, cy + half // 4, w=2)
elif kind == "calendar":
draw.rounded_rectangle((cx - half, cy - half // 2, cx + half, cy + half), 4, outline=o, width=2)
line(cx - half // 2, cy - half, cx - half // 2, cy - half // 3, w=2)
line(cx + half // 2, cy - half, cx + half // 2, cy - half // 3, w=2)
line(cx - half, cy - half // 6, cx + half, cy - half // 6, w=2)
elif kind == "coin":
draw.ellipse((cx - half, cy - half, cx + half, cy + half), fill=YELLOW, outline=ORANGE_D, width=3)
draw.ellipse((cx - half // 2, cy - half // 2, cx + half // 2, cy + half // 2), outline=ORANGE_D, width=2)
elif kind == "play":
draw.polygon([(cx - half // 2, cy - half), (cx + half, cy), (cx - half // 2, cy + half)], fill=WHITE, outline=o)
def try_font(size: int):
for name in (
"C:/Windows/Fonts/msyh.ttc",
"C:/Windows/Fonts/segoeui.ttf",
"C:/Windows/Fonts/arial.ttf",
):
try:
return ImageFont.truetype(name, size)
except OSError:
continue
return ImageFont.load_default()
def place(img, name, x, y, w, h, painter):
"""Paint a slice; painter(sub_img, draw, w, h)."""
sub = Image.new("RGBA", (w, h), TRANSPARENT)
d = ImageDraw.Draw(sub)
painter(sub, d, w, h)
img.paste(sub, (x, y), sub)
def main():
img = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
# subtle checker to show transparency zones (very faint, optional) — keep fully transparent for Unity
font_sm = try_font(18)
font_md = try_font(22)
font_lg = try_font(28)
# ---------- panels ----------
def p_main(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), CREAM, CREAM_EDGE, 40, 6)
# wood header
soft_panel(sub, (8, 8, w - 9, 72), WOOD_LIGHT, WOOD_DARK, 28, 4)
d.text((w // 2, 40), "Title", font=font_md, fill=WHITE, anchor="mm")
place(img, "ui_panel_main", 8, 8, 512, 384, p_main)
def p_popup(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), CREAM, CREAM_EDGE, 32, 5)
soft_panel(sub, (8, 8, w - 9, 56), BLUE_L, BLUE_D, 22, 3)
d.text((w // 2, 36), "Popup", font=font_sm, fill=WHITE, anchor="mm")
place(img, "ui_panel_popup", 528, 8, 384, 256, p_popup)
def p_content(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), shade(CREAM, 10), CREAM_DARK, 28, 4)
place(img, "ui_panel_content", 920, 8, 384, 256, p_content)
def p_header(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), WOOD_LIGHT, WOOD_DARK, 24, 4)
d.text((w // 2, h // 2), "Header", font=font_md, fill=WHITE, anchor="mm")
place(img, "ui_panel_header", 1312, 8, 512, 96, p_header)
def p_footer(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), shade(CREAM, -5), CREAM_EDGE, 24, 4)
place(img, "ui_panel_footer", 1312, 112, 512, 96, p_footer)
def p_tooltip(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), (255, 250, 235, 255), CREAM_EDGE, 22, 4)
place(img, "ui_panel_tooltip", 528, 272, 320, 160, p_tooltip)
def list_item(fill, edge):
def _p(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), fill, edge, 20, 3)
return _p
place(img, "ui_list_item_normal", 856, 272, 384, 96, list_item(CREAM, CREAM_DARK))
place(img, "ui_list_item_selected", 1248, 272, 384, 96, list_item(BLUE_L, BLUE_D))
place(img, "ui_list_item_disabled", 1640, 272, 384, 96, list_item(GREY, GREY_D))
def overlay(sub, d, w, h):
d.rectangle((0, 0, w, h), fill=DIM)
place(img, "ui_overlay_dim", 920, 376, 256, 256, overlay)
def div_h(sub, d, w, h):
soft_panel(sub, (0, 2, w - 1, h - 1), CREAM_DARK, CREAM_EDGE, 4, 1, False)
place(img, "ui_divider_h", 1184, 376, 512, 16, div_h)
def div_v(sub, d, w, h):
soft_panel(sub, (2, 0, w - 1, h - 1), CREAM_DARK, CREAM_EDGE, 4, 1, False)
place(img, "ui_divider_v", 1704, 376, 16, 256, div_v)
def badge(fill, edge):
def _p(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), fill, edge, h // 2, 3)
d.text((w // 2, h // 2), "99", font=font_sm, fill=WHITE, anchor="mm")
return _p
place(img, "ui_badge_base", 1728, 376, 128, 64, badge(BLUE, BLUE_D))
place(img, "ui_badge_alert", 1864, 376, 128, 64, badge(RED, RED_D))
# ---------- buttons ----------
def btn(fill, edge, label=None, pressed=False, disabled=False):
def _p(sub, d, w, h):
f, e = fill, edge
if disabled:
f, e = GREY, GREY_D
elif pressed:
f, e = shade(fill, -25), shade(edge, -25)
soft_panel(sub, (0, 0 if not pressed else 2, w - 1, h - 1 if not pressed else h - 3), f, e, h // 2, 3)
if label:
d.text((w // 2, h // 2), label, font=font_md, fill=WHITE if not disabled else shade(WHITE, -40), anchor="mm")
return _p
place(img, "btn_primary_normal", 8, 648, 256, 96, btn(GREEN, GREEN_D, "OK"))
place(img, "btn_primary_hover", 272, 648, 256, 96, btn(GREEN_L, GREEN, "OK"))
place(img, "btn_primary_pressed", 536, 648, 256, 96, btn(GREEN, GREEN_D, "OK", pressed=True))
place(img, "btn_primary_disabled", 800, 648, 256, 96, btn(GREEN, GREEN_D, "OK", disabled=True))
place(img, "btn_secondary_normal", 1064, 648, 256, 96, btn(BLUE, BLUE_D, "Cancel"))
place(img, "btn_secondary_pressed", 1328, 648, 256, 96, btn(BLUE, BLUE_D, "Cancel", pressed=True))
place(img, "btn_warning_normal", 1592, 648, 224, 96, btn(PINK, PINK_D, "Warn"))
place(img, "btn_warning_pressed", 1816, 648, 224, 96, btn(PINK, PINK_D, "Warn", pressed=True))
place(img, "btn_small_normal", 8, 752, 192, 72, btn(ORANGE, ORANGE_D, "Small"))
place(img, "btn_small_hover", 208, 752, 192, 72, btn(shade(ORANGE, 20), ORANGE, "Small"))
place(img, "btn_small_pressed", 408, 752, 192, 72, btn(ORANGE, ORANGE_D, "Small", pressed=True))
place(img, "btn_small_disabled", 608, 752, 192, 72, btn(ORANGE, ORANGE_D, "Small", disabled=True))
def tab(active):
def _p(sub, d, w, h):
fill, edge = (BLUE_L, BLUE_D) if active else (CREAM, CREAM_EDGE)
soft_panel(sub, (0, 8 if not active else 0, w - 1, h - 1), fill, edge, 22, 3)
d.text((w // 2, h // 2 + (4 if not active else 0)), "Tab", font=font_sm, fill=WHITE if active else TEXT_BRN, anchor="mm")
return _p
place(img, "tab_active", 808, 752, 224, 80, tab(True))
place(img, "tab_inactive", 1040, 752, 224, 80, tab(False))
def seg(side, active=False):
def _p(sub, d, w, h):
fill = BLUE if active else CREAM
edge = BLUE_D if active else CREAM_EDGE
if side == "left":
r = (28, 8, 28, 8)
elif side == "right":
r = (8, 28, 28, 8)
else:
r = 8
# approximate with rounded_rectangle
soft_panel(sub, (0, 0, w - 1, h - 1), fill, edge, 16 if side == "mid" else 24, 3)
d.text((w // 2, h // 2), side[0].upper(), font=font_sm, fill=WHITE if active else TEXT_BRN, anchor="mm")
return _p
place(img, "seg_left", 1272, 752, 160, 80, seg("left", True))
place(img, "seg_mid", 1440, 752, 160, 80, seg("mid", False))
place(img, "seg_right", 1608, 752, 160, 80, seg("right", False))
def icon_btn(kind, fill=RED, edge=RED_D):
def _p(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), fill, edge, 18, 3)
draw_icon_simple(d, kind, w // 2, h // 2, 18, WHITE, shade(edge, -20))
return _p
place(img, "btn_close", 1776, 752, 80, 80, icon_btn("x", RED, RED_D))
place(img, "btn_back", 1864, 752, 80, 80, icon_btn("back", BLUE, BLUE_D))
place(img, "btn_home", 1952, 752, 80, 80, icon_btn("home", GREEN, GREEN_D))
# ---------- input ----------
def input_bg(edge_extra=None, focus=False, error=False):
def _p(sub, d, w, h):
fill = WHITE
edge = CREAM_EDGE
if focus:
edge = BLUE_D
if error:
edge = PINK_D
soft_panel(sub, (0, 0, w - 1, h - 1), fill, edge, 24, 3)
d.text((24, h // 2), "Input...", font=font_sm, fill=GREY_D, anchor="lm")
return _p
place(img, "input_bg_normal", 8, 840, 384, 80, input_bg())
place(img, "input_bg_focus", 400, 840, 384, 80, input_bg(focus=True))
place(img, "input_bg_error", 792, 840, 384, 80, input_bg(error=True))
def dropdown(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), WHITE, CREAM_EDGE, 24, 3)
d.text((24, h // 2), "Select", font=font_sm, fill=TEXT_BRN, anchor="lm")
draw_icon_simple(d, "arrow_down", w - 36, h // 2, 14, TEXT_BRN, TEXT_BRN)
place(img, "dropdown_bg", 1184, 840, 384, 80, dropdown)
def arrow(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), BLUE, BLUE_D, 12, 2)
draw_icon_simple(d, "arrow_down", w // 2, h // 2, 14, WHITE, WHITE)
place(img, "dropdown_arrow", 1576, 840, 64, 64, arrow)
def caret(sub, d, w, h):
d.rectangle((1, 4, w - 1, h - 4), fill=BLUE_D)
place(img, "input_caret", 1648, 840, 8, 64, caret)
for name, x, kind in (
("ico_keyboard", 1664, "keyboard"),
("ico_search", 1736, "search"),
("ico_clear", 1808, "clear"),
("ico_copy", 1880, "copy"),
("ico_paste", 1952, "paste"),
):
def make_ico(k):
def _p(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), CREAM, CREAM_EDGE, 14, 2)
draw_icon_simple(d, k, w // 2, h // 2, 16, TEXT_BRN, TEXT_BRN)
return _p
place(img, name, x, 840, 64, 64, make_ico(kind))
# ---------- progress / slider / scrollbar ----------
def progress_bg(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), shade(CREAM, -15), CREAM_EDGE, h // 2, 2, False)
place(img, "progress_bg", 8, 928, 512, 40, progress_bg)
def progress_fill(fill, edge):
def _p(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), fill, edge, h // 2, 2)
return _p
place(img, "progress_fill_blue", 528, 928, 512, 40, progress_fill(BLUE, BLUE_D))
place(img, "progress_fill_green", 1048, 928, 512, 40, progress_fill(GREEN, GREEN_D))
place(img, "progress_fill_warn", 1568, 928, 472, 40, progress_fill(ORANGE, ORANGE_D))
def slider_track(sub, d, w, h):
soft_panel(sub, (0, 4, w - 1, h - 1), GREY, GREY_D, 10, 2, False)
place(img, "slider_track", 8, 976, 512, 32, slider_track)
def slider_fill(sub, d, w, h):
soft_panel(sub, (0, 4, w - 1, h - 1), BLUE, BLUE_D, 10, 2)
place(img, "slider_fill", 528, 976, 512, 32, slider_fill)
def slider_handle(sub, d, w, h):
soft_panel(sub, (4, 4, w - 5, h - 5), WHITE, BLUE_D, w // 2, 3)
d.ellipse((w // 2 - 8, h // 2 - 8, w // 2 + 8, h // 2 + 8), fill=BLUE_L)
place(img, "slider_handle", 1048, 976, 64, 64, slider_handle)
def sb_track_v(sub, d, w, h):
soft_panel(sub, (4, 0, w - 5, h - 1), shade(CREAM, -10), CREAM_DARK, 10, 2, False)
place(img, "scrollbar_track_v", 1120, 976, 32, 512, sb_track_v)
def sb_thumb_v(sub, d, w, h):
soft_panel(sub, (2, 0, w - 3, h - 1), BLUE, BLUE_D, 12, 2)
place(img, "scrollbar_thumb_v", 1160, 976, 32, 128, sb_thumb_v)
def sb_track_h(sub, d, w, h):
soft_panel(sub, (0, 4, w - 1, h - 5), shade(CREAM, -10), CREAM_DARK, 10, 2, False)
place(img, "scrollbar_track_h", 1200, 976, 512, 32, sb_track_h)
def sb_thumb_h(sub, d, w, h):
soft_panel(sub, (0, 2, w - 1, h - 3), BLUE, BLUE_D, 12, 2)
place(img, "scrollbar_thumb_h", 1720, 976, 128, 32, sb_thumb_h)
def loading(frame):
def _p(sub, d, w, h):
soft_panel(sub, (2, 2, w - 3, h - 3), CREAM, CREAM_EDGE, w // 2, 2, False)
bbox = (8, 8, w - 9, h - 9)
start = frame * 180
d.arc(bbox, start, start + 270, fill=BLUE_D, width=6)
return _p
place(img, "loading_ring_0", 1856, 976, 64, 64, loading(0))
place(img, "loading_ring_1", 1928, 976, 64, 64, loading(1))
# ---------- toggle / checkbox / radio / status ----------
def toggle_bg(on):
def _p(sub, d, w, h):
fill, edge = (GREEN, GREEN_D) if on else (GREY, GREY_D)
soft_panel(sub, (0, 0, w - 1, h - 1), fill, edge, h // 2, 3)
return _p
place(img, "toggle_bg_off", 8, 1048, 112, 56, toggle_bg(False))
place(img, "toggle_bg_on", 128, 1048, 112, 56, toggle_bg(True))
def knob(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), WHITE, CREAM_EDGE, w // 2, 2)
place(img, "toggle_knob", 248, 1048, 48, 48, knob)
def checkbox(on):
def _p(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), GREEN if on else WHITE, GREEN_D if on else CREAM_EDGE, 12, 3)
if on:
draw_icon_simple(d, "check", w // 2, h // 2, 16, WHITE, WHITE)
return _p
place(img, "checkbox_off", 304, 1048, 64, 64, checkbox(False))
place(img, "checkbox_on", 376, 1048, 64, 64, checkbox(True))
def radio(on):
def _p(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), WHITE, BLUE_D if on else CREAM_EDGE, w // 2, 3)
if on:
d.ellipse((16, 16, w - 17, h - 17), fill=BLUE)
return _p
place(img, "radio_off", 448, 1048, 64, 64, radio(False))
place(img, "radio_on", 520, 1048, 64, 64, radio(True))
status_icons = [
("ico_check", 592, "check", GREEN),
("ico_lock", 664, "lock", ORANGE),
("ico_unlock", 736, "unlock", GREEN),
("ico_warning", 808, "warning", YELLOW),
("ico_info", 880, "info", BLUE),
("ico_plus", 952, "plus", GREEN),
("ico_minus", 1024, "minus", BLUE),
]
for name, x, kind, col in status_icons:
def make_st(k, c):
def _p(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), shade(c, 40), shade(c, -40), 14, 2)
draw_icon_simple(d, k, w // 2, h // 2, 16, WHITE if k != "warning" else TEXT_BRN, shade(c, -60))
return _p
place(img, name, x, 1048, 64, 64, make_st(kind, col))
def slot(kind):
def _p(sub, d, w, h):
if kind == "empty":
soft_panel(sub, (0, 0, w - 1, h - 1), shade(CREAM, 15), CREAM_EDGE, 16, 3)
elif kind == "selected":
soft_panel(sub, (0, 0, w - 1, h - 1), shade(YELLOW, 30), ORANGE_D, 16, 3)
else:
soft_panel(sub, (0, 0, w - 1, h - 1), GREY, GREY_D, 16, 3)
draw_icon_simple(d, "lock", w // 2, h // 2, 18, WHITE, TEXT_BRN)
return _p
place(img, "slot_empty", 1200, 1048, 96, 96, slot("empty"))
place(img, "slot_selected", 1304, 1048, 96, 96, slot("selected"))
place(img, "slot_locked", 1408, 1048, 96, 96, slot("locked"))
def avatar(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), WOOD_LIGHT, WOOD_DARK, w // 2, 4)
soft_panel(sub, (16, 16, w - 17, h - 17), BLUE_L, BLUE_D, (w - 32) // 2, 3)
draw_icon_simple(d, "user", w // 2, h // 2, 28, WHITE, WOOD_DARK)
place(img, "avatar_frame", 1512, 1048, 128, 128, avatar)
def medal(rank):
colors = {1: (YELLOW, ORANGE_D), 2: (GREY, GREY_D), 3: (ORANGE, ORANGE_D)}
def _p(sub, d, w, h):
fill, edge = colors[rank]
soft_panel(sub, (8, 0, w - 9, h - 17), fill, edge, w // 2 - 8, 3)
d.text((w // 2, h // 2 - 6), str(rank), font=font_lg, fill=WHITE, anchor="mm")
d.polygon([(w // 2 - 10, h - 20), (w // 2, h - 4), (w // 2 + 10, h - 20)], fill=edge)
return _p
place(img, "rank_medal_1", 1648, 1048, 80, 80, medal(1))
place(img, "rank_medal_2", 1736, 1048, 80, 80, medal(2))
place(img, "rank_medal_3", 1824, 1048, 80, 80, medal(3))
def coin(sub, d, w, h):
soft_panel(sub, (4, 4, w - 5, h - 5), YELLOW, ORANGE_D, w // 2, 3)
draw_icon_simple(d, "coin", w // 2, h // 2, 22, YELLOW, ORANGE_D)
place(img, "ico_coin", 1912, 1048, 80, 80, coin)
# ---------- menu / toast / business icons ----------
def menu_bg(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), CREAM, CREAM_EDGE, 28, 5)
place(img, "menu_bg", 8, 1120, 384, 320, menu_bg)
def menu_item(fill, edge):
def _p(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), fill, edge, 14, 2)
return _p
place(img, "menu_item_normal", 400, 1120, 384, 64, menu_item(CREAM, CREAM_DARK))
place(img, "menu_item_hover", 400, 1192, 384, 64, menu_item(BLUE_L, BLUE_D))
place(img, "menu_item_disabled", 400, 1264, 384, 64, menu_item(GREY, GREY_D))
def toast(fill, edge, label):
def _p(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), fill, edge, 28, 4)
d.text((w // 2, h // 2), label, font=font_sm, fill=WHITE, anchor="mm")
return _p
place(img, "toast_bg", 792, 1120, 320, 96, toast(shade(TEXT_BRN, 40), TEXT_BRN, "Toast"))
place(img, "notice_bg", 792, 1224, 320, 96, toast(BLUE, BLUE_D, "Notice"))
def drag(sub, d, w, h):
soft_panel(sub, (0, 4, w - 1, h - 5), GREY, GREY_D, 8, 2, False)
for i in range(3):
xx = w // 2 - 20 + i * 16
d.ellipse((xx, h // 2 - 4, xx + 8, h // 2 + 4), fill=WHITE)
place(img, "drag_handle", 792, 1328, 160, 32, drag)
def qr(sub, d, w, h):
draw_icon_simple(d, "scan", w // 2, h // 2, 40, BLUE_D, BLUE_D)
# corner brackets thicker
soft_panel(sub, (0, 0, 28, 8), BLUE, BLUE_D, 2, 1, False)
soft_panel(sub, (0, 0, 8, 28), BLUE, BLUE_D, 2, 1, False)
soft_panel(sub, (w - 29, 0, w - 1, 8), BLUE, BLUE_D, 2, 1, False)
soft_panel(sub, (w - 9, 0, w - 1, 28), BLUE, BLUE_D, 2, 1, False)
soft_panel(sub, (0, h - 9, 28, h - 1), BLUE, BLUE_D, 2, 1, False)
soft_panel(sub, (0, h - 29, 8, h - 1), BLUE, BLUE_D, 2, 1, False)
soft_panel(sub, (w - 29, h - 9, w - 1, h - 1), BLUE, BLUE_D, 2, 1, False)
soft_panel(sub, (w - 9, h - 29, w - 1, h - 1), BLUE, BLUE_D, 2, 1, False)
place(img, "qr_scan_frame", 960, 1328, 128, 128, qr)
biz_row1 = [
("ico_server", 1200, "server"),
("ico_user", 1272, "user"),
("ico_chat", 1344, "chat"),
("ico_mail", 1416, "mail"),
("ico_settings", 1488, "settings"),
("ico_sound_on", 1560, "sound_on"),
("ico_sound_off", 1632, "sound_off"),
("ico_scan", 1704, "scan"),
("ico_refresh", 1776, "refresh"),
("ico_download", 1848, "download"),
("ico_upload", 1920, "upload"),
]
biz_row2 = [
("ico_bag", 1200, "bag"),
("ico_item", 1272, "item"),
("ico_sword", 1344, "sword"),
("ico_shield", 1416, "shield"),
("ico_map", 1488, "map"),
("ico_star", 1560, "star"),
("ico_heart", 1632, "heart"),
("ico_diamond", 1704, "diamond"),
("ico_key", 1776, "key"),
("ico_clock", 1848, "clock"),
("ico_calendar", 1920, "calendar"),
]
icon_colors = {
"server": BLUE,
"user": ORANGE,
"chat": GREEN,
"mail": BLUE_L,
"settings": GREY_D,
"sound_on": BLUE,
"sound_off": GREY,
"scan": BLUE_D,
"refresh": GREEN,
"download": BLUE,
"upload": ORANGE,
"bag": ORANGE,
"item": YELLOW,
"sword": GREY_D,
"shield": BLUE,
"map": GREEN_L,
"star": YELLOW,
"heart": PINK,
"diamond": BLUE_L,
"key": ORANGE,
"clock": BLUE,
"calendar": PINK,
}
def make_biz(k):
col = icon_colors.get(k, TEXT_BRN)
def _p(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), shade(col, 70), shade(col, -30), 14, 2)
draw_icon_simple(d, k, w // 2, h // 2, 18, WHITE, shade(col, -50))
return _p
for name, x, kind in biz_row1:
place(img, name, x, 1184, 64, 64, make_biz(kind))
for name, x, kind in biz_row2:
place(img, name, x, 1256, 64, 64, make_biz(kind))
# ---------- reserved placeholders (dashed boxes, labeled) ----------
def reserved(label):
def _p(sub, d, w, h):
# translucent dashed-style placeholder
soft_panel(sub, (0, 0, w - 1, h - 1), (245, 232, 210, 80), (168, 132, 92, 120), 20, 3, False)
d.text((w // 2, h // 2), label, font=font_md, fill=(168, 132, 92, 180), anchor="mm")
return _p
place(img, "reserved_large_0", 8, 1464, 512, 256, reserved("Reserved"))
place(img, "reserved_large_1", 528, 1464, 512, 256, reserved("Reserved"))
def icon_grid(sub, d, w, h):
soft_panel(sub, (0, 0, w - 1, h - 1), (245, 232, 210, 60), (168, 132, 92, 100), 16, 2, False)
# 64x64 grid hint
cols = w // 72
rows = h // 72
for r in range(rows):
for c in range(cols):
x0 = 8 + c * 72
y0 = 8 + r * 72
if x0 + 64 <= w - 4 and y0 + 64 <= h - 4:
soft_panel(sub, (x0, y0, x0 + 63, y0 + 63), (255, 255, 255, 40), (168, 132, 92, 80), 10, 1, False)
place(img, "reserved_icon_grid", 1200, 1336, 784, 192, icon_grid)
# footer label strip (non-slice decoration outside reserved? skip — keep clean for Unity)
img.save(OUT, "PNG")
print(f"Wrote {OUT} ({SIZE}x{SIZE})")
if __name__ == "__main__":
main()