166 lines
12 KiB
Python
166 lines
12 KiB
Python
# 로비 UI 개편안 이미지 — (1) 현재 로비 번호 주석 (2) 기본(상주) 모드 와이어프레임 (3) 메뉴 열림 모드 와이어프레임
|
||
import os
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
|
||
BASE = r"E:\BurningTimes\공유\기획\FirstRising"
|
||
OUT = os.path.join(BASE, "리뉴얼", "img"); os.makedirs(OUT, exist_ok=True)
|
||
SRC_LOBBY = r"C:\Users\sw\AppData\Local\Temp\claude\E--BurningTimes\1528ace6-dec7-4abd-8b4a-a81c25af8709\images\1.webp"
|
||
F = lambda s, b=False: ImageFont.truetype(r"C:\Windows\Fonts\malgunbd.ttf" if b else r"C:\Windows\Fonts\malgun.ttf", s)
|
||
RED = (217, 55, 66); INK = (58, 58, 58); SUB = (122, 122, 122); GOLD = (176, 138, 74); PANEL = (42, 39, 41); PANEL2 = (58, 53, 56); HEAD = (94, 36, 41)
|
||
|
||
def badge(d, x, y, n, r=17, fill=RED):
|
||
d.ellipse([x - r, y - r, x + r, y + r], fill=fill, outline=(255, 255, 255), width=2)
|
||
f = F(int(r * 1.15), True); tw = d.textlength(str(n), font=f)
|
||
d.text((x - tw / 2, y - r * 0.78), str(n), font=f, fill=(255, 255, 255))
|
||
|
||
def tag(d, x, y, text, size=17, fg=RED, bg=(255, 255, 255)):
|
||
f = F(size, True); tw = d.textlength(text, font=f)
|
||
d.rounded_rectangle([x, y, x + tw + 16, y + size + 12], 6, fill=bg, outline=fg, width=2)
|
||
d.text((x + 8, y + 3), text, font=f, fill=fg)
|
||
|
||
# ---------------------------------------------------------------- (1) 현재 로비 주석
|
||
lob = Image.open(SRC_LOBBY).convert("RGB")
|
||
lob.save(os.path.join(OUT, "ui_current.png"))
|
||
ov = Image.new("RGBA", lob.size, (0, 0, 0, 0)); od = ImageDraw.Draw(ov)
|
||
boxes = [(1, (262, 18, 704, 96)), (2, (128, 128, 582, 248)), (3, (203, 250, 502, 718)), (4, (4, 312, 104, 858)), (5, (598, 312, 705, 636)),
|
||
(6, (134, 645, 482, 868)), (7, (0, 946, 134, 1056)), (7, (574, 946, 707, 1056)), (8, (203, 988, 507, 1138)), (9, (0, 1140, 707, 1261)), (10, (14, 126, 90, 200))]
|
||
for n, (x0, y0, x1, y1) in boxes:
|
||
od.rectangle([x0, y0, x1, y1], fill=(217, 55, 66, 38), outline=(217, 55, 66, 255), width=3)
|
||
lob2 = Image.alpha_composite(lob.convert("RGBA"), ov).convert("RGB"); d = ImageDraw.Draw(lob2)
|
||
seen = set()
|
||
for n, (x0, y0, x1, y1) in boxes:
|
||
if (n, x0) in seen: continue
|
||
seen.add((n, x0)); badge(d, x0 + 16, y0 + 16, n, r=16)
|
||
lob2.save(os.path.join(OUT, "ui_current_annotated.png"))
|
||
|
||
# ---------------------------------------------------------------- 공통: 바탕화면 + 작업표시줄 + 전투 스트립
|
||
W, H = 1920, 1080
|
||
SX, SW, SH = 260, 1400, 218
|
||
SY = H - 48 - SH - 2
|
||
band = Image.open(os.path.join(BASE, "시스템분석", "img", "capture_01_wave_combat.jpg")).convert("RGB").crop((0, 300, 1600, 620)).resize((SW, SH), Image.LANCZOS)
|
||
|
||
def desktop(work=True):
|
||
im = Image.new("RGB", (W, H), (228, 231, 236)); d = ImageDraw.Draw(im)
|
||
if work:
|
||
d.rectangle([118, 52, 1102, 660], fill=(250, 250, 251), outline=(210, 213, 219), width=2)
|
||
d.rectangle([118, 52, 1102, 90], fill=(234, 236, 240)); d.text((134, 58), "업무 문서 — 예시", font=F(18), fill=SUB)
|
||
for i, w in enumerate((0.8, 0.9, 0.66, 0.84, 0.58, 0.9, 0.72)):
|
||
d.rounded_rectangle([160, 130 + i * 62, 160 + int(900 * w), 146 + i * 62], 6, fill=(221, 224, 229))
|
||
d.rectangle([0, H - 48, W, H], fill=(30, 31, 36))
|
||
for i in range(4):
|
||
d.rectangle([20 + (i % 2) * 12, H - 38 + (i // 2) * 12, 30 + (i % 2) * 12, H - 28 + (i // 2) * 12], fill=(90, 160, 240))
|
||
for i in range(6):
|
||
d.rounded_rectangle([86 + i * 54, H - 40, 120 + i * 54, H - 8], 6, fill=(64, 67, 78))
|
||
d.text((W - 108, H - 38), "오후 3:24", font=F(17), fill=(220, 222, 228))
|
||
# 전투 스트립(FirstRising 실캡처 가로 띠)
|
||
mask = Image.new("L", band.size, 255); md = ImageDraw.Draw(mask)
|
||
for y in range(46):
|
||
md.line([(0, y), (SW, y)], fill=int(255 * y / 46))
|
||
im.paste(band, (SX, SY), mask)
|
||
d = ImageDraw.Draw(im)
|
||
d.rounded_rectangle([SX + 18, SY + 58, SX + 96, SY + 94], 8, fill=(18, 20, 28)); d.text((SX + 34, SY + 60), "2-7", font=F(22, True), fill=(240, 240, 240))
|
||
for px in (SX + 44, SX + SW - 44):
|
||
d.ellipse([px - 26, SY + 124, px + 26, SY + 176], fill=(24, 40, 64), outline=(96, 170, 236), width=4)
|
||
d.text((SX + 34, SY + 132), "◀", font=F(24, True), fill=(150, 200, 250)); d.text((SX + SW - 56, SY + 132), "▶", font=F(24, True), fill=(150, 200, 250))
|
||
for i in range(3):
|
||
hx = SX + 560 + i * 90
|
||
d.rectangle([hx, SY + 70, hx + 66, SY + 80], fill=(28, 28, 28)); d.rectangle([hx, SY + 70, hx + 56 - i * 9, SY + 80], fill=(92, 214, 110))
|
||
d.rounded_rectangle([SX + 120, SY + 198, SX + SW - 160, SY + 208], 4, fill=(40, 40, 46)); d.rounded_rectangle([SX + 120, SY + 198, SX + 120 + int((SW - 280) * 0.62), SY + 208], 4, fill=(242, 163, 58))
|
||
for i, (lbl, col) in enumerate((("일반 3", (70, 130, 200)), ("보스 1", (150, 90, 40)))):
|
||
cx = SX + 880 + i * 110
|
||
d.rounded_rectangle([cx, SY + 56, cx + 42, SY + 92], 6, fill=col, outline=(230, 200, 140), width=2)
|
||
d.text((cx + 48, SY + 62), lbl, font=F(17, True), fill=(250, 236, 180))
|
||
d.ellipse([SX + 1086, SY + 62, SX + 1110, SY + 86], fill=(242, 190, 58)); d.text((SX + 1116, SY + 58), "12,845,210", font=F(20, True), fill=(250, 236, 180))
|
||
d.rounded_rectangle([SX + 1236, SY + 118, SX + 1290, SY + 172], 8, fill=(60, 36, 24), outline=(200, 150, 80), width=3)
|
||
for i in range(3):
|
||
d.line([(SX + 1248, SY + 132 + i * 12), (SX + 1278, SY + 132 + i * 12)], fill=(240, 220, 170), width=4)
|
||
return im
|
||
|
||
# ---------------------------------------------------------------- (2) 기본(상주) 모드
|
||
im = desktop(); d = ImageDraw.Draw(im)
|
||
d.rounded_rectangle([SX + 430, SY - 44, SX + 900, SY - 12], 8, fill=(20, 22, 30)); d.text((SX + 446, SY - 42), "전설 대검 분해 → 2,590 골드", font=F(18), fill=(235, 235, 235))
|
||
marks = [(1, SX + 30, SY + 40, "스테이지 · 좌우 포털"), (2, SX + 548, SY + 52, "파티 3인 HP"), (3, SX + 104, SY + 196, "웨이브 진행"),
|
||
(4, SX + 866, SY + 40, "상자 카운터(자동 개봉)"), (5, SX + 1072, SY + 44, "골드 — 유일 재화"), (6, SX + 1300, SY + 108, "메뉴 토글"), (7, SX + 416, SY - 54, "전투 로그 한 줄")]
|
||
for n, x, y, _ in marks:
|
||
badge(d, x, y, n)
|
||
# 범례(이미지 단독 열람용)
|
||
d.rounded_rectangle([1180, 60, 1860, 520], 14, fill=(255, 255, 255), outline=(210, 213, 219), width=2)
|
||
d.text((1204, 76), "기본(상주) 모드 — 전투 스트립만 보인다", font=F(24, True), fill=INK)
|
||
d.rectangle([1204, 118, 1836, 120], fill=(226, 226, 226))
|
||
for i, (n, _, _, t) in enumerate(marks):
|
||
badge(d, 1224, 150 + i * 50, n, r=14); d.text((1250, 136 + i * 50), t, font=F(20), fill=INK)
|
||
d.text((24, 16), "와이어프레임 A — 전투 영상은 FirstRising 실캡처 합성, UI는 배치 예시", font=F(18, True), fill=RED)
|
||
im.save(os.path.join(OUT, "ui_wire_idle.png"))
|
||
|
||
# ---------------------------------------------------------------- (3) 메뉴 열림 모드
|
||
im = desktop(work=False); d = ImageDraw.Draw(im)
|
||
def panel(x0, y0, x1, y1, title):
|
||
d.rounded_rectangle([x0, y0, x1, y1], 12, fill=PANEL, outline=GOLD, width=3)
|
||
d.rounded_rectangle([x0 + 3, y0 + 3, x1 - 3, y0 + 48], 10, fill=HEAD)
|
||
tw = d.textlength(title, font=F(22, True)); d.text(((x0 + x1) / 2 - tw / 2, y0 + 10), title, font=F(22, True), fill=(250, 240, 225))
|
||
d.rectangle([x1 - 42, y0 + 10, x1 - 14, y0 + 38], outline=(230, 200, 160), width=2); d.text((x1 - 36, y0 + 8), "×", font=F(22, True), fill=(230, 200, 160))
|
||
def blk(x0, y0, x1, y1, label=None, fill=PANEL2, fs=16):
|
||
d.rounded_rectangle([x0, y0, x1, y1], 6, fill=fill)
|
||
if label:
|
||
tw = d.textlength(label, font=F(fs)); d.text(((x0 + x1) / 2 - tw / 2, (y0 + y1) / 2 - fs * 0.7), label, font=F(fs), fill=(200, 196, 190))
|
||
# 왼쪽: 스테이터스
|
||
L = (262, 150, 700, 700); panel(*L, "스테이터스")
|
||
blk(282, 214, 480, 250, "스테이터스", fill=(120, 44, 50)); blk(486, 214, 680, 250, "창고")
|
||
d.ellipse([290, 270, 390, 370], fill=PANEL2, outline=GOLD, width=2); d.text((410, 290), "Lv 52 · 방패 탱커", font=F(20, True), fill=(240, 236, 230)); d.text((410, 324), "계정 Lv · 경험치", font=F(16), fill=(170, 166, 160))
|
||
for i, (k, v) in enumerate((("공격력", "2,069"), ("최대 체력", "4,675"), ("방어", "812"), ("치명타", "24%"))):
|
||
y = 392 + i * 36; d.text((296, y), k, font=F(17), fill=(190, 186, 180)); d.text((560, y), v, font=F(17, True), fill=(245, 240, 230))
|
||
d.line([(292, y + 30), (670, y + 30)], fill=(70, 66, 68), width=1)
|
||
d.text((296, 544), "스킬 · 특성 포인트", font=F(17, True), fill=(245, 240, 230))
|
||
for r in range(2):
|
||
for c in range(6):
|
||
blk(296 + c * 62, 578 + r * 56, 346 + c * 62, 626 + r * 56)
|
||
# 가운데: 영웅·장비
|
||
C = (720, 110, 1200, 700); panel(*C, "영웅 · 장비")
|
||
for i in range(3):
|
||
d.ellipse([790 + i * 120, 172, 860 + i * 120, 242], fill=PANEL2, outline=(92, 214, 110) if i == 0 else GOLD, width=3)
|
||
d.text((1100, 190), "편성", font=F(18, True), fill=(230, 200, 160))
|
||
for r in range(4):
|
||
blk(746, 262 + r * 66, 802, 318 + r * 66); blk(1118, 262 + r * 66, 1174, 318 + r * 66)
|
||
blk(820, 262, 1100, 522, "영웅 3D 미리보기", fs=18)
|
||
grades = [(120, 120, 128), (60, 150, 90), (60, 110, 200), (200, 120, 40), (190, 50, 60), (130, 70, 190)]
|
||
for r in range(2):
|
||
for c in range(8):
|
||
col = grades[(r * 3 + c) % len(grades)]
|
||
d.rounded_rectangle([746 + c * 54, 540 + r * 54, 792 + c * 54, 586 + r * 54], 5, fill=PANEL2, outline=col, width=3)
|
||
blk(746, 652, 960, 688, "가방", fill=(120, 44, 50)); blk(968, 652, 1174, 688, "정렬 · 필터")
|
||
d.ellipse([730, 118, 754, 142], fill=(242, 190, 58)); d.text((760, 116), "12,845,210", font=F(18, True), fill=(250, 236, 180))
|
||
# 오른쪽: 포털
|
||
R = (1220, 150, 1660, 700); panel(*R, "포털 — 스테이지 지도")
|
||
for i, t in enumerate(("일반", "악몽", "지옥", "도전")):
|
||
blk(1240 + i * 104, 214, 1336 + i * 104, 250, t, fill=(120, 44, 50) if i == 0 else PANEL2)
|
||
d.text((1244, 266), "1막 — 버려진 묘지", font=F(20, True), fill=(245, 240, 230))
|
||
pts = [(1270, 330), (1350, 360), (1430, 330), (1510, 370), (1590, 340), (1560, 420), (1480, 450), (1400, 430), (1320, 460), (1270, 520)]
|
||
d.line(pts, fill=(120, 110, 100), width=4)
|
||
for i, (x, y) in enumerate(pts):
|
||
cur = i == 6
|
||
d.ellipse([x - 16, y - 16, x + 16, y + 16], fill=(217, 55, 66) if cur else ((200, 170, 110) if i < 6 else PANEL2), outline=(250, 240, 225) if cur else GOLD, width=3)
|
||
d.rounded_rectangle([1240, 552, 1640, 590], 8, fill=PANEL2); d.text((1256, 558), "스테이지 반복", font=F(18, True), fill=(245, 240, 230))
|
||
d.rounded_rectangle([1560, 558, 1620, 584], 13, fill=(92, 214, 110)); d.ellipse([1594, 560, 1618, 582], fill=(255, 255, 255))
|
||
d.text((1244, 606), "도전 탭: 일일 · 특별 · 캠프 침공", font=F(17), fill=(200, 196, 190))
|
||
d.text((1244, 638), "액트 보스는 입장권으로 · 자동 재도전", font=F(17), fill=(200, 196, 190))
|
||
# 하단 메뉴 5
|
||
labels = ["창고", "스테이터스", "방치 트리", "큐브", "교역선"]
|
||
for i, t in enumerate(labels):
|
||
cx = 780 + i * 90
|
||
d.ellipse([cx - 32, 718, cx + 32, 782], fill=(70, 34, 30), outline=GOLD, width=3)
|
||
tw = d.textlength(t, font=F(16, True)); d.text((cx - tw / 2, 786), t, font=F(16, True), fill=INK)
|
||
# 상단 유틸 바
|
||
d.rounded_rectangle([900, 60, 1200, 100], 10, fill=PANEL, outline=GOLD, width=2)
|
||
for i, t in enumerate(("고정", "소리", "우편", "상점", "설정")):
|
||
x = 912 + i * 58; d.rounded_rectangle([x, 66, x + 46, 94], 5, fill=PANEL2); d.text((x + 6, 69), t, font=F(15, True), fill=(230, 220, 200))
|
||
# 이동 표시(빨간 태그)
|
||
tag(d, 264, 110, "← 프로필·레벨 · 미션 센터")
|
||
tag(d, 1222, 110, "← 챕터 선택 · 빠른 파밍 · 도전 · 캠프 침공")
|
||
tag(d, 1210, 58, "← 좌측 BM 5개 → 상점 1개 · 햄버거 → 설정", size=15)
|
||
tag(d, 722, 70, "← 장비 탭 · 가방", size=15)
|
||
tag(d, 270, 742, "↓ 하단 탭 5개 → 메뉴 5개", size=15)
|
||
tag(d, SX + 820, SY + 100, "← 챕터 보상 상자", size=15)
|
||
d.text((24, 16), "와이어프레임 B — 메뉴를 열면 스트립 위에 패널이 뜬다 (빨간 태그 = 지금 UI에서 옮겨 온 것)", font=F(18, True), fill=RED)
|
||
im.save(os.path.join(OUT, "ui_wire_menu.png"))
|
||
print("saved:", "ui_current_annotated.png", "ui_wire_idle.png", "ui_wire_menu.png")
|