feat(web): InfiPlot low-fi homepage with AI-generated cards + gender-reactive hero + audio toggle fix

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>
This commit is contained in:
DESKTOP-I1T6TF3\Q
2026-06-01 16:55:55 +08:00
parent 774f3734fd
commit 136ceff69f
36 changed files with 1709 additions and 218 deletions
+22 -25
View File
@@ -1,25 +1,9 @@
import type { Metadata } from "next";
import { Cormorant_Garamond, Inter } from "next/font/google";
import "./globals.css";
const cormorant = Cormorant_Garamond({
subsets: ["latin"],
weight: ["300", "400", "500", "600"],
style: ["normal", "italic"],
variable: "--font-serif",
display: "swap",
});
const inter = Inter({
subsets: ["latin"],
weight: ["300", "400", "500"],
variable: "--font-sans",
display: "swap",
});
export const metadata: Metadata = {
title: "云梦 — AI 视觉小说",
description: "一部由 AI 实时绘制每一帧的开源视觉小说。",
title: "InfiPlot — AI 实时交互剧情游戏",
description: "InfiPlot 是一款用 AI 实时生成图片、语音与剧情分支的交互式剧情游戏 Demo。",
};
export default function RootLayout({
@@ -28,18 +12,31 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html
lang="zh-CN"
className={`${cormorant.variable} ${inter.variable}`}
suppressHydrationWarning
>
<html lang="zh-CN" suppressHydrationWarning>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
href="https://fonts.googleapis.com/css2?family=Patrick+Hand&family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap"
/>
</head>
<body className="bg-cream-50 text-clay-900 font-sans antialiased min-h-screen">
<body className="min-h-screen overflow-x-hidden">
{/* Hand-drawn jitter filters used by every .frame element */}
<svg width="0" height="0" style={{ position: "absolute" }} aria-hidden>
<filter id="s1">
<feTurbulence type="fractalNoise" baseFrequency="0.012" numOctaves="2" seed="7" result="n" />
<feDisplacementMap in="SourceGraphic" in2="n" scale="1.2" />
</filter>
<filter id="s2">
<feTurbulence type="fractalNoise" baseFrequency="0.016" numOctaves="2" seed="4" result="n" />
<feDisplacementMap in="SourceGraphic" in2="n" scale="2.6" />
</filter>
<filter id="s3">
<feTurbulence type="fractalNoise" baseFrequency="0.022" numOctaves="3" seed="11" result="n" />
<feDisplacementMap in="SourceGraphic" in2="n" scale="4.2" />
</filter>
</svg>
{children}
</body>
</html>