Files
Shuery 399f99edd7 test: 零依赖对拍测试台(可在不改 dist/ 的前提下复现 WE 行为)
tools/serve.mjs    本地静态服务 + 测试驱动注入(模拟 WE 下发用户属性、冻结动画相位、藏 canvas 取参考帧)
tools/cdp.mjs      最小 CDP 客户端(真实时间等待;--screenshot 的虚拟时间会在骨骼加载完成前就耗尽)
tools/shot.mjs     截图/取状态,就绪判据可传入
tools/diff.mjs     ffmpeg 差分:PSNR / SSIM / 平均亮度差
tools/capture.mjs  对拍矩阵:壁纸 × 分辨率 × (正常, 藏 canvas)

实测:xilian 1920x1080 spine 层贡献 mean|diff|=26.1/255,kv37 为 10.2/255,
同配置两次截图噪声底 0.68/255。
2026-09-19 19:13:57 +08:00

71 lines
2.8 KiB
JavaScript

// 对拍矩阵:每套壁纸 × 每种分辨率 各出两张 PNG —— 正常渲染 + 藏起 canvas 的"只有背景"参考帧,
// 然后给出"spine 层占了多少像素"的数值。
// 用法:node tools/capture.mjs <tag> [--presets xilian,kv37] [--sizes 3440x1440,1920x1080,1080x1920]
// [--base http://127.0.0.1:8181] [--propsAt load|dom] [--no-ref]
import { spawn } from "node:child_process";
import { mkdir } from "node:fs/promises";
import { resolve } from "node:path";
const argv = process.argv.slice(2);
const tag = argv[0];
if (!tag || tag.startsWith("--")) {
console.error("usage: node tools/capture.mjs <tag> [--presets a,b] [--sizes WxH,...] [--base url] [--propsAt dom] [--no-ref]");
process.exit(2);
}
const flag = (name, dflt) => {
const i = argv.indexOf(`--${name}`);
return i >= 0 ? argv[i + 1] : dflt;
};
const presets = flag("presets", "xilian,kv37").split(",").filter(Boolean);
const sizes = flag("sizes", "3440x1440,1920x1080,1080x1920").split(",").filter(Boolean);
const base = flag("base", "http://127.0.0.1:8181");
const propsAt = flag("propsAt", "load");
const wantRef = !argv.includes("--no-ref");
const wait = flag("wait", "25000");
const outDir = resolve("tools/shots", tag);
await mkdir(outDir, { recursive: true });
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", (code) => resolve({ code, out }));
});
}
const url = (extra) => `${base}/index.html?${extra}`;
const props = (preset) => `__props=${encodeURIComponent(JSON.stringify({ preset }))}`;
const rows = [];
for (const preset of presets) {
for (const size of sizes) {
const [w, h] = size.split("x");
const main = resolve(outDir, `${preset}-${size}.png`);
const q = `${props(preset)}&__freeze=1${propsAt === "dom" ? "&__propsAt=dom" : ""}`;
const a = await run(process.execPath, ["tools/shot.mjs", main, url(q), w, h, wait]);
if (a.code !== 0) {
console.log(`FAIL ${preset} ${size}: ${a.out.trim().split("\n").pop()}`);
continue;
}
let refLine = "";
if (wantRef) {
const ref = resolve(outDir, `${preset}-${size}.ref.png`);
const b = await run(process.execPath, ["tools/shot.mjs", ref, url(q + "&__hide=canvas"), w, h, wait]);
if (b.code === 0) {
const d = await run(process.execPath, ["tools/diff.mjs", ref, main]);
refLine = d.out.trim();
} else {
refLine = "ref FAILED";
}
}
rows.push(`${preset.padEnd(7)} ${size.padEnd(11)} spine 层贡献: ${refLine}`);
console.log(rows[rows.length - 1]);
}
}
console.log(`\n输出目录: tools/shots/${tag}/`);