# -*- coding: utf-8 -*- """UI 框材组量化改色脚本(M-1 迭代 2 T2,2026-09-28)。 ♻ 复用件 → 复用闸①色板量化:全部产物按明度重映射到 art-bible §4.1/§7.2 token 渐变带, 不保留原包杂色;闸②禁入带=token 色天然合规;闸③灰度可辨=图标四形互异;闸④授权=全 CC0。 源包 = artwork/ui-reuse-2026-09-23/(素材不入 git——先查本仓库,缺位回退主检出 D:/code/g-plan2): oga-kenney-pixel-ui-750(CC0,OGA) 9-Slice/Ancient 面板/按钮九宫格 48×48 kenney-fantasy-ui-borders(CC0) panel-border 石框 48×48 5-石纹底噪/stone_6.png(CC0,OGA Handpainted stone) 无缝底噪 1200×1200 kenney-board-game-icons(CC0) 64px 图标四件 产物 → game/assets/art/ui/(终稿素材目录,不入 git;消费 = UiKit.load_tex 守卫,缺位程序化回退)。 用法:python tools/quantize_ui_skin.py """ from PIL import Image import os ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) MAIN_ROOT = "D:/code/g-plan2" SRC = ("artwork", "ui-reuse-2026-09-23") OUT = ("game", "assets", "art", "ui") def src_path(*parts): for base in (ROOT, MAIN_ROOT): p = os.path.join(base, *SRC, *parts) if os.path.exists(p): return p raise FileNotFoundError("/".join(SRC + parts)) def out_path(name): p = os.path.join(ROOT, *OUT, name) os.makedirs(os.path.dirname(p), exist_ok=True) return p # §4.1/§7.2 token 渐变带(量化目标值——样张 v2 冻结色板) RAMP_PANEL = [(120, 106, 88), (226, 210, 182), (243, 234, 214)] # PANEL_BORDER→PANEL_DATA→PLATE 微亮 RAMP_BUTTON = [(179, 154, 120), (201, 175, 131), (245, 234, 211)] # FLOOR→BRICK(掩尘"可按的砖")→PLATE RAMP_PRESSED = [(120, 106, 88), (179, 154, 120)] # PANEL_BORDER→FLOOR RAMP_BACKDROP = [(30, 25, 20), (120, 106, 88)] # BG_ROOT→PANEL_BORDER(低对比自然后退 §6.1) RAMP_BORDER = [(120, 106, 88), (201, 175, 131)] # PANEL_BORDER→BRICK ICON_GOLD = (240, 168, 40) # 奖励金(钥石) ICON_WARM = (240, 226, 200) # 暖白(柜台件,暗卡上可读) def ramp_color(ramp, t): if t <= 0: return ramp[0] if t >= 1: return ramp[-1] seg = t * (len(ramp) - 1) i = min(int(seg), len(ramp) - 2) k = seg - i a, b = ramp[i], ramp[i + 1] return tuple(int(a[j] + (b[j] - a[j]) * k) for j in range(3)) def remap_luma(img, ramp, gamma=1.0): """明度→token 渐变带重映射(保留 alpha 与纹理结构——闸①量化)。""" im = img.convert("RGBA") px = im.load() vals = [sum(px[x, y][:3]) / 3.0 for y in range(im.size[1]) for x in range(im.size[0]) if px[x, y][3] > 8] lo, hi = min(vals), max(vals) span = max(1.0, hi - lo) for y in range(im.size[1]): for x in range(im.size[0]): r, g, b, a = px[x, y] if a <= 8: continue t = ((sum((r, g, b)) / 3.0) - lo) / span px[x, y] = ramp_color(ramp, t ** gamma) + (a,) return im def recolor_flat(img, rgb): """剪影图标单色重着色(保留 alpha 抗锯齿——形状承载识别,闸③)。""" im = img.convert("RGBA") px = im.load() for y in range(im.size[1]): for x in range(im.size[0]): r, g, b, a = px[x, y] if a > 0: px[x, y] = rgb + (a,) return im def main(): jobs = [] # 面板/按钮九宫格(pixel-ui-750 Ancient;texture_margin 取 8px——消费侧同值) jobs.append(("panel_stone.png", remap_luma(Image.open(src_path("1-UI面板与按钮", "oga-kenney-pixel-ui-750(CC0)", "9-Slice", "Ancient", "brown.png")), RAMP_PANEL))) jobs.append(("button_stone.png", remap_luma(Image.open(src_path("1-UI面板与按钮", "oga-kenney-pixel-ui-750(CC0)", "9-Slice", "Ancient", "tan.png")), RAMP_BUTTON))) jobs.append(("button_stone_pressed.png", remap_luma(Image.open(src_path("1-UI面板与按钮", "oga-kenney-pixel-ui-750(CC0)", "9-Slice", "Ancient", "tan_pressed.png")), RAMP_PRESSED))) # 石框(fantasy-ui-borders) jobs.append(("border_stone.png", remap_luma(Image.open(src_path("1-UI面板与按钮", "kenney-fantasy-ui-borders(CC0)", "PNG", "Default", "Border", "panel-border-000.png")), RAMP_BORDER))) # 石纹底噪(seamless 1200→256,暗带低对比——B 区/开始层背景) stone = Image.open(src_path("5-石纹底噪", "stone_6.png")).convert("RGB").resize((256, 256), Image.LANCZOS) jobs.append(("backdrop_stone.png", remap_luma(stone, RAMP_BACKDROP, gamma=1.6))) # 营地图标四件(board-game-icons 64px;钥石=奖励金,柜台件=暖白) icon_dir = ("3-节点图与营地图标", "kenney-board-game-icons(CC0)", "PNG", "Default (64px)") jobs.append(("icon_keystone.png", recolor_flat(Image.open(src_path(*(icon_dir + ("suit_diamonds.png",)))), ICON_GOLD))) jobs.append(("icon_forge.png", recolor_flat(Image.open(src_path(*(icon_dir + ("sword.png",)))), ICON_WARM))) jobs.append(("icon_workshop.png", recolor_flat(Image.open(src_path(*(icon_dir + ("flask_full.png",)))), ICON_WARM))) jobs.append(("icon_atlas.png", recolor_flat(Image.open(src_path(*(icon_dir + ("book_open.png",)))), ICON_WARM))) for name, im in jobs: im.save(out_path(name)) print("saved:", "/".join(OUT + (name,))) print("=== 量化改色完成(%d 件,全 CC0;闸①②③④ 见脚本头注) ===" % len(jobs)) return 0 if __name__ == "__main__": import sys sys.exit(main())