136ceff69f
Rebuilds the landing page from the prototype: 1900px scale-to-fit hero with hand-drawn SVG-jitter frames, typewriter input + start button, 5 horizontal collapsible category selectors (with style-picker modal), 7 scattered hero cards over a 16-card masonry gallery, and project intro panel. Each card is filled with a Runware FLUX.2 image, pre-generated and stored as WebP (~2 MB total for 30 cards). Hero card content + image switches by 性向 (男性向 / 女性向); gallery stays shared. Hover overlay on every card shows title + outline in a bottom-up dark gradient, matching the prior homepage's interaction style. Bug fixes uncovered by tracing the form-state → engine pipeline: - 「语音配音:关闭」was previously stuffed into styleGuide (consumed only by FLUX, ignored by TTS). Now serialized as audioEnabled boolean in the sessionStorage payload; play page's fetchBeatAudio early-returns when false, so no /api/beat-audio request fires. - 「绘画风格:自动」used to pass the literal Chinese phrase "由模型根据 prompt 自动判断画风" to FLUX, which painted it as text. Now maps to the 二次元/galgame default prompt. Adds reusable scripts under apps/web/scripts/: - generate-home-images.mjs — Runware FLUX.2 idempotent batch generator - optimize-home-images.mjs — sharp WebP downscale + recompress Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
43 lines
1.6 KiB
JavaScript
43 lines
1.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Compresses the freshly generated apps/web/public/home/*.png into much
|
|
* smaller .webp files alongside them, then deletes the originals.
|
|
* Output webps target ~1200px on the long edge and quality 78.
|
|
*/
|
|
|
|
import { readdirSync, statSync, unlinkSync } from "node:fs";
|
|
import { resolve, dirname, extname, basename } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import sharp from "sharp";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const DIR = resolve(__dirname, "..", "public", "home");
|
|
|
|
const MAX_EDGE = 1200;
|
|
const QUALITY = 78;
|
|
|
|
const files = readdirSync(DIR).filter((f) => f.toLowerCase().endsWith(".png"));
|
|
let totalIn = 0;
|
|
let totalOut = 0;
|
|
|
|
for (const f of files) {
|
|
const inPath = resolve(DIR, f);
|
|
const outPath = resolve(DIR, basename(f, extname(f)) + ".webp");
|
|
const inSize = statSync(inPath).size;
|
|
totalIn += inSize;
|
|
|
|
const img = sharp(inPath);
|
|
const meta = await img.metadata();
|
|
const longEdge = Math.max(meta.width ?? 0, meta.height ?? 0);
|
|
const resized = longEdge > MAX_EDGE ? img.resize({ width: meta.width >= meta.height ? MAX_EDGE : undefined, height: meta.height > meta.width ? MAX_EDGE : undefined }) : img;
|
|
await resized.webp({ quality: QUALITY, effort: 5 }).toFile(outPath);
|
|
const outSize = statSync(outPath).size;
|
|
totalOut += outSize;
|
|
console.log(`${f.padEnd(16)} ${(inSize / 1024).toFixed(0).padStart(5)} KB → ${(outSize / 1024).toFixed(0).padStart(4)} KB`);
|
|
unlinkSync(inPath);
|
|
}
|
|
|
|
console.log(
|
|
`\nTotal: ${(totalIn / 1024 / 1024).toFixed(1)} MB → ${(totalOut / 1024 / 1024).toFixed(2)} MB`,
|
|
);
|