lili 86df18c4d8
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(art): 山海风格版批次一——42 样张+产线三件真跑,创始人眼验拍锁锚开批产
矿彩国风锚 r1 定稿:《山海行纪》30 帧(hero×6/狰梼杌穷奇×15/法器×3/UI×2/场景/兽潮/Boss/拾取)+
《山海巡异录》12 张(卡面 3 稀有度×2 职业/敌兽 2/场景 2/结算 2);同名兽狰两款同谱面可辨=
「一条美术管线供两款」首检成立;clean-room 五条全程生效(prompt 零现代作品专名、兽形古籍演绎、
参考图仅自产、全量留痕 manifest.jsonl 含 12 张淘汰逐张原因)。
产线三件真跑演示:色键抠图+除杂(角落印章自动清)/图集 2 页 png 4006KB→webp 599KB(实测修正:
free-tex-packer 不直出 webp 补 PIL 转码)/字体子集 780 字→176KB woff2(LXGW OFL)。
成本良率实测:57 呼 ¥1.44(官方口径)/¥1.88(实测口径),首过率 34/42=81%,重摇≤4 轮填满;
工艺发现 7 条入批次 README(兽名不入 prompt/「鲛」prime 龙形/subject-ref 证伪/UI 伪字走程序排字等)。
创始人裁定(2026-07-07):锁锚开批产——两款按锚 r1 进 M3/M4 资产批产,主角跨帧漂移用
「定妆帧作参考图+关键姿态逐帧」工艺压,不换通道。台账:作战清单 M2 波全记账+_index 三行 M2 已交+设定档裁定戳。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:20:47 -07:00

61 lines
3.5 KiB
JavaScript
Executable File

#!/usr/bin/env node
// 产线件② 图集打包:透明底精灵 → 2048² 图集(png + webp q80)+ Phaser3 坐标 JSON
// 用法: node atlas-pack.mjs [输入目录=../demo/keyed] [输出目录=../demo/atlas] [图集名=shanhai-batch1]
// 实现:free-tex-packer-cli(MIT,--project 工程文件驱动)打包出 png+json;
// 实测 free-tex-packer-core 的 textureFormat 只支持 png/jpg、不直出 webp(材料档 §5 设想有偏),
// 故补一步 Pillow 转码 webp q80,JSON 同步改指 webp —— 包体账按 webp 计。
// LittleJS 侧同一份 json 的 frames 坐标可直接换算 tile 索引;Phaser 侧 load.atlas 原生可用。
import { readFileSync, writeFileSync, readdirSync, mkdirSync, statSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const inDir = resolve(process.argv[2] || join(HERE, '..', 'demo', 'keyed'));
const outDir = resolve(process.argv[3] || join(HERE, '..', 'demo', 'atlas'));
const name = process.argv[4] || 'shanhai-batch1';
mkdirSync(outDir, { recursive: true });
const pngs = readdirSync(inDir).filter(f => f.endsWith('.png'));
if (!pngs.length) { console.error(`输入目录无 png:${inDir}`); process.exit(1); }
// free-tex-packer 工程文件(schema 逐字核自 free-tex-packer-cli@0.3.0 index.js)
const project = {
savePath: outDir,
images: pngs.map(f => ({ name: f, path: join(inDir, f) })),
folders: [],
packOptions: {
textureName: name,
width: 2048, height: 2048, fixedSize: false, powerOfTwo: false,
padding: 2, extrude: 1, allowRotation: false, detectIdentical: true,
allowTrim: true, trimMode: 'trim', alphaThreshold: 0,
exporter: 'Phaser3', removeFileExtension: true, prependFolderName: false,
textureFormat: 'png', base64Export: false, scale: 1, packer: 'MaxRectsPacker',
},
};
const proj = join(outDir, `${name}.ftpp`);
writeFileSync(proj, JSON.stringify(project, null, 2));
execFileSync('free-tex-packer-cli', ['--project', proj, '--output', outDir], { stdio: 'inherit', timeout: 120_000 });
// png → webp q80(PIL),JSON 改指 webp;两格式都保留(png 供开发查看,webp 进包体账)
// 注意:装不下单页时 free-tex-packer 自动分页,产物带 -0/-1 后缀 —— 按通配收集
const pages = readdirSync(outDir).filter(f => f.startsWith(name) && f.endsWith('.png'));
const kb = p => (statSync(p).size / 1024).toFixed(0);
let totalFrames = 0, pngKB = 0, webpKB = 0;
for (const png of pages) {
const base = png.replace(/\.png$/, '');
const pngPath = join(outDir, png);
const webpPath = join(outDir, `${base}.webp`);
execFileSync('python3', ['-c',
`from PIL import Image\nImage.open(${JSON.stringify(pngPath)}).save(${JSON.stringify(webpPath)}, quality=80, method=6)`,
], { stdio: 'inherit', timeout: 60_000 });
const jsonPath = join(outDir, `${base}.json`);
const j = JSON.parse(readFileSync(jsonPath, 'utf8'));
for (const t of j.textures || []) if (t.image) t.image = t.image.replace(/\.png$/, '.webp');
writeFileSync(jsonPath, JSON.stringify(j, null, 2));
const frames = (j.textures || []).reduce((s, t) => s + (t.frames?.length || 0), 0);
totalFrames += frames; pngKB += Number(kb(pngPath)); webpKB += Number(kb(webpPath));
console.log(`${base}:${frames} 帧 | png ${kb(pngPath)}KB → webp ${kb(webpPath)}KB`);
}
console.log(`图集完成:${pages.length} 页 / ${totalFrames} 帧 | png 合计 ${pngKB}KB → webp 合计 ${webpKB}KB`);