完成项目基础搭建:配置 Vite 开发构建环境,集成 Phaser 3 与虚拟摇杆插件,实现地图漫游、景点触发、拍立得弹窗、进度存档等核心功能,添加占位资源与批量生成脚本,配置项目元数据与忽略规则。
117 lines
3.9 KiB
JavaScript
117 lines
3.9 KiB
JavaScript
/**
|
||
* @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('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>');
|
||
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);
|
||
});
|