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
+628 -45
View File
@@ -2,67 +2,650 @@
@tailwind components;
@tailwind utilities;
/* ==================== InfiPlot — low-fi prototype tokens ==================== */
:root {
--ink: #3a3a38;
--ink-soft: #6f6e69;
--ink-faint: #a9a7a0;
--line: #cfccc4;
--paper: #f3f1ec;
--paper-2: #e9e6df;
--fill: #ddd9d0;
--accent: #d4824a;
--jit: 1;
--sketch-filter: url(#s2);
}
@layer base {
html {
font-feature-settings: "ss01", "kern", "liga";
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: var(--paper);
}
body {
background-image:
radial-gradient(rgba(133, 79, 37, 0.025) 1px, transparent 1px),
radial-gradient(rgba(133, 79, 37, 0.018) 1px, transparent 1px);
background-size: 28px 28px, 38px 38px;
background-position: 0 0, 14px 19px;
background:
repeating-linear-gradient(0deg, transparent 0 38px, rgba(0, 0, 0, 0.018) 38px 39px),
var(--paper);
color: var(--ink);
font-family: "Noto Sans SC", "PingFang SC", system-ui, sans-serif;
}
::selection {
background-color: rgb(217 122 46 / 0.28);
color: #2d1810;
}
textarea::placeholder {
color: rgb(168 105 59 / 0.45);
background-color: rgb(212 130 74 / 0.28);
color: var(--ink);
}
}
@layer utilities {
.hairline {
background-image: linear-gradient(
to right,
transparent,
rgba(45, 24, 16, 0.18) 18%,
rgba(45, 24, 16, 0.18) 82%,
transparent
);
height: 1px;
}
.hairline-full {
height: 1px;
background: rgba(45, 24, 16, 0.14);
}
.num {
font-variant-numeric: tabular-nums lining-nums;
}
.smallcaps {
text-transform: uppercase;
letter-spacing: 0.32em;
.latin {
font-family: "Patrick Hand", "Caveat", "Cormorant Garamond", cursive;
}
}
@keyframes yume-ripple {
0% {
width: 14px;
height: 14px;
opacity: 0.95;
}
100% {
width: 110px;
height: 110px;
opacity: 0;
}
/* ==================== hand-drawn frame ==================== */
.frame {
position: absolute;
inset: 0;
border: 2px solid var(--ink);
border-radius: 12px;
filter: var(--sketch-filter);
background: transparent;
pointer-events: none;
}
.frame.soft {
border-color: var(--ink-soft);
}
/* ==================== logo ==================== */
.ip-logo {
display: inline-flex;
align-items: center;
gap: 12px;
}
.ip-logo .mark {
position: relative;
width: 30px;
height: 30px;
}
.ip-logo .mark .frame {
border-radius: 50%;
}
.ip-logo .mark span {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-size: 15px;
color: var(--ink-soft);
}
.ip-logo .word {
font-family: "Patrick Hand", "Caveat", cursive;
font-size: 24px;
letter-spacing: 2px;
color: var(--ink);
font-weight: 400;
}
/* ==================== input bar (prompt + start) ==================== */
.ip-tagline {
font-size: clamp(22px, 2.6vw, 33px);
color: var(--ink);
font-weight: 500;
letter-spacing: 1px;
text-align: center;
}
.ip-bar {
display: flex;
gap: 16px;
align-items: stretch;
width: min(1100px, 92vw);
height: 68px;
}
.ip-field {
position: relative;
flex: 1;
}
.ip-field .frame {
border-radius: 16px;
}
.ip-field input {
position: relative;
width: 100%;
height: 100%;
padding: 0 28px;
background: transparent;
border: none;
outline: none;
font: inherit;
font-size: 20px;
color: var(--ink);
z-index: 1;
}
.ip-field input::placeholder {
color: transparent;
}
.ip-field .ph {
position: absolute;
inset: 0;
display: flex;
align-items: center;
padding: 0 28px;
font-size: 20px;
color: var(--ink-faint);
font-weight: 300;
white-space: nowrap;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
.ip-cursor {
display: inline-block;
width: 2px;
height: 23px;
background: var(--ink-faint);
margin: 0 1px 0 3px;
vertical-align: -4px;
animation: ip-blink 1.1s steps(1) infinite;
}
@keyframes ip-blink {
50% { opacity: 0; }
}
.ip-start {
position: relative;
width: 176px;
flex: none;
display: grid;
place-items: center;
cursor: pointer;
border: none;
background: transparent;
padding: 0;
}
.ip-start .frame {
border-radius: 16px;
background: var(--accent);
border-color: var(--accent);
z-index: 0;
}
.ip-start span {
position: relative;
z-index: 1;
color: #fff;
font-size: 22px;
letter-spacing: 6px;
font-weight: 500;
padding-left: 6px;
}
.ip-start:disabled {
opacity: 0.55;
cursor: not-allowed;
}
/* ==================== collapsible category pills ==================== */
.ip-cat {
position: relative;
}
.ip-catbtn {
position: relative;
height: 42px;
padding: 0 16px;
display: flex;
align-items: center;
gap: 9px;
cursor: pointer;
white-space: nowrap;
border: none;
background: transparent;
}
.ip-catbtn .frame {
border-radius: 21px;
border-color: var(--line);
}
.ip-catname {
position: relative;
z-index: 2;
font-size: 12.5px;
color: var(--ink-faint);
}
.ip-catval {
position: relative;
z-index: 2;
font-size: 15px;
color: var(--ink);
font-weight: 600;
}
.ip-caret {
position: relative;
z-index: 2;
font-size: 11px;
color: var(--ink-soft);
transition: transform 0.15s;
}
.ip-cat.open .ip-caret {
transform: rotate(180deg);
}
.ip-cat.open .ip-catbtn .frame {
border-color: var(--accent);
}
.ip-cat.open .ip-catval {
color: var(--accent);
}
.ip-catmenu {
position: absolute;
top: 50px;
left: 0;
min-width: calc(100% + 8px);
padding: 7px;
z-index: 20;
display: flex;
flex-direction: column;
gap: 2px;
background: var(--paper);
}
.ip-catmenu .frame {
border-radius: 12px;
border-color: var(--ink-soft);
}
.ip-catopt {
position: relative;
z-index: 1;
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
color: var(--ink-soft);
cursor: pointer;
white-space: nowrap;
background: transparent;
border: none;
text-align: left;
font: inherit;
}
.ip-catopt:hover {
background: var(--paper-2);
}
.ip-catopt.on {
color: var(--accent);
font-weight: 600;
}
.ip-catopt.on::after {
content: "\2713";
margin-left: 8px;
font-size: 12px;
}
/* ==================== scattered story cards ==================== */
.ip-card {
position: absolute;
cursor: pointer;
transition: transform 0.25s ease;
}
.ip-card:hover {
transform: rotate(0deg) translateY(-4px) !important;
}
.ip-card .inner {
position: absolute;
inset: 0;
border-radius: 12px;
overflow: hidden;
}
.ip-card .img {
position: absolute;
inset: 0;
background: var(--fill);
display: grid;
place-items: center;
}
.ip-card .img svg {
width: 40%;
max-width: 120px;
opacity: 0.55;
}
.ip-card .img img.card-photo {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.ip-card .frame {
z-index: 3;
}
/* hover overlay: bottom-up dark gradient with title + outline */
.ip-hover {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 16px 18px 16px;
color: #fff;
background: linear-gradient(to top, rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0.45) 45%, rgba(0, 0, 0, 0) 100%);
opacity: 0;
transition: opacity 0.3s ease;
user-select: none;
border-radius: 12px;
}
.ip-card:hover .ip-hover {
opacity: 1;
}
.ip-hover-title {
margin: 0 0 6px;
font-size: 16px;
font-weight: 700;
letter-spacing: 0.08em;
color: #fff;
}
.ip-hover-outline {
margin: 0;
font-size: 12px;
line-height: 1.55;
color: rgba(231, 226, 215, 0.92);
font-style: italic;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* ==================== gallery masonry ==================== */
.ip-gallery {
width: min(1640px, 96vw);
margin: 14px auto 0;
column-count: 4;
column-gap: 24px;
}
@media (max-width: 1100px) {
.ip-gallery { column-count: 3; }
}
@media (max-width: 780px) {
.ip-gallery { column-count: 2; }
}
@media (max-width: 480px) {
.ip-gallery { column-count: 1; }
}
.ip-card.gcard {
position: relative;
width: 100%;
display: inline-block;
margin: 0 0 24px;
break-inside: avoid;
transform: rotate(calc(var(--gr, 0deg) * var(--jit)));
}
.ip-sectionnote {
width: 100%;
margin: 34px auto 6px;
text-align: center;
font-family: "Patrick Hand", "Caveat", cursive;
font-size: 19px;
color: var(--accent);
}
.ip-sectionnote .arr {
display: block;
font-size: 22px;
line-height: 1;
margin-bottom: 2px;
}
/* ==================== project intro ==================== */
.ip-intro {
position: relative;
width: min(1500px, 94vw);
margin: 70px auto 110px;
padding: 58px clamp(28px, 5vw, 80px) 64px;
}
.ip-intro .frame {
border-radius: 16px;
border-color: var(--ink-soft);
}
.ip-intro .imark {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 6px;
}
.ip-intro .imark .gl {
width: 34px;
height: 34px;
display: grid;
place-items: center;
font-size: 18px;
color: var(--ink-soft);
border: 2px solid var(--ink-soft);
border-radius: 50%;
}
.ip-intro h2 {
position: relative;
z-index: 1;
margin: 0;
font-family: "Patrick Hand", "Caveat", cursive;
font-size: 38px;
letter-spacing: 1px;
color: var(--ink);
font-weight: 400;
}
.ip-intro .kicker {
position: relative;
z-index: 1;
font-size: 15px;
color: var(--ink-soft);
letter-spacing: 3px;
margin: 2px 0 26px;
font-family: "Patrick Hand", "Caveat", cursive;
}
.ip-intro p {
position: relative;
z-index: 1;
max-width: 1180px;
font-size: 18px;
line-height: 1.9;
color: var(--ink-soft);
margin: 0 0 18px;
font-weight: 300;
text-wrap: pretty;
}
.ip-intro .label {
position: relative;
z-index: 1;
font-size: 13px;
color: var(--ink-faint);
letter-spacing: 2px;
margin: 26px 0 8px;
font-weight: 500;
}
.ip-intro b {
color: var(--ink);
font-weight: 600;
}
.ip-intro .mail {
color: var(--accent);
font-weight: 500;
}
/* ==================== style picker modal ==================== */
.ip-modal-ov {
position: fixed;
inset: 0;
z-index: 60;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
background: rgba(40, 38, 34, 0);
backdrop-filter: blur(0px);
-webkit-backdrop-filter: blur(0px);
transition: background 0.28s ease, backdrop-filter 0.28s ease, -webkit-backdrop-filter 0.28s ease;
}
.ip-modal-ov.show {
background: rgba(40, 38, 34, 0.34);
backdrop-filter: blur(7px);
-webkit-backdrop-filter: blur(7px);
}
.ip-modal {
position: relative;
width: 1120px;
max-width: 94vw;
max-height: 88vh;
display: flex;
flex-direction: column;
background: var(--paper);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.32);
transform: scale(0.92);
opacity: 0;
transition: transform 0.3s cubic-bezier(0.2, 0.82, 0.25, 1), opacity 0.24s ease;
}
.ip-modal-ov.show .ip-modal {
transform: scale(1);
opacity: 1;
}
.ip-modal .frame {
border-radius: 16px;
border-color: var(--ink-soft);
z-index: 0;
}
.ip-modal-hd {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 20px;
padding: 22px 26px;
border-bottom: 1.5px dashed var(--line);
}
.ip-modal-ttl {
display: flex;
flex-direction: column;
font-size: 22px;
font-weight: 600;
color: var(--ink);
white-space: nowrap;
}
.ip-modal-sub {
font-size: 13px;
font-weight: 400;
color: var(--ink-faint);
margin-top: 3px;
}
.ip-modal-search {
position: relative;
margin-left: auto;
width: 320px;
max-width: 46vw;
}
.ip-modal-search input {
width: 100%;
height: 42px;
padding: 0 40px 0 16px;
border-radius: 21px;
border: 1.5px solid var(--line);
background: var(--paper-2);
font: inherit;
font-size: 15px;
color: var(--ink);
outline: none;
}
.ip-modal-search input::placeholder {
color: var(--ink-faint);
}
.ip-modal-search .si {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
color: var(--ink-faint);
font-size: 18px;
pointer-events: none;
}
.ip-modal-x {
font-size: 28px;
color: var(--ink-soft);
cursor: pointer;
line-height: 1;
padding: 0 2px;
background: transparent;
border: none;
}
.ip-modal-grid {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 18px;
padding: 24px 26px 28px;
overflow-y: auto;
}
@media (max-width: 780px) {
.ip-modal-grid { grid-template-columns: repeat(2, 1fr); }
}
.ip-scard {
position: relative;
cursor: pointer;
}
.ip-scard .sthumb {
position: relative;
height: 160px;
background: var(--fill);
display: grid;
place-items: center;
border-radius: 11px;
overflow: hidden;
}
.ip-scard .sthumb svg {
width: 32%;
max-width: 84px;
opacity: 0.5;
}
.ip-scard .sthumb::after {
content: "";
position: absolute;
inset: 0;
border-radius: 11px;
border: 2px solid transparent;
}
.ip-scard.on .sthumb::after {
border-color: var(--accent);
}
.ip-scard .sname {
text-align: center;
padding: 10px 4px 2px;
font-size: 15px;
color: var(--ink);
}
.ip-scard.on .sname {
color: var(--accent);
font-weight: 600;
}
.ip-noresult {
grid-column: 1 / -1;
text-align: center;
color: var(--ink-faint);
padding: 48px 0;
font-size: 15px;
}
/* ==================== avatar (bottom-left of hero) ==================== */
.ip-avatar {
position: relative;
width: 46px;
height: 46px;
}
.ip-avatar .frame {
border-radius: 50%;
border-color: var(--ink-soft);
}
.ip-avatar span {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-family: "Patrick Hand", "Caveat", cursive;
font-size: 22px;
color: var(--ink-soft);
}