bayuMIR/tools/create_wulin_design_layered_psd.py
2026-06-24 21:00:14 +08:00

270 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()