refactor: 资源按 游戏/壁纸 分层,预设拆进各自壁纸目录

- assets/<游戏>/<壁纸>/{effects,images,audios}/,每档壁纸自包含
- 每档壁纸自带 preset.js:用 import.meta.url 推导自身资源路径,目录可整体搬走;
  目录名用显示名(含「」),代码键用稳定 id(xilian/kv37,与 WE 属性值绑定)
- preset-store.js -> presets.js:静态 import 两份预设并建 id 索引
- 新增 CONTEXT.md 术语表与 ADR-0001/0002
- 新增 check-paths / compare / heatmap 三个验证工具;serve.mjs 改为每帧强制动画相位归零

验证:check-paths 11/11 路径存在(含此前零引用的 昔涟.flac);浏览器 0 个真实 404;
与基线 worktree 同冻结设置对拍,9 对跨版本比较中 4 对逐像素完全相同(差异 0),
其余 ≤0.19/255,远低于基线自身的双模态抖动(2.27/255)。
This commit is contained in:
Shuery committed 2026-09-19 19:26:46 +08:00
1 parent 399f99edd7
commit 09ff322fe8
29 files changed
+388 -127

No files matched your search

@@ -0,0 +1,34 @@
// 「成为昨日的明天」—— 官方 3.7 版本专题展示页
// 这份预设自带全部资源,路径由 import.meta.url 从本文件位置推导,
// 因此整个目录可以整体搬走或改名,而不用改这里的任何一行。
const base = new URL("./", import.meta.url);
const asset = (path) => new URL(path, base).href;
export default {
id: "kv37", // 与 project.json 中 combo 选项的 value 一致,勿改
name: "「成为昨日的明天」",
game: "崩坏:星穹铁道",
backgroundImage: asset("./images/kv37_xilian.png"),
spineConfig: {
jsonUrl: asset("./effects/kv37.json"),
atlasUrl: asset("./images/kv37.atlas"),
animation: "animation",
viewport: {
x: -1215,
y: -410,
width: 2048,
height: 1024,
},
},
audioOptions: {
source: asset("./audios/pv37.mp3"),
volume: 0.5,
},
// 本壁纸可选的音源(第 6 步会接进 project.json 的 bgm 属性)
audioChoices: [{ id: "pv37", name: "版本 PV", source: asset("./audios/pv37.mp3") }],
};
File renamed without changes.
@@ -0,0 +1,37 @@
// 昔涟立绘 —— 官方昔涟动态立绘
// 这份预设自带全部资源,路径由 import.meta.url 从本文件位置推导,
// 因此整个目录可以整体搬走或改名,而不用改这里的任何一行。
const base = new URL("./", import.meta.url);
const asset = (path) => new URL(path, base).href;
export default {
id: "xilian", // 与 project.json 中 combo 选项的 value 一致,勿改
name: "昔涟立绘",
game: "崩坏:星穹铁道",
backgroundImage: asset("./images/ava.jpg"),
spineConfig: {
jsonUrl: asset("./effects/xilian.json"),
atlasUrl: asset("./images/xilian.atlas"),
animation: "idle",
viewport: {
padLeft: "-25%",
padRight: "-28%",
padTop: "-30%",
padBottom: "-23%",
},
},
audioOptions: {
source: asset("./audios/「再度和你」.flac"),
volume: 0.5,
},
// 本壁纸可选的音源(第 6 步会接进 project.json 的 bgm 属性)
audioChoices: [
{ id: "zaiduheni", name: "再度和你", source: asset("./audios/「再度和你」.flac") },
{ id: "xilian", name: "昔涟", source: asset("./audios/昔涟.flac") },
],
};
+87 -87
View File
@@ -1,87 +1,87 @@
import PresetStore from "./preset-store.js";
import PresetController from "./preset-controller.js";
import BackgroundController from "./background-controller.js";
import SpineController from "./spine-controller.js";
import AudioController from "./audio-controller.js";
let presetController = new PresetController(PresetStore);
let { backgroundImage, spineConfig, audioOptions } =
presetController.set("xilian");
let backgroundController = new BackgroundController(backgroundImage);
let spineController = new SpineController("spine-container", spineConfig);
let audioController = new AudioController("background-music", audioOptions);
let currentAudioSource = "";
let currentAudioVolume = 0;
window.addEventListener("load", function () {
backgroundController.setImage();
spineController.create();
audioController.play();
});
window.addEventListener("resize", () => {
spineController.adjustCanvas();
});
window.wallpaperRegisterListener = {
applyGeneralProperties: function (properties) {
if (properties.fps) {
// wallpaperSettings.fps = properties.fps;
}
},
applyUserProperties: function (properties) {
if (properties.preset) {
const preset = properties.preset.value;
let { backgroundImage, spineConfig, audioOptions } =
presetController.set(preset);
backgroundController.changeImage(backgroundImage);
spineController.changeConfig(spineConfig);
audioController.changeOptions(audioOptions);
}
if (properties.audio_file) {
const audio_file = properties.audio_file.value;
currentAudioSource = audio_file;
audioController.changeSource(audio_file);
}
if (properties.audio_volume) {
const audio_volume = properties.audio_volume.value;
currentAudioVolume = audio_volume;
audioController.changeVolume(audio_volume);
}
},
};
window.wallpaperPropertyListener = {
applyGeneralProperties: function (properties) {
if (properties.fps) {
// wallpaperSettings.fps = properties.fps;
}
},
applyUserProperties: function (properties) {
if (properties.preset) {
const preset = properties.preset.value;
let { backgroundImage, spineConfig, audioOptions } = presetController.set(
preset,
currentAudioSource,
currentAudioVolume
);
backgroundController.changeImage(backgroundImage);
spineController.changeConfig(spineConfig);
audioController.changeOptions(audioOptions);
}
if (properties.audio_file) {
const audio_file = properties.audio_file.value;
currentAudioSource = audio_file;
audioController.changeSource(audio_file);
}
if (properties.audio_volume) {
const audio_volume = properties.audio_volume.value;
currentAudioVolume = audio_volume;
audioController.changeVolume(audio_volume);
}
},
};
import Presets from "./presets.js";
import PresetController from "./preset-controller.js";
import BackgroundController from "./background-controller.js";
import SpineController from "./spine-controller.js";
import AudioController from "./audio-controller.js";
let presetController = new PresetController(Presets);
let { backgroundImage, spineConfig, audioOptions } =
presetController.set("xilian");
let backgroundController = new BackgroundController(backgroundImage);
let spineController = new SpineController("spine-container", spineConfig);
let audioController = new AudioController("background-music", audioOptions);
let currentAudioSource = "";
let currentAudioVolume = 0;
window.addEventListener("load", function () {
backgroundController.setImage();
spineController.create();
audioController.play();
});
window.addEventListener("resize", () => {
spineController.adjustCanvas();
});
window.wallpaperRegisterListener = {
applyGeneralProperties: function (properties) {
if (properties.fps) {
// wallpaperSettings.fps = properties.fps;
}
},
applyUserProperties: function (properties) {
if (properties.preset) {
const preset = properties.preset.value;
let { backgroundImage, spineConfig, audioOptions } =
presetController.set(preset);
backgroundController.changeImage(backgroundImage);
spineController.changeConfig(spineConfig);
audioController.changeOptions(audioOptions);
}
if (properties.audio_file) {
const audio_file = properties.audio_file.value;
currentAudioSource = audio_file;
audioController.changeSource(audio_file);
}
if (properties.audio_volume) {
const audio_volume = properties.audio_volume.value;
currentAudioVolume = audio_volume;
audioController.changeVolume(audio_volume);
}
},
};
window.wallpaperPropertyListener = {
applyGeneralProperties: function (properties) {
if (properties.fps) {
// wallpaperSettings.fps = properties.fps;
}
},
applyUserProperties: function (properties) {
if (properties.preset) {
const preset = properties.preset.value;
let { backgroundImage, spineConfig, audioOptions } = presetController.set(
preset,
currentAudioSource,
currentAudioVolume
);
backgroundController.changeImage(backgroundImage);
spineController.changeConfig(spineConfig);
audioController.changeOptions(audioOptions);
}
if (properties.audio_file) {
const audio_file = properties.audio_file.value;
currentAudioSource = audio_file;
audioController.changeSource(audio_file);
}
if (properties.audio_volume) {
const audio_volume = properties.audio_volume.value;
currentAudioVolume = audio_volume;
audioController.changeVolume(audio_volume);
}
},
};
-40
View File
@@ -1,40 +0,0 @@
const PresetStore = {
kv37: {
backgroundImage: "./assets/images/kv37_xilian.png",
spineConfig: {
jsonUrl: "./assets/effects/kv37.json",
atlasUrl: "./assets/images/kv37.atlas",
animation: "animation",
viewport: {
x: -1215,
y: -410,
width: 2048,
height: 1024,
},
},
audioOptions: {
source: "./assets/audios/pv37.mp3",
volume: 0.5,
},
},
xilian: {
backgroundImage: "./assets/images/ava.jpg",
spineConfig: {
jsonUrl: "./assets/effects/xilian.json",
atlasUrl: "./assets/images/xilian.atlas",
animation: "idle",
viewport: {
padLeft: "-25%",
padRight: "-28%",
padTop: "-30%",
padBottom: "-23%",
},
},
audioOptions: {
source: "./assets/audios/「再度和你」.flac",
volume: 0.5,
},
},
};
export default PresetStore;
+12
View File
@@ -0,0 +1,12 @@
// 预设索引:把每份壁纸自己的 preset.js 汇总成 id → 预设 的查表。
//
// id 必须与 project.json 里 combo 选项的 value 一致(xilian / kv37),
// 改了会让老用户已保存的预设选择失效,所以 id 永远跟着 WE 属性走,
// 而目录名(游戏名/壁纸显示名)是给人看的、可以改。
import xilian from "../assets/崩坏:星穹铁道/昔涟立绘/preset.js";
import kv37 from "../assets/崩坏:星穹铁道/「成为昨日的明天」/preset.js";
const Presets = Object.fromEntries([xilian, kv37].map((preset) => [preset.id, preset]));
export default Presets;