Skip to content

玄学放置 · 像素图标 SVG 批量导出脚本

动机

到这个阶段,游戏里的像素图标已经形成了一个完整体系:8 大学科 + 玄学 + 9 件装备 + 5 件道具 + 1 个人物剪影,一共 24 个图标。这些视觉资产很可能在游戏外也用得上——比如做插图、贴纸、演示稿配图、甚至独立的素材包。

总不能每次需要一个图标就手动复制一段字符画再写 SVG,需要一个一键批量导出的工具。

用 Node 脚本而不是 Vite 插件

最初想过把它做成 Vite 插件,开发服务器跑起来时自动生成。但很快放弃了,原因有两个:

  • 场景错位:导出素材是低频操作,玩家或开发都不需要每次启动都跑一次
  • 设计耦合:Vite 插件会绑定项目的构建生命周期,未来如果想在另一个项目里复用就得拆出来

最终选择最简单的路径:独立的 Node ESM 脚本,通过 npm run export-icons 触发。

js
// 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:

js
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 / heightscale 写出,决定默认显示尺寸;但因为 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 单位):

js
parts.push(
  `<text x="${tx}" y="${ty}" text-anchor="middle"
         font-size="3" font-family="monospace" fill="#9aa7c2">
     ${name}
   </text>`
)

这样在外部预览工具里直接能看清每个图标对应的 key,不用反查代码。

README 索引

总览图是给眼睛看的,索引是给检索用的。脚本同时输出一份 _README.md

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 加一行:

json
"scripts": {
  "export-icons": "node scripts/export-icons.mjs"
}

.gitignore 加上导出目录,避免每次跑完都在 git 状态里看到一堆变更:

gitignore
# 像素图标导出产物(按需生成,不入库)
assets-export/
assets-export-*/

assets-export-*/ 通配符是配合 --out 参数自定义输出目录的——如果跑 --out assets-export-large/ 也会被忽略。

实测验证

跑几个不同的参数组合验证一下。

默认导出全部:

$ npm run export-icons
✓ 导出完成 → frontend/assets-export
  共 24 个图标,写入 26 个文件
  默认渲染倍率 ×8

26 个文件 = 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 在任意工具中都能被二次缩放

这一阶段也算项目第一阶段的尾声。后续如果继续推进玩法(商店、副本、技能树、转生),这套小工具会随着资产规模一起增长——这是工具型代码长期价值的来源:它服务的是项目本身,而不是某个单一任务

总结

「玄学放置」整个第一阶段做完之后,回头看几次迭代的脉络很清晰:

  1. 搭框架 → 主页布局、ProgressBar、ChatPanel、状态管理
  2. 像素体系 → 8 学科 + 玄学的视觉语言
  3. 装备系统 + 任务单线程 → 玩法骨架成型
  4. 细节打磨 → Tab、favicon、命名规整
  5. 工具化 → 素材导出脚本

每一步都是小步前进,但每一步都把架构往可扩展的方向推了一点。这种节奏比一开始就铺一个大蓝图、然后慢慢"实现"要稳得多——做放置游戏本身就是一个长跑,工具链和架构的健康度,最终会变成内容上线的速度。