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

+58
View File
@@ -0,0 +1,58 @@
// 对拍比较:把两批截图逐对做差值,给出"改完到底有没有动到画面"的判定。
// 用法:node tools/compare.mjs <A目录> <B目录> [--tag A] [--tag B] [--threshold 1.0]
// 例: node tools/compare.mjs before after
import { spawn } from "node:child_process";
import { readdir } from "node:fs/promises";
import { resolve } from "node:path";
const argv = process.argv.slice(2);
const [dirA, dirB] = argv;
if (!dirA || !dirB) {
console.error("usage: node tools/compare.mjs <dirA> <dirB> [--threshold 1.0]");
process.exit(2);
}
const opt = (name, dflt) => {
const i = argv.indexOf(`--${name}`);
return i >= 0 ? argv[i + 1] : dflt;
};
const threshold = Number(opt("threshold", "1.0"));
function run(cmd, args) {
return new Promise((resolve) => {
const p = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
let out = "";
p.stdout.on("data", (d) => (out += d));
p.stderr.on("data", (d) => (out += d));
p.on("close", () => resolve(out));
});
}
const A = resolve("tools/shots", dirA);
const B = resolve("tools/shots", dirB);
const files = (await readdir(A)).filter((f) => f.endsWith(".png") && !f.endsWith(".ref.png")).sort();
let worst = 0;
let worstName = "";
let failed = 0;
for (const f of files) {
const a = resolve(A, f);
const b = resolve(B, f);
const out = await run(process.execPath, ["tools/diff.mjs", a, b]);
const m = out.match(/mean\|diff\|=([\d.]+)/);
const mean = m ? Number(m[1]) : NaN;
const verdict = Number.isNaN(mean) ? "MISSING" : mean < threshold ? "same" : "CHANGED";
if (Number.isNaN(mean)) failed++;
else if (mean > worst) {
worst = mean;
worstName = f;
}
console.log(`${f.padEnd(28)} mean|diff|=${String(mean).padStart(7)} ${verdict}`);
}
console.log(`\n${dirA} → ${dirB}:${files.length} 对;最大差异 ${worst.toFixed(3)}/255(${worstName || "无"});判定阈值 ${threshold}`);
if (failed) {
console.log(`${failed} 对缺少对照文件`);
process.exit(1);
}