把项目从「手写 dist/」改成「wallpapers/ 是唯一真相来源,dist/ 由 pnpm build 生成」,
并补上配套的类型、门禁与抓取器。一次提交落地整条管线,因为拆开会留下不能构建的中间态。
- src/:运行时与模拟器源码(TS,strict),编译到 build/ 再拷进各分发
- tools/:build / dev / check-{syntax,paths,dist},以及抓取器与回归门禁 tools/checks/
(.scratch/ 下那批一次性脚本移入 tools/checks/ 并入库为长期门禁)
- wallpapers/:七档壁纸的源数据 + README.md(id/音频/预设的完整规范)
- docs/adr/0005-0008:构建管线与分发拓扑、模拟器契约、自包含 sim、每骨架资源布局
- .gitignore:排除 .scratch/ 的参考资料副本(上游 spine 整仓克隆 ~1.2 GB、
抓取侦查数据 ~680 MB)与调试转储;这些是本地调查材料,补偿会让仓库无法克隆
- 归一化 .gitignore/CONTEXT.md 行尾(工作区 CRLF、索引 LF 造成的整文件假 diff)
同时修掉三档卡住构建的未完工壁纸:
- kv45 的 meta.json 里 id 还是抓取期场景名 scene_main,经 downloader promote 正名为 kv45
- shajin / zhigengniao_juheye 的 meta.json 误用了骨架描述文件(name/spine/animations/pages)、
且都缺 preset.template.json;现按规范重建:骨架沉到 spines/<名>/(spine-ts 按 atlas 所在
目录解析贴图页)、补上元数据与单骨架预设,并清掉 zhigengniao 骨架里指向作者机的绝对路径
- 顺带 promote 已在 sources.yml 里的 kv46(月升之前,与兽共舞)
pnpm check 五道门全绿:10 个分发 / 7 档壁纸 / 141 处引用自包含。
22 KiB
SpineWallpaper
把米哈游活动页里的 Spine 立绘与整页场景,做成一份可以直接上传 Wallpaper Engine 的网页壁纸。
📚 目录
- ✨ 项目简介
- 🧩 特性
- 🚀 快速开始
- 📦 三种产物
- 🧪 本地调试
- 🛠 命令与配置
- 🗂 目录结构
- 🏗 架构与数据流
- 🔬 取景算法
- ✅ 质量门禁
- 📥 资源抓取
- 🚧 已知约束
- 🧠 决策记录
- 🤝 参与与反馈
- 📄 许可证
- 🙏 致谢
✨ 项目简介
这是一个 Wallpaper Engine 网页壁纸(Web 类型)作品 + 围绕它的构建与验证管线。源数据只有一份
(wallpapers/),其余全部由 pnpm build 生成:单档分发、游戏合集、全部合集,各自都能整体拷到别的机器直接上传。
当前收录 3 档壁纸、2 个游戏,共 6 个分发:
| 分发目录 | 内容 | 体积 |
|---|---|---|
single-hsr-xilian |
昔涟立绘(单骨架) | 86.0 MB |
single-hsr-kv37 |
「成为昨日的明天」专题页(单骨架 + 显式像素视口) | 11.6 MB |
single-ys-nico-tea |
魔女尼可的茶会(多骨架 + 场景合成) | 9.7 MB |
collection-hsr |
上面前两档的切换合集 | 97.0 MB |
collection-ys |
原神这一档 | 9.7 MB |
collection-all |
全部壁纸合集(已发布的那一档,创意工坊 3604974793) |
106.3 MB |
Note
dist/是纯产物目录(全量约 390 MB),已 gitignore;真相永远在wallpapers/与src/。 术语(作品 / 分发 / 合集 / 单档 / 预设 / 视口 / 对拍……)见 CONTEXT.md。
🧩 特性
- 🎞 保留作者原始观感:Spine 骨架按原始世界单位渲染,不做矩形归一化;16:9 下与作者取景逐像素一致。
- 🖼 跨比例构图同步:立绘层的缩放跟着背景图的
cover走,竖屏 / 带鱼屏不会出现「特效与插画脱节」。 - 🔀 多预设切换:合集分发用 WE 的
preset下拉在壁纸之间切换,音源、背景、骨架整套跟随。 - 🎵 音源可切换:每档壁纸自带音源清单(无损 FLAC 优先),另有「随预设」与自定义 URL 两种模式。
- 🔊 音量 / 作者信息 / 主题配色:全部走 WE 原生属性表,样式与官方面板对齐。
- 🧪 可在没有 WE 的机器上调试:内置一层严格还原官方 API 面的 WE 模拟器,另可产出双击即看的自包含包。
- 🧱 自包含产物:每个分发目录都是完整的字节副本,不出现任何绝对链接与跨目录相对链接。
- 📐 带门禁:从源数据校验、产物自包含、逐像素对拍到面板还原度,共 24 个验收脚本(见 质量门禁)。
🚀 快速开始
环境要求:
| 依赖 | 版本 | 用途 |
|---|---|---|
| Node.js | ≥ 24(开发用 26) | 构建、调试服、验收脚本 |
| pnpm | 11.x | 包管理与任务入口 |
| Python | 3.10+ 与 PyYAML | 只有资源抓取(tools/downloader/)需要,不需要浏览器 |
| 浏览器 | Chromium 内核(Edge / Chrome) | 调试服、对拍与面板验收 |
# 1) 装依赖(只有 typescript 与 @types/node)
pnpm install
# 2) 全量编译六个分发到 dist/releases/
pnpm build
# 3) 起调试服(默认注入 WE 模拟器),浏览器打开 /release/<分发目录>/
node tools/dev.ts --port 5173
# 4) 跑一遍全部质量门禁(类型 + 构建 + 语法 + 路径 + 产物自包含 + 文案归属)
pnpm check
Tip
只想看某几档、不想等全量拷贝时,用
pnpm build <壁纸id>/pnpm build collect hsr/pnpm build single;产物目录默认会清掉本次未构建的旧分发,保留用--no-clean。
📦 三种产物
「产物形态」是这个项目最容易混的地方,pnpm build 的三档开关分别对应三种用途:
| 用途 | 命令 | 产物 | 关键性质 |
|---|---|---|---|
| 📤 上传创意工坊 | pnpm build |
dist/releases/<分发目录>/ |
干净,没有任何模拟器痕迹 |
| 🌐 静态托管(如 GitHub Pages) | pnpm build --with-sim |
同上,index.html 里多一段驱动 |
地址必须是相对路径(子路径下根绝对路径会 404) |
| 🖱 双击即看 / 离线分享 | pnpm build sim |
额外产出 <分发根>/sim/index.html |
资源全内联(单页 15 MB~129 MB),file:// 可用 |
类型只有两种:单档与合集,区别在收档范围(scope: "game" | "all")——
「全部合集」也是合集,不是第三种类型。
Warning
自包含包会因为体积被 GitHub 的单文件上限挡住(合集约 128.7 MB > 100 MB), 所以静态托管走的是「分发目录 + 构建期注入」,而不是直接发布
sim/index.html。
🧪 本地调试
flowchart TD
Q{"这次要做什么?"} --> A["改 runtime / 模拟器源码"]
Q --> B["验证渲染是否逐像素一致"]
Q --> C["发给别人看 / 静态托管"]
Q --> D["上传创意工坊"]
Q --> E["离线分享,双击即看"]
A --> A1["node tools/dev.ts --port 5173<br/>热更新 + WE 模拟面板"]
B --> B1["node tools/serve.mjs 8190 --root dist/releases/collection-all<br/>+ tools/capture.mjs + verify-visual-equivalence"]
C --> C1["pnpm build --with-sim"]
D --> D1["pnpm build(默认产物,无注入痕迹)"]
E --> E1["pnpm build sim"]
模拟器只在需要时出现,永不进发布产物:调试服按路由注入、静态托管构建期注入、上传 WE 用默认产物零注入。
Important
调试服监听
src/与wallpapers/,改 CSS 只换样式表、改 runtime 整页刷新。 跑test-inline-guard前必须先停掉调试服——它会故意改坏src/vendor/来验证守卫,两者会抢构建。
🛠 命令与配置
npm scripts
| 命令 | 作用 |
|---|---|
pnpm dev |
调试服(SSE 热更新 + 按路由注入 WE 模拟器) |
pnpm build |
全编:所有单档 + 每个游戏合集 + 全部合集 |
pnpm build:pages |
静态托管用产物(--with-sim) |
pnpm typecheck |
tsc 全量类型检查(含验收脚本) |
pnpm check |
门禁总入口:类型 → 构建 → 语法 → 路径 → 产物自包含 → 合集文案归属 |
pnpm serve |
静态服务器(对拍用),node tools/serve.mjs <端口> --root <分发目录> |
pnpm capture |
按 CDP 真实时间拍摄渲染图 |
构建开关
| 开关 | 作用 |
|---|---|
--with-sim |
让分发自带模拟器驱动(静态托管预览用) |
--sim |
额外产出双击可看的自包含包 |
--no-embed-audio |
--sim 时音频不内联(包更小,但 file:// 下播不了) |
--strict |
把 warning 升级为 failure |
--no-clean |
保留本次未构建的旧分发目录 |
--dry-run |
只打印计划,不落盘 |
源数据配置
wallpapers/<游戏id>/<壁纸id>/meta.json:显示名、发布文案(BBCode)、预览图、音源清单。wallpapers/<游戏id>/<壁纸id>/preset.template.json:背景图、骨架 / 场景、动画名、取景。wallpapers/<游戏id>/meta.json:游戏显示名、共享音频、title(该游戏合集的标题,必填)。wallpapers/meta.json:全局文案与defaultPresetId(决定preset下拉首项)。wallpapers/sources.yml:抓取目标页清单(不是构建输入)。VERSION:写进project.json.version(WE 要求数字)。
完整字段表与目录规则见 wallpapers/README.md —— 动 wallpapers/ 之前先读它。
🗂 目录结构
SpineWallpaper/
├── wallpapers/ ← 唯一真相来源(构建的全部输入)
│ ├── meta.json ← 全局文案(= collection-all 那一档)
│ ├── preview.gif
│ └── <游戏id>/
│ ├── meta.json ← 游戏显示名 / 共享音频 / 合集标题
│ └── <壁纸id>/
│ ├── meta.json ← 发布文案 + 音源清单
│ ├── preset.template.json ← 运行时配置(背景 / 骨架 / 取景)
│ ├── spines/<骨架名>/ ← 一具骨架一组:json + atlas + 贴图页
│ ├── scene/ ← 场景图与背景图
│ ├── audios/ ← 音源
│ └── preview.gif
├── src/
│ ├── runtime/ ← 运行时(preset / 背景 / 骨架 / 场景 / 音频 / 取景)
│ ├── simulator/ ← WE 模拟器(严格还原官方 API 面)
│ ├── styles/ templates/ ← 页面骨架与样式
│ ├── vendor/spine-player.js ← spine-ts 运行时副本
│ └── project.template.json ← project.json 的骨架
├── tools/
│ ├── build.ts dev.ts serve.mjs capture.mjs
│ ├── lib/{vault,generate,bundle,types,fs,drivers}.ts
│ ├── checks/ ← 验收套件(24 个脚本)
│ └── downloader/ ← 米哈游活动页 Spine 抓取器(Python)
├── docs/adr/0001–0008 ← 架构决策记录
├── CONTEXT.md ← 领域术语表
└── dist/, build/ ← 产物与编译中间物(gitignore)
🏗 架构与数据流
flowchart LR
subgraph SRC["源数据 · wallpapers/(唯一真相)"]
W["壁纸目录<br/>spines/ · scene/ · audios/<br/>meta.json · preset.template.json"]
end
subgraph FETCH["抓取器(可选)"]
DL["tools/downloader<br/>fetch → staging → promote"]
end
subgraph BUILD["pnpm build"]
V["lib/vault.ts<br/>读取 + 校验"]
G["lib/generate.ts<br/>合成 preset.js / project.json"]
B["lib/bundle.ts<br/>自包含包"]
end
subgraph OUT["dist/releases/"]
S["single-游戏-壁纸"]
C["collection-游戏 / collection-all"]
SIM["分发根/sim/index.html"]
end
WE["Wallpaper Engine<br/>浏览器 / 静态托管"]
DL -->|promote 后再进源数据| W
W --> V --> G
G --> S
G --> C
G --> B --> SIM
S --> WE
C --> WE
SIM -->|file:// 双击| WE
构建只做拷贝,不做链接:早先考虑过 NTFS 硬链接省磁盘,被否掉了——链接会让「产物自包含」这条 基本性质失效,拷到别的机器或只拷一个分发目录时,链接目标不在产物就残了。
🔬 取景算法
背景图走 CSS background-size: cover(铺满、按需裁切),而播放器默认按「含住作者视口」(contain)
映射立绘层——两者只在作者调参的那个比例(16:9)下一致。项目让立绘层跟着背景的缩放走:
\operatorname{coverScale}(a,\ a_{\text{img}}) = \max\!\left(\frac{a}{a_{\text{img}}},\ 1\right)
V_h = V_0.h \times \frac{\operatorname{coverScale}(16/9,\ a_{\text{img}})}{\operatorname{coverScale}(a,\ a_{\text{img}})},
\qquad V_w = V_h \cdot a
其中 a 是当前画布宽高比,a_{\text{img}} 是背景图自身的宽高比(运行时读 naturalWidth / naturalHeight,
不在预设里重复写容易写错的数字),V_0 是作者视口换算到 16:9 后的可见矩形。中心不变,
等价于「立绘层相对背景插画永远 cover」。实现是纯函数,见
viewport-fitter.ts,决策见 ADR 0003。
Important
应用方式是原地改写
config.viewport的字段后调player.setViewport(动画名), 绝不整体替换该对象——setViewport()会无条件读config.viewport.animations[name], 换掉整个对象会在spine-player.js第 14973 行崩溃。维护时机挂在config.frame回调上。
✅ 质量门禁
pnpm check 是最小闭环(类型 + 构建 + 语法 + 路径 + 产物自包含 + 合集文案归属)。其余脚本前置条件不同,
没有一条命令能全跑完;完整清单与逐条调用方式见 tools/checks/README.md。
# ① 不需要服务器
pnpm check
pnpm build sim
node tools/checks/diff-project.mts # 产物 project.json vs 已发布基线
node tools/checks/check-collection-copy.mts # 游戏合集的文案必须是"它自己那个游戏"的
node tools/checks/verify-sim-page.mts <分发名> # 自包含包 file:// 可用(含"零 http 请求")
# ② 需要 tools/serve.mjs(对拍驱动 window.__test 只有它注入)
node tools/serve.mjs 8190 --root dist/releases/collection-all # 另开一个终端
node tools/checks/test-props.mjs test-resize.mjs test-acceptance.mjs
# ③ 需要 pnpm dev(模拟器面板)
node tools/dev.ts --port 5173 --no-build
node tools/checks/verify-panel.mts http://127.0.0.1:5173 # 面板还原度(12 组)
node tools/checks/check-single-bgm.mts http://127.0.0.1:5173 # 单档里切歌真的换音源
Caution
一个从不失败的检查等于没有检查。 门禁类脚本必须先证明它会红(注入一个坏东西、确认报错、再还原)。 写
.mjs时不要加类型注解——它是纯 JS,tsc不管它,注解会在运行时抛 SyntaxError。 另外:不要与调试服同时跑会写dist/的构建,watcher 会和你抢。
📥 资源抓取
tools/downloader/(Python)从 wallpapers/sources.yml 列出的活动页抓 Spine 骨架、贴图与场景装配信息到
staging(_out/)。每个有内容的场景各产出一个可直接搬走的壁纸目录——整个目录挪进
wallpapers/<游戏id>/ 就能用,也可以让 promote 代劳。它不是构建输入:
python -m tools.downloader fetch --page kv45 # 抓一页的全部场景(结尾自动 verify)
python -m tools.downloader fetch # 抓 sources.yml 里的全部页面
python -m tools.downloader fetch --interactive # 逐页问:这一页推荐哪个场景(不影响落盘范围)
python -m tools.downloader select --page nico-tea # 只做选择,写 selection.yml
python -m tools.downloader verify # 只自检产物
python -m tools.downloader fetch --offline # 只用缓存 + 已落盘产物重跑,不发任何请求
python -m tools.downloader promote --page kv45/scene_ava --game hsr --id scene_ava … # 搬进 wallpapers/
node tools/checks/check-downloader.mts # 门:编译 + 夹具绿 + 四处破坏必红
场景装配信息写成 scene.json 侧车(在每个场景目录内),构建期烘焙进 preset.js,运行时不再读侧车。
详见 tools/downloader/README.md。
🚧 已知约束
- WE 运行时(本机 2.8.0.485 / CEF 146):官方只注入
window.wallpaperPropertyListener(applyUserProperties/applyGeneralProperties/setPaused/userDirectoryFiles*),window.wallpaperRegisterListener不存在——按它自检会在真实 WE 里误判。属性事件可能在window.load之前到达,注册必须在模块顶层。 - 音频编码:CEF 的 FFmpeg 白名单含 flac / vorbis / opus / mp3 / pcm,不含 aac,所以 m4a 大概率不可用; 项目按 ADR 0004 优先无损 FLAC,不按体积取舍。
- 帧率:WE 不限流
requestAnimationFrame,用户的 fps 上限经applyGeneralProperties下发,由壁纸自己实现。 - 源码语法:Node 的 strip-only TS 模式不支持
enum、构造函数参数属性、namespace,源码里禁用这三种写法 (pnpm check:syntax会拦);类型检查交给tsc。 - 第三方体积:
src/vendor/spine-player.js是 spine-ts 运行时的打包副本,修改前请先确认上游授权。 - 无托管 CI:
package.json是private,没有.github/workflows;门禁全部在本地跑(见上节)。
🧠 决策记录
| ADR | 决策 |
|---|---|
| 0001 | 资源按「游戏 / 壁纸」分层,目录用 id、显示名只进元数据 |
| 0002 | 预设用 ES module(preset.js)而不是 JSON,路径相对自身推导 |
| 0003 | 立绘层取景跟随背景图的 cover 缩放(16:9 为参考比例) |
| 0004 | 音频用无损,不按体积取舍 |
| 0005 | 构建管线与分发拓扑:id 化目录、不产生任何链接、合集层级 |
| 0006 | 模拟器契约:还原 WE 的 API 面、不自检、替身显式化 |
| 0007 | 自包含调试包的产出方式与两个基准 |
| 0008 | 资源布局按「一具骨架一组」(spines/ + scene/),取代 0001 / 0005 的按类型分层 |
🤝 参与与反馈
这是作者个人的壁纸作品仓库,但欢迎反馈问题与建议:
- 🐛 问题与规格按仓库约定写在
.scratch/<feature>/下(见 docs/agents/issue-tracker.md), 而不是只留在聊天里。 - 🧭 改代码前:先读 CONTEXT.md(术语)与相关 ADR(决策), 再读 AGENTS.md(本仓库给 agent 的约定)。
- ✅ 提交前:
pnpm check必须全绿;新增门禁要先证明它会红。 - 📦 改
wallpapers/前:先读 wallpapers/README.md。 - 🎨 改面板 / 模拟器前:先看 ADR 0006 与
tools/checks/verify-panel.mts。
📄 许可证
仓库当前没有声明开源许可证(package.json 为 private),代码与素材默认保留所有权利;
若要复用,请先联系作者。第三方组件遵循其各自授权:
- Spine 运行时(
src/vendor/spine-player.js):spine-ts,授权随上游 Spine Runtimes License。 - 壁纸中的立绘、场景与音频素材版权归米哈游 / 相关权利人所有,本仓库仅作个人壁纸作品使用。
🙏 致谢
- Spine / spine-ts:骨架动画运行时。
- Wallpaper Engine:宿主与属性面板 API。
- 米哈游活动页:立绘与场景的原始来源(各档来源 URL 见
wallpapers/sources.yml与各壁纸的preset.template.json)。
