玄学放置 · 像素图标 SVG 批量导出脚本
动机
到这个阶段,游戏里的像素图标已经形成了一个完整体系:8 大学科 + 玄学 + 9 件装备 + 5 件道具 + 1 个人物剪影,一共 24 个图标。这些视觉资产很可能在游戏外也用得上——比如做插图、贴纸、演示稿配图、甚至独立的素材包。
总不能每次需要一个图标就手动复制一段字符画再写 SVG,需要一个一键批量导出的工具。
用 Node 脚本而不是 Vite 插件
最初想过把它做成 Vite 插件,开发服务器跑起来时自动生成。但很快放弃了,原因有两个:
- 场景错位:导出素材是低频操作,玩家或开发都不需要每次启动都跑一次
- 设计耦合:Vite 插件会绑定项目的构建生命周期,未来如果想在另一个项目里复用就得拆出来
最终选择最简单的路径:独立的 Node ESM 脚本,通过 npm run export-icons 触发。
// scripts/export-icons.mjs
import { ICONS, ICON_SIZE } from '../src/assets/pixelIcons.js'这里有一个关键决策:直接 import 业务代码里的 pixelIcons.js。脚本不维护自己的图标数据副本,永远跟业务源同步。后续给游戏新增一个图标,重跑脚本即可同步素材包。
这是「单一数据源」原则的一个典型应用——同一份数据驱动两个完全不同的输出(游戏运行时 + 素材文件包)。
脚本设计
命令行参数
| 参数 | 作用 | 默认值 |
|---|---|---|
--out <dir> | 输出目录 | frontend/assets-export |
--scale <n> | SVG 默认渲染倍率 | 8 |
--bg <color> | 背景色(写 none 表示透明) | 透明 |
--margin <n> | 画板四周留白像素 | 0 |
--only <names> | 仅导出指定图标,逗号分隔 | 全部 |
--no-overview | 不生成总览图 | 默认生成 |
--no-individual | 不生成单图(只要总览) | 默认生成 |
-h / --help | 打印帮助 | — |
参数解析没用 commander/yargs 之类的库,手写一个 switch case 就够了。这个脚本本身就 200 行不到,多引一个依赖反而是负担。
核心:图标 → SVG
每个图标输出一份独立 SVG:
function buildIconSvg(name, icon, opts) {
const { palette, art } = icon
const margin = opts.margin
const size = ICON_SIZE + margin * 2
const display = size * opts.scale
const rects = []
if (opts.bg) {
rects.push(`<rect width="${size}" height="${size}" fill="${opts.bg}"/>`)
}
for (let y = 0; y < art.length; y++) {
const row = art[y]
for (let x = 0; x < row.length; x++) {
const ch = row[x]
if (ch === '.' || ch === ' ') continue
const color = palette[ch]
if (!color) continue
rects.push(
`<rect x="${x + margin}" y="${y + margin}"
width="1" height="1" fill="${color}"/>`
)
}
}
return `<?xml version="1.0" encoding="UTF-8"?>
<!-- ${name} · 16x16 pixel art exported from Metaphysics Idle -->
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 ${size} ${size}"
width="${display}" height="${display}"
shape-rendering="crispEdges">
${rects.map(r => ' ' + r).join('\n')}
</svg>
`
}输出 SVG 的几个关键设计:
viewBox始终保持 16×16(带边距时按 16+2*margin 扩展),保证缩放语义稳定width / height按scale写出,决定默认显示尺寸;但因为 viewBox 不变,任意工具二次缩放都不失真shape-rendering="crispEdges"渲染时关闭反锯齿,缩放保持像素锐利- 头部带
<!-- name · 16x16 pixel art -->注释,后续在素材库里也能看出来源
总览图
只有单图还不够,做素材选型时需要一张能"一眼扫完"的总览图。脚本生成一个 5 列网格的合成 SVG:
chinese math politics history geography
physics chemistry biology mysticism pencil
eraser brush uniform.. uniform.. sneakers
scholar.. scarf glasses character notebook
chalk mineral.. scholar.. hallPass每个单元格内除了图标本身,还在下方加一行小字标签(用 <text font-size="3">,按 viewBox 单位):
parts.push(
`<text x="${tx}" y="${ty}" text-anchor="middle"
font-size="3" font-family="monospace" fill="#9aa7c2">
${name}
</text>`
)这样在外部预览工具里直接能看清每个图标对应的 key,不用反查代码。
README 索引
总览图是给眼睛看的,索引是给检索用的。脚本同时输出一份 _README.md:
# 像素图标素材导出
由 `scripts/export-icons.mjs` 自动生成。
总览见 [_overview.svg](./_overview.svg)。
| 图标 key | 预览 |
|---|---|
| `chinese` | <img src="./chinese.svg" width="64" height="64" /> |
| `math` | <img src="./math.svg" width="64" height="64" /> |
| ...GitHub 渲染 markdown 时会直接显示 SVG 预览,相当于一份可浏览的素材清单。
配套的 npm script 与 .gitignore
package.json 加一行:
"scripts": {
"export-icons": "node scripts/export-icons.mjs"
}.gitignore 加上导出目录,避免每次跑完都在 git 状态里看到一堆变更:
# 像素图标导出产物(按需生成,不入库)
assets-export/
assets-export-*/assets-export-*/ 通配符是配合 --out 参数自定义输出目录的——如果跑 --out assets-export-large/ 也会被忽略。
实测验证
跑几个不同的参数组合验证一下。
默认导出全部:
$ npm run export-icons
✓ 导出完成 → frontend/assets-export
共 24 个图标,写入 26 个文件
默认渲染倍率 ×826 个文件 = 24 个单图 + _overview.svg + _README.md。
带参数:仅导出 3 个图标 + 深色背景 + 1 像素边距:
$ npm run export-icons -- --out test --bg "#0b0f1a" --margin 1 \
--only mysticism,math,physics --no-overview输出 SVG 的 viewBox 自动扩展为 18×18(16+1+1),width / height 按 144 (18×8) 计算,背景色按参数填充。
收获
这个脚本本身代码量不多,但承载的设计思路值得记一下:
- 复用业务代码作为单一数据源:脚本
import业务的pixelIcons.js,不维护副本 - 零依赖:手写 CLI 解析 + Node 标准库写文件,没有任何外部包
- 多产物并行:单图、总览图、README 索引同时输出,覆盖不同使用场景
- viewBox 与 width/height 解耦:保证 SVG 在任意工具中都能被二次缩放
这一阶段也算项目第一阶段的尾声。后续如果继续推进玩法(商店、副本、技能树、转生),这套小工具会随着资产规模一起增长——这是工具型代码长期价值的来源:它服务的是项目本身,而不是某个单一任务。
总结
「玄学放置」整个第一阶段做完之后,回头看几次迭代的脉络很清晰:
- 搭框架 → 主页布局、ProgressBar、ChatPanel、状态管理
- 像素体系 → 8 学科 + 玄学的视觉语言
- 装备系统 + 任务单线程 → 玩法骨架成型
- 细节打磨 → Tab、favicon、命名规整
- 工具化 → 素材导出脚本
每一步都是小步前进,但每一步都把架构往可扩展的方向推了一点。这种节奏比一开始就铺一个大蓝图、然后慢慢"实现"要稳得多——做放置游戏本身就是一个长跑,工具链和架构的健康度,最终会变成内容上线的速度。