#!/usr/bin/env python3 # -*- coding: utf-8 -*- """UI 全局主题批小位图生成脚本(迭代 25 T0,2026-10-13)。 产出(→ game/assets/art/ui/gen/,纯色/几何、像素风): slider_grabber.png 滑轨柄·常 12×12 CANDLE #F6C667 + PANEL_BORDER #786E58 1px 描边 slider_grabber_highlight.png 滑轨柄·悬 12×12 CANDLE 提亮档(lightened≈12%) + 同描边 slider_grabber_disabled.png 滑轨柄·废 12×12 STONE_DEEP #554D41 + 同描边 spinbox_updown.png SpinBox 箭头 16×16 INK #33261B ▲▼ 合一(Godot 4 SpinBox 走 updown 合并图标槽) option_arrow.png OptionButton 箭头 8×8 INK 向下三角 checkbox_unchecked.png 勾选框·空 14×14 PANEL_DATA #E9D9B8 + PANEL_BORDER 边 checkbox_checked.png 勾选框·选 14×14 同底边 + INK 对勾 checkbox_unchecked_disabled.png 勾选框·空·废 14×14 同底 + STONE_DEEP 边 checkbox_checked_disabled.png 勾选框·选·废 14×14 同底边 + STONE_DEEP 对勾 色值全部取自 game/presentation/views/ui_kit.gd 既有常量(溯源表见 game/assets/theme/README.md)—— 不新增美术素材、不动美术圣经 §7(迭代 25 契约)。滚动条柄不走位图:Godot 4 ScrollBar 的 grabber 是 StyleBox(非 Icon),纯色 StyleBoxFlat 直接承载(见 main_theme.tres)。 ♻ 可重跑:本脚本幂等,重复执行覆盖产出;改色值只改本脚本顶部常量后重跑 + 重导。 用法: python tools/gen_theme_icons.py """ import os from PIL import Image ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) OUT = os.path.join(ROOT, "game", "assets", "art", "ui", "gen") # ── 色值(↔ ui_kit.gd 常量,冻结)──────────────────────────────────────────── INK = (51, 38, 27) # INK #33261B PANEL_DATA = (233, 217, 184) # PANEL_DATA #E9D9B8 PANEL_BORDER = (120, 110, 88) # PANEL_BORDER #786E58 STONE_DEEP = (85, 77, 65) # STONE_DEEP #554D41 CANDLE = (246, 198, 103) # CANDLE #F6C667 def lighten(c, amount=0.12): """复刻 Godot Color.lightened:每通道 + (255-通道)×amount(sRGB 存储值域)""" return tuple(min(255, round(v + (255 - v) * amount)) for v in c) def save(img, name): path = os.path.join(OUT, name) img.save(path) print("wrote %s (%dx%d)" % (name, img.width, img.height)) def slider_grabber(fill): """12×12 方柄:fill 实心 + PANEL_BORDER 1px 描边""" img = Image.new("RGBA", (12, 12), (0, 0, 0, 0)) for x in range(12): for y in range(12): edge = x in (0, 11) or y in (0, 11) img.putpixel((x, y), PANEL_BORDER + (255,) if edge else fill + (255,)) return img def spinbox_updown(): """16×16 ▲▼ 合一:上三角 y2-7、下三角 y9-14,各宽 12(INK)""" img = Image.new("RGBA", (16, 16), (0, 0, 0, 0)) # 上三角:顶行中心 2px(x7-8),第 6 行满宽 x2-13 for row in range(6): half = 1 + row # 第 row 行左右各扩 1 for x in range(8 - half, 8 + half): img.putpixel((x, 2 + row), INK + (255,)) # 下三角:镜像(第 row 行满宽在底) for row in range(6): half = 6 - row for x in range(8 - half, 8 + half): img.putpixel((x, 9 + row), INK + (255,)) return img def option_arrow(): """8×8 向下三角(OptionButton 箭头)""" img = Image.new("RGBA", (8, 8), (0, 0, 0, 0)) for row in range(5): half = row + 1 for x in range(4 - half, 4 + half - 1): img.putpixel((x, 1 + row), INK + (255,)) return img def checkbox(border, mark): """14×14 勾选框:PANEL_DATA 底 + 1px 边;mark 非 None 时画 2px 对勾""" img = Image.new("RGBA", (14, 14), (0, 0, 0, 0)) for x in range(14): for y in range(14): edge = x in (0, 13) or y in (0, 13) img.putpixel((x, y), border + (255,) if edge else PANEL_DATA + (255,)) if mark is not None: # 对勾:(3,7)→(6,10)→(11,3),2px 粗(内区 1..12) seg = [(3, 7), (4, 7), (4, 8), (5, 8), (5, 9), (6, 9), (6, 10), (7, 10), (7, 11), (8, 11), (8, 10), (9, 10), (9, 9), (10, 9), (10, 8), (11, 8), (11, 7), (10, 7), (10, 6), (9, 6), (9, 5), (8, 5), (8, 4), (7, 4), (7, 3), (6, 3)] for (x, y) in seg: if 1 <= x <= 12 and 1 <= y <= 12: img.putpixel((x, y), mark + (255,)) return img def main(): os.makedirs(OUT, exist_ok=True) save(slider_grabber(CANDLE), "slider_grabber.png") save(slider_grabber(lighten(CANDLE)), "slider_grabber_highlight.png") save(slider_grabber(STONE_DEEP), "slider_grabber_disabled.png") save(spinbox_updown(), "spinbox_updown.png") save(option_arrow(), "option_arrow.png") save(checkbox(PANEL_BORDER, None), "checkbox_unchecked.png") save(checkbox(PANEL_BORDER, INK), "checkbox_checked.png") save(checkbox(STONE_DEEP, None), "checkbox_unchecked_disabled.png") save(checkbox(STONE_DEEP, STONE_DEEP), "checkbox_checked_disabled.png") print("done → %s" % OUT) if __name__ == "__main__": main()