795 lines
33 KiB
Python
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()
|