feat: TS 构建管线、Spine 抓取器与 wallpapers/ 唯一真相来源

把项目从「手写 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 处引用自包含。
This commit is contained in:
Shuery committed 2026-10-02 01:27:02 +08:00
1 parent b8eee05d78
commit 3f11426964
297 files changed
+216627 -1926

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
import type { Rect, ViewportConfig } from "../globals.js";
// 立绘层的取景计算。纯函数:只做世界坐标矩形的算术,不碰 DOM,也不碰播放器实例。
//
// 矩形一律是 { x, y, width, height },世界单位,y 轴向上。
//
// 为什么需要这一层:
// 背景图走 CSS `background-size: cover`(铺满、按需裁切),
// 立绘层却由播放器按「含住作者视口」映射(contain)。两者只在作者调参的那个比例下一致,
// 比例一变,缩放倍率就分叉——竖屏下背景放大 1.78 倍,立绘层却纹丝不动,特效于是和背景插画脱节。
// 这里让立绘层的缩放跟着背景 cover 的缩放走,两者始终同倍率、同中心。
/** 作者调参时的基准比例。在这个比例下取景与作者设定逐像素一致。 */
export const REFERENCE_ASPECT = 16 / 9;
/** 播放器把 currentViewport(基矩形 + 四边内边距)解成最终可见矩形。
* 对应 spine-player.js 里 `viewport.x = currentViewport.x - padLeft` 那四行。 */
export function resolveViewport(viewport: ViewportConfig): Rect {
return {
x: viewport.x - viewport.padLeft,
y: viewport.y - viewport.padBottom,
width: viewport.width + viewport.padLeft + viewport.padRight,
height: viewport.height + viewport.padBottom + viewport.padTop,
};
}
/** 把矩形按 aspect 撑到刚好含住它,中心不变。这正是播放器在参考比例下的行为。 */
export function contain(rect: Rect, aspect: number): Rect {
const width = Math.max(rect.width, rect.height * aspect);
const height = width / aspect;
const cx = rect.x + rect.width / 2;
const cy = rect.y + rect.height / 2;
return { x: cx - width / 2, y: cy - height / 2, width, height };
}
/** 背景在某个画布比例下的 cover 缩放,以「受高度约束时的倍率」为 1 归一化。
* 背景比画布更宽时按高度铺满(倍率不变),画布更宽时按宽度铺满(倍率随 aspect 增长)。 */
export function coverScale(aspect: number, imageAspect: number): number {
return Math.max(aspect / imageAspect, 1);
}
/** 目标可见矩形:参考比例下正好等于作者视口,其余比例按背景 cover 缩放的反比收放。
* 效果等价于「立绘层相对背景插画永远 cover」。 */
export function frameForAspect(
reference: Rect,
aspect: number,
imageAspect: number,
referenceAspect = REFERENCE_ASPECT,
): Rect {
const height =
(reference.height * coverScale(referenceAspect, imageAspect)) / coverScale(aspect, imageAspect);
const width = height * aspect;
const cx = reference.x + reference.width / 2;
const cy = reference.y + reference.height / 2;
return { x: cx - width / 2, y: cy - height / 2, width, height };
}
/** 把目标可见矩形写成播放器 config.viewport 需要的字段:内边距归零,
* 这样 resolveViewport() 得到的就是它本身。 */
export function toViewportConfig(rect: Rect): ViewportConfig {
return {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
padLeft: 0,
padRight: 0,
padTop: 0,
padBottom: 0,
};
}