Files
xtrip/scripts/gen-placeholder-photos.mjs
T
Stephen a404e42ca8 feat: 初始化西安旅行记忆互动地图游戏项目
完成项目基础搭建:配置 Vite 开发构建环境,集成 Phaser 3 与虚拟摇杆插件,实现地图漫游、景点触发、拍立得弹窗、进度存档等核心功能,添加占位资源与批量生成脚本,配置项目元数据与忽略规则。
2026-10-17 18:01:46 +08:00

117 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @fileoverview 占位照片生成脚本。
*
* 文件用途:
* - 依据 src/data/spots.js 的照片清单,批量生成 4:3 手账风占位 SVG,
* 供照片未就绪期的开发与演示使用。
*
* 核心职责:
* 1. 读取景点配置,为每张照片生成 `public/assets/photos/<id>_<序号>.svg`;
* 2. 占位视觉:暖色渐变 + 景点图标 + 名称与 caption 水印 + 虚线相框;
* 3. 文件已存在时跳过(--force 强制重生成)。
*
* 主要流程:
* node scripts/gen-placeholder-photos.mjs [--force] → 遍历 spots →
* 写出 SVG → 控制台汇总生成结果。
*
* 用法:
* node scripts/gen-placeholder-photos.mjs # 仅生成缺失文件
* node scripts/gen-placeholder-photos.mjs --force # 强制重新生成全部
*/
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import { SPOTS } from '../src/data/spots.js';
/** 脚本所在目录。 */
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
/** 项目根目录。 */
const PROJECT_ROOT = resolve(SCRIPT_DIR, '..');
/** 照片输出目录。 */
const OUT_DIR = resolve(PROJECT_ROOT, 'public/assets/photos');
/** 占位卡宽度(像素)。 */
const W = 1200;
/** 占位卡高度(像素,4:3)。 */
const H = 900;
/**
* 按景点 id 生成稳定的色相(0-360)。
*
* @param {string} id 景点 id。
* @returns {number} 色相角度。
*/
function hueOf(id) {
let hash = 0;
for (const ch of id) {
hash = (hash * 31 + ch.charCodeAt(0)) % 360;
}
return hash;
}
/**
* 生成单张占位照片 SVG 内容。
*
* @param {Object} spot 景点配置。
* @param {Object} photo 照片配置(含 caption)。
* @returns {string} SVG 文本。
*/
function buildSvg(spot, photo) {
const hue = hueOf(spot.id);
const icon = spot.icon;
const escapedCaption = photo.caption
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="hsl(${hue} 38% 78%)" />
<stop offset="1" stop-color="hsl(${(hue + 24) % 360} 30% 64%)" />
</linearGradient>
</defs>
<rect width="${W}" height="${H}" fill="url(#bg)" />
<rect x="36" y="36" width="${W - 72}" height="${H - 72}" fill="none"
stroke="rgba(255,250,235,0.7)" stroke-width="4" stroke-dasharray="18 12" rx="10" />
<text x="${W / 2}" y="${H / 2 - 44}" font-size="150" text-anchor="middle">${icon}</text>
<text x="${W / 2}" y="${H / 2 + 92}" font-family="Kaiti SC, KaiTi, serif" font-size="64"
fill="rgba(70,50,28,0.82)" text-anchor="middle">${escapedCaption}</text>
<text x="${W / 2}" y="${H / 2 + 158}" font-family="Kaiti SC, KaiTi, serif" font-size="36"
fill="rgba(70,50,28,0.55)" text-anchor="middle" letter-spacing="8">· 占位照片 ·</text>
</svg>
`;
}
/**
* 脚本入口:遍历景点生成占位照片。
*
* @returns {Promise<void>} 完成后输出汇总。
*/
async function main() {
const force = process.argv.includes('--force');
mkdirSync(OUT_DIR, { recursive: true });
let created = 0;
let skipped = 0;
for (const spot of SPOTS) {
spot.photos.forEach((photo, index) => {
const fileName = photo.src.split('/').pop();
const outPath = resolve(OUT_DIR, fileName);
if (!force && existsSync(outPath)) {
skipped += 1;
return;
}
writeFileSync(outPath, buildSvg(spot, photo), 'utf8');
created += 1;
console.log(`[photos] 生成 ${fileName}`);
});
}
console.log(`[photos] 完成:新建 ${created} 张,跳过已存在 ${skipped} 张`);
}
main().catch((err) => {
console.error('[photos] 生成失败:', err);
process.exit(1);
});