270 lines
13 KiB
Python
270 lines
13 KiB
Python
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
from typing import Iterable
|
||
|
||
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||
from psd_tools import PSDImage
|
||
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
ASSET_DIR = ROOT / "client" / "dev" / "res" / "custom" / "47"
|
||
SOURCE_PSD = ASSET_DIR / "\u6b66\u6797\u7edd\u5b66.psd"
|
||
OUTPUT = ASSET_DIR / "wulin_juexue_design_fully_layered.psd"
|
||
PREVIEW = ASSET_DIR / "wulin_juexue_design_fully_layered_preview.png"
|
||
POPUP_PREVIEW = ASSET_DIR / "wulin_juexue_design_fully_layered_popup_preview.png"
|
||
|
||
CANVAS = (816, 582)
|
||
CONTENT = (51, 104, 733, 447)
|
||
|
||
FONT_REGULAR = Path("C:/Windows/Fonts/NotoSansSC-VF.ttf")
|
||
FONT_BOLD = Path("C:/Windows/Fonts/simhei.ttf")
|
||
|
||
|
||
def font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont:
|
||
path = FONT_BOLD if bold and FONT_BOLD.exists() else FONT_REGULAR
|
||
return ImageFont.truetype(str(path), size)
|
||
|
||
|
||
def text_size(draw: ImageDraw.ImageDraw, text: str, fnt: ImageFont.FreeTypeFont) -> tuple[int, int]:
|
||
box = draw.textbbox((0, 0), text, font=fnt)
|
||
return box[2] - box[0], box[3] - box[1]
|
||
|
||
|
||
def make_layer(size: tuple[int, int]) -> tuple[Image.Image, ImageDraw.ImageDraw]:
|
||
img = Image.new("RGBA", size, (0, 0, 0, 0))
|
||
return img, ImageDraw.Draw(img)
|
||
|
||
|
||
def add(psd: PSDImage, image: Image.Image, name: str, x: int, y: int, opacity: int = 255) -> None:
|
||
if image.getbbox() is None:
|
||
return
|
||
psd.create_pixel_layer(image.convert("RGBA"), name=name, top=y, left=x, opacity=opacity)
|
||
|
||
|
||
def layer_rect(
|
||
psd: PSDImage,
|
||
name: str,
|
||
box: tuple[int, int, int, int],
|
||
fill: tuple[int, int, int, int],
|
||
outline: tuple[int, int, int, int] | None = None,
|
||
radius: int = 0,
|
||
width: int = 1,
|
||
) -> None:
|
||
x, y, w, h = box
|
||
img, draw = make_layer((w, h))
|
||
rect = (0, 0, w - 1, h - 1)
|
||
if radius:
|
||
draw.rounded_rectangle(rect, radius=radius, fill=fill, outline=outline, width=width)
|
||
else:
|
||
draw.rectangle(rect, fill=fill, outline=outline, width=width)
|
||
add(psd, img, name, x, y)
|
||
|
||
|
||
def layer_text(
|
||
psd: PSDImage,
|
||
name: str,
|
||
text: str,
|
||
x: int,
|
||
y: int,
|
||
size: int,
|
||
color: tuple[int, int, int, int],
|
||
bold: bool = False,
|
||
max_width: int | None = None,
|
||
align: str = "left",
|
||
) -> None:
|
||
fnt = font(size, bold)
|
||
probe, probe_draw = make_layer((1, 1))
|
||
w, h = text_size(probe_draw, text, fnt)
|
||
if max_width is not None:
|
||
w = min(max_width, w)
|
||
img_w = max(4, (max_width or w) + 4)
|
||
img_h = max(4, h + 8)
|
||
img, draw = make_layer((img_w, img_h))
|
||
tx = 2
|
||
if align == "center":
|
||
real_w, _ = text_size(draw, text, fnt)
|
||
tx = max(2, (img_w - real_w) // 2)
|
||
draw.text((tx, 0), text, font=fnt, fill=color)
|
||
add(psd, img, name, x, y)
|
||
|
||
|
||
def layer_line(
|
||
psd: PSDImage,
|
||
name: str,
|
||
x1: int,
|
||
y1: int,
|
||
x2: int,
|
||
y2: int,
|
||
color: tuple[int, int, int, int],
|
||
width: int = 1,
|
||
) -> None:
|
||
x, y = min(x1, x2), min(y1, y2)
|
||
w, h = abs(x2 - x1) + width + 2, abs(y2 - y1) + width + 2
|
||
img, draw = make_layer((w, h))
|
||
draw.line((x1 - x + 1, y1 - y + 1, x2 - x + 1, y2 - y + 1), fill=color, width=width)
|
||
add(psd, img, name, x, y)
|
||
|
||
|
||
def glow_rect(psd: PSDImage, name: str, box: tuple[int, int, int, int], color: tuple[int, int, int, int], radius: int = 8) -> None:
|
||
x, y, w, h = box
|
||
img, draw = make_layer((w + 24, h + 24))
|
||
draw.rounded_rectangle((12, 12, w + 11, h + 11), radius=radius, outline=color, width=3)
|
||
img = img.filter(ImageFilter.GaussianBlur(7))
|
||
add(psd, img, name, x - 12, y - 12)
|
||
|
||
|
||
def pill(psd: PSDImage, name: str, text: str, x: int, y: int, w: int, color: tuple[int, int, int, int]) -> None:
|
||
layer_rect(psd, f"{name}_Bg", (x, y, w, 24), color, (222, 201, 131, 120), 7)
|
||
layer_text(psd, f"{name}_Text", text, x, y + 2, 13, (252, 237, 183, 255), True, w, "center")
|
||
|
||
|
||
def source_layer_image(layer) -> Image.Image:
|
||
bbox = layer.bbox
|
||
image = layer.composite()
|
||
canvas = Image.new("RGBA", CANVAS, (0, 0, 0, 0))
|
||
canvas.alpha_composite(image.convert("RGBA"), (bbox[0], bbox[1]))
|
||
return canvas
|
||
|
||
|
||
def draw_slots(psd: PSDImage) -> None:
|
||
slots = [
|
||
("Slot01", "归元掌", "已学习", "壹", (83, 252), True),
|
||
("Slot02", "破军剑意", "已学习", "贰", (248, 252), True),
|
||
("Slot03", "未洗炼", "空槽", "叁", (83, 355), False),
|
||
("Slot04", "待解锁", "需佩戴秘籍", "肆", (248, 355), False),
|
||
]
|
||
for key, title, status, num, (x, y), active in slots:
|
||
glow = (235, 181, 61, 150) if active else (62, 172, 216, 90)
|
||
glow_rect(psd, f"{key}_Glow", (x, y, 150, 80), glow, 9)
|
||
fill = (35, 24, 21, 225) if active else (22, 26, 34, 210)
|
||
outline = (230, 174, 75, 190) if active else (83, 119, 134, 170)
|
||
layer_rect(psd, f"{key}_Card", (x, y, 150, 80), fill, outline, 8, 2)
|
||
layer_text(psd, f"{key}_CornerNo", num, x + 11, y + 8, 20, (245, 221, 149, 255), True)
|
||
layer_text(psd, f"{key}_Name", title, x + 42, y + 12, 17, (255, 238, 183, 255), True, 95)
|
||
layer_text(psd, f"{key}_Status", status, x + 42, y + 42, 13, (137, 229, 245, 230), False, 95)
|
||
if not active:
|
||
layer_rect(psd, f"{key}_LockIcon_Back", (x + 116, y + 14, 20, 24), (5, 7, 11, 120), (148, 183, 182, 160), 4)
|
||
layer_text(psd, f"{key}_LockIcon", "锁", x + 116, y + 13, 15, (205, 224, 214, 230), True, 20, "center")
|
||
|
||
|
||
def draw_candidate_rows(psd: PSDImage) -> None:
|
||
rows = [
|
||
("Candidate01", "归元掌", "掌法", "50600"),
|
||
("Candidate02", "破军剑意", "剑意", "50603"),
|
||
("Candidate03", "玄冰指", "指法", "50606"),
|
||
("Candidate04", "金钟罩", "护体", "50702"),
|
||
("Candidate05", "龙象劲", "内功", "50708"),
|
||
("Candidate06", "踏雪无痕", "身法", "50611"),
|
||
]
|
||
x, y = 69, 169
|
||
for i, (key, name, tag, effect_id) in enumerate(rows):
|
||
yy = y + i * 45
|
||
fill = (40, 31, 26, 218) if i == 0 else (24, 27, 32, 205)
|
||
outline = (222, 169, 79, 165) if i == 0 else (80, 107, 116, 130)
|
||
layer_rect(psd, f"{key}_RowBg", (x, yy, 174, 36), fill, outline, 6)
|
||
layer_rect(psd, f"{key}_EffectSwatch", (x + 8, yy + 6, 25, 24), (24, 105, 133, 180), (102, 221, 236, 180), 5)
|
||
layer_line(psd, f"{key}_EffectStrokeA", x + 13, yy + 24, x + 29, yy + 9, (126, 236, 255, 210), 2)
|
||
layer_line(psd, f"{key}_EffectStrokeB", x + 16, yy + 11, x + 29, yy + 25, (246, 198, 82, 190), 2)
|
||
layer_text(psd, f"{key}_Name", name, x + 39, yy + 5, 15, (252, 235, 180, 255), True, 75)
|
||
layer_text(psd, f"{key}_Tag", tag, x + 114, yy + 5, 12, (152, 235, 244, 230), False, 34, "center")
|
||
layer_text(psd, f"{key}_EffectId", effect_id, x + 115, yy + 20, 10, (184, 162, 112, 230), False, 42, "center")
|
||
|
||
|
||
def draw_showcase(psd: PSDImage) -> None:
|
||
cx, cy = 603, 252
|
||
glow_rect(psd, "Showcase_CircleGlow", (cx - 74, cy - 74, 148, 148), (62, 216, 237, 150), 74)
|
||
for idx, radius in enumerate([72, 55, 38]):
|
||
img, draw = make_layer((radius * 2 + 8, radius * 2 + 8))
|
||
draw.ellipse((4, 4, radius * 2 + 3, radius * 2 + 3), outline=(89, 224, 242, 125 - idx * 20), width=2)
|
||
add(psd, img, f"Showcase_Ring_{idx+1}", cx - radius - 4, cy - radius - 4)
|
||
layer_line(psd, "Showcase_SwordLight_01", cx - 45, cy + 47, cx + 43, cy - 48, (128, 239, 255, 230), 4)
|
||
layer_line(psd, "Showcase_SwordLight_02", cx - 20, cy + 55, cx + 23, cy - 52, (248, 202, 74, 190), 3)
|
||
layer_text(psd, "Showcase_EffectId", "Effect 50603", cx - 47, cy + 77, 12, (145, 231, 241, 230), False, 94, "center")
|
||
|
||
|
||
def draw_button(psd: PSDImage, key: str, text: str, x: int, y: int, w: int, primary: bool = False) -> None:
|
||
fill = (139, 78, 38, 235) if primary else (38, 58, 65, 220)
|
||
outline = (252, 198, 93, 210) if primary else (102, 188, 198, 170)
|
||
layer_rect(psd, f"{key}_Bg", (x, y, w, 34), fill, outline, 7, 2)
|
||
layer_text(psd, f"{key}_Text", text, x, y + 6, 15, (255, 241, 193, 255), True, w, "center")
|
||
|
||
|
||
def draw_unlock_popup(psd: PSDImage) -> None:
|
||
layer_rect(psd, "Popup_DimOverlay", (0, 0, 816, 582), (0, 0, 0, 92), None, 0)
|
||
x, y, w, h = 198, 172, 420, 261
|
||
glow_rect(psd, "Popup_OuterGlow", (x, y, w, h), (230, 172, 67, 140), 10)
|
||
layer_rect(psd, "Popup_Frame", (x, y, w, h), (31, 24, 23, 246), (231, 177, 83, 230), 9, 2)
|
||
layer_rect(psd, "Popup_TitleBar", (x + 10, y + 10, w - 20, 38), (75, 45, 29, 235), (235, 194, 97, 150), 7)
|
||
layer_text(psd, "Popup_TitleText", "解锁绝学槽位", x + 15, y + 17, 18, (255, 235, 176, 255), True, w - 30, "center")
|
||
layer_text(psd, "Popup_Subtitle", "佩戴武林秘籍后,可消耗材料开启新的绝学槽", x + 34, y + 60, 14, (165, 232, 239, 240), False, 352, "center")
|
||
|
||
layer_rect(psd, "Popup_ConditionBox", (x + 30, y + 90, 360, 58), (18, 26, 31, 220), (80, 142, 150, 130), 7)
|
||
layer_text(psd, "Popup_ConditionTitle", "解锁条件", x + 46, y + 99, 15, (248, 216, 142, 255), True)
|
||
layer_text(psd, "Popup_ConditionValue", "角色 4 转以上 / 已佩戴武林秘籍", x + 142, y + 100, 14, (212, 238, 225, 255), False, 220)
|
||
layer_text(psd, "Popup_ConditionState", "满足", x + 331, y + 99, 14, (119, 239, 163, 255), True, 42, "center")
|
||
|
||
layer_rect(psd, "Popup_CostBox", (x + 30, y + 158, 360, 42), (22, 20, 17, 220), (160, 122, 62, 130), 7)
|
||
layer_text(psd, "Popup_CostTitle", "消耗", x + 46, y + 167, 15, (248, 216, 142, 255), True)
|
||
layer_text(psd, "Popup_CostValue", "灵石 x1000", x + 143, y + 167, 15, (255, 238, 190, 255), False)
|
||
layer_text(psd, "Popup_CostOwned", "当前 1280", x + 278, y + 168, 13, (156, 229, 240, 240), False)
|
||
|
||
draw_button(psd, "Popup_CancelButton", "取消", x + 83, y + 214, 110, False)
|
||
draw_button(psd, "Popup_ConfirmButton", "确认解锁", x + 226, y + 214, 120, True)
|
||
|
||
|
||
def main() -> None:
|
||
psd = PSDImage.new("RGB", CANVAS, color=(0, 0, 0))
|
||
source = PSDImage.open(SOURCE_PSD)
|
||
|
||
source_names = ["Frame_Source", "ContentFrame_Source", "Title_Source"]
|
||
for source_name, layer in zip(source_names, source):
|
||
add(psd, source_layer_image(layer), source_name, 0, 0)
|
||
|
||
layer_rect(psd, "Content_Backdrop_Tint", CONTENT, (5, 8, 12, 82), None, 0)
|
||
layer_rect(psd, "Top_Status_Bar", (69, 118, 696, 36), (17, 29, 32, 225), (91, 178, 185, 130), 8)
|
||
pill(psd, "Equip_State", "已佩戴 武林秘籍", 83, 124, 132, (54, 95, 55, 230))
|
||
layer_text(psd, "Equip_State_Desc", "可解锁与洗炼,学习到的绝学将展示在秘籍装备上", 228, 126, 14, (202, 231, 219, 245), False, 380)
|
||
layer_text(psd, "Power_Label", "绝学战力", 630, 121, 13, (160, 218, 226, 220), False, 70, "center")
|
||
layer_text(psd, "Power_Value", "28600", 621, 137, 18, (255, 214, 112, 255), True, 88, "center")
|
||
|
||
layer_rect(psd, "LeftPanel_Bg", (69, 162, 190, 359), (13, 17, 22, 218), (89, 135, 142, 140), 8)
|
||
layer_text(psd, "LeftPanel_Title", "绝学候选池", 80, 169, 17, (255, 230, 163, 255), True, 116)
|
||
layer_text(psd, "LeftPanel_Note", "洗炼随机获得其一", 81, 193, 12, (134, 210, 221, 220), False, 130)
|
||
layer_line(psd, "LeftPanel_Divider", 80, 217, 246, 217, (141, 112, 62, 135), 1)
|
||
draw_candidate_rows(psd)
|
||
|
||
layer_rect(psd, "CenterPanel_Bg", (273, 162, 236, 359), (12, 15, 20, 214), (91, 135, 142, 135), 8)
|
||
layer_text(psd, "CenterPanel_Title", "秘籍绝学槽", 289, 169, 17, (255, 230, 163, 255), True, 112)
|
||
layer_text(psd, "CenterPanel_Note", "槽位解锁后才可洗炼", 290, 193, 12, (134, 210, 221, 220), False, 145)
|
||
layer_line(psd, "CenterPanel_Divider", 289, 217, 493, 217, (141, 112, 62, 135), 1)
|
||
draw_slots(psd)
|
||
|
||
layer_rect(psd, "RightPanel_Bg", (522, 162, 243, 359), (14, 17, 23, 218), (91, 135, 142, 140), 8)
|
||
layer_text(psd, "RightPanel_Title", "当前绝学展示", 539, 169, 17, (255, 230, 163, 255), True, 130)
|
||
layer_text(psd, "RightPanel_Note", "动态特效区:GUI:Effect_Create", 540, 193, 12, (134, 210, 221, 220), False, 172)
|
||
layer_line(psd, "RightPanel_Divider", 539, 217, 748, 217, (141, 112, 62, 135), 1)
|
||
draw_showcase(psd)
|
||
layer_text(psd, "Skill_Name", "破军剑意", 540, 344, 22, (255, 231, 156, 255), True, 195, "center")
|
||
layer_text(psd, "Skill_Type", "剑意类 · 攻击绝学", 548, 378, 14, (131, 230, 240, 245), False, 180, "center")
|
||
layer_text(psd, "Skill_Attr01", "攻击 +180 暴击伤害 +5%", 547, 406, 14, (230, 230, 210, 245), False, 186)
|
||
layer_text(psd, "Skill_Attr02", "洗炼后替换当前槽位绝学", 547, 431, 13, (184, 160, 109, 245), False, 186)
|
||
draw_button(psd, "Main_WashButton", "洗炼一次", 542, 474, 94, True)
|
||
draw_button(psd, "Main_SaveButton", "保存绝学", 648, 474, 94, False)
|
||
|
||
draw_unlock_popup(psd)
|
||
|
||
psd.composite().save(POPUP_PREVIEW)
|
||
for layer in psd:
|
||
if layer.name.startswith("Popup_"):
|
||
layer.visible = False
|
||
|
||
psd.save(OUTPUT)
|
||
psd.composite().save(PREVIEW)
|
||
print(OUTPUT)
|
||
print(PREVIEW)
|
||
print(POPUP_PREVIEW)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|