feat(web): revamp style modal UI with grid cards, thumbnails, and dual-view
Redesign the painting-style picker inspired by Pollo AI: widen modal to 1400px, show styles as square thumbnail cards in a 4-column grid with name labels below, add ember glow hover effect, and split custom-style editing into its own view. Simplify style names (e.g. "京阿尼细腻日常" → "京阿尼"), add 22 .webp preview thumbnails, and remove the per-preset override mechanism in favor of a cleaner grid + custom flow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@@ -54,20 +54,26 @@ const OPTS: Opt[] = [
|
||||
type StoryContent = { title: string; outline: string; style: string; tags: string[] };
|
||||
|
||||
const STYLE_MAP: Record<string, string> = {
|
||||
"古典厚涂油画": "Dark fantasy oil painting style, grand clockwork steampunk city built into a mountain range at twilight, immense gothic spires with glowing green lamps, complex gears and platforms. Richly detailed, impasto texture, dramatic academic lighting. Horizontal cinematic composition.",
|
||||
"极简中国水墨": "Minimalist Chinese ink wash style, vertical sea of clouds and distant jagged peaks. Ethereal, sparse composition with poetic brushstrokes, monochrome palette with subtle blue hints. Large blank mist area for copy space.",
|
||||
"浮世绘木刻": "Ukiyo-e woodblock print style, majestic waves and Mount Fuji visible through cherry branches. Bold outlines, flat colors with paper texture, ancient and mystical atmosphere.",
|
||||
"莫高窟壁画": "Dunhuang fresco style, celestial patterns, stylized lotus flowers and floating geometric patterns on an aged stucco wall. Muted, oxidized mineral colors, delicate line art, historical and divine ambiance.",
|
||||
"波斯细密画": "Persian miniature style, ornate vertical tiled garden pavilion surrounded by tall cypress trees and complex geometric mosaics. High detail, jewel-like colors, flattened perspective, decorative borders.",
|
||||
"吉卜力治愈手绘": "Ghibli hand-painted watercolor style, a vast wildflower meadow hill under a bright blue sky with fluffy clouds, a fantastical airship flying in the distance. Natural daylight, soft washes, nostalgic feel.",
|
||||
"京阿尼细腻日常": "KyoAni anime style, fine line art, warm indoor lighting contrasting the cool moonlight outside, rain streaks on a tall window. Deep emotional atmosphere, delicate light and shadow reflections.",
|
||||
"新海诚唯美光影": "Makoto Shinkai anime style, hyper-detailed, towering dramatic night starry sky with a descending comet trail, glowing cherry tree branches in the foreground. Brilliant lighting effects, vivid colors.",
|
||||
"Galgame CG": "High-quality Galgame CG illustration, dreamlike beach scene at sunset with sparkling waves rolling in. Pastel colors, bloom lighting, clean composition, soft focus.",
|
||||
"3D 动漫电影": "Cinematic 3D animated film style, a rustic wooden hangar at sunrise with volumetric lighting, warm golden hour colors, deep textures, cinematic composition.",
|
||||
"赛博朋克": "Cyberpunk anime style, cel-shaded animation, rainy night streets of a dense neon-drenched futuristic megacity with towering skyscrapers. Hard edges, high saturation, sharp contrast.",
|
||||
"蒸汽波": "Vaporwave aesthetic, anime style, a geometric pink grid floor leading to a palm tree silhouette, neon pink sunset over a purple ocean in the background. Glitch effects, retro pastel colors.",
|
||||
"哥特庄园": "Gothic romance illustration, desolate moonlit ruins of a grand gothic manor on a foggy cliff, misty atmosphere, melancholic blue and grey tones.",
|
||||
"废土科幻": "Post-apocalyptic landscape, vast desert wasteland with the rusted remains of an overgrown highway and ruined skyscrapers under a dusty orange sunset sky.",
|
||||
"京阿尼": "KyoAni anime style, fine line art, delicate light and shadow reflections, warm soft lighting, deep emotional atmosphere, subtle color gradients, clean detailed backgrounds.",
|
||||
"新海诚": "Makoto Shinkai anime style, hyper-detailed, brilliant lighting effects, vivid saturated colors, dramatic lens flare, golden hour glow, photorealistic backgrounds with anime characters.",
|
||||
"吉卜力": "Ghibli hand-painted watercolor style, soft washes, natural daylight, warm nostalgic feel, gentle rounded forms, rich earthy palette, whimsical and serene atmosphere.",
|
||||
"3D 动画": "Cinematic 3D animated film style, volumetric lighting, deep textures, cinematic composition, warm color grading, subsurface scattering on skin, Pixar-quality rendering.",
|
||||
"赛博朋克": "Cyberpunk anime style, cel-shaded rendering, neon-lit color palette, hard edges, high saturation, sharp contrast, dark atmosphere with vivid accent lighting.",
|
||||
"哥特": "Gothic romance illustration, misty atmosphere, melancholic blue and grey tones, dramatic chiaroscuro, ornate dark aesthetic, moonlit cold lighting.",
|
||||
"废土": "Post-apocalyptic illustration, muted dusty color palette, weathered rough textures, hazy amber lighting, desolate atmosphere, rusted and overgrown aesthetic.",
|
||||
"像素风": "Pixel art style, retro 16-bit color palette, blocky geometric shapes, nostalgic video game aesthetic, clean pixel edges, vibrant limited palette.",
|
||||
"真实": "Photorealistic style, natural lighting, cinematic depth of field, film grain texture, lifelike skin tones, realistic material rendering, subtle color grading.",
|
||||
"古典油画": "Classical oil painting style, richly detailed, impasto texture, dramatic academic lighting, Renaissance composition, warm chiaroscuro tones.",
|
||||
"水彩": "Watercolor illustration, wet-on-wet washes, soft bleeding edges, paper texture visible, gentle pastel tones, dreamy translucent layers.",
|
||||
"水墨": "Minimalist Chinese ink wash style, ethereal sparse composition with poetic brushstrokes, monochrome palette with subtle blue hints, large blank mist areas.",
|
||||
"莫奈": "Impressionist painting in the style of Claude Monet, visible brushstrokes, vibrant dappled light, soft focus, rich natural colors, plein-air atmosphere.",
|
||||
"彩铅": "Colored pencil drawing style, fine crosshatch strokes, warm paper texture, soft blended gradients, hand-drawn warmth, delicate shading.",
|
||||
"黑白漫画": "Black and white manga style, bold ink lines, high contrast, dramatic shadows, halftone dot patterns, cinematic panel composition.",
|
||||
"浮世绘": "Ukiyo-e woodblock print style, bold outlines, flat colors with paper texture, ancient and mystical atmosphere.",
|
||||
"儿童绘本": "Children's book illustration style, soft rounded shapes, bright warm colors, clean cute line art, whimsical cheerful atmosphere.",
|
||||
"手绘素描": "Hand-drawn pencil sketch style, natural pencil strokes, paper texture, loose expressive composition, raw unfinished aesthetic.",
|
||||
"儿童涂鸦": "Child's crayon drawing style, naive clumsy strokes, bold vivid colors, free perspective, playful uninhibited composition.",
|
||||
"黏土手工": "Claymation stop-motion style, soft rounded sculpted forms, handmade clay texture, warm diffused lighting, miniature scene atmosphere.",
|
||||
};
|
||||
|
||||
/* 每个性向 24 篇预设剧情(与封面 /home/{m|f}{i}.webp 按索引一一对应)。
|
||||
@@ -157,7 +163,7 @@ const STORIES: Record<Gender, StoryContent[]> = {
|
||||
{
|
||||
"title": "社团存亡日",
|
||||
"outline": "濒临废部的动画社,唯一社员是总在睡觉的怪人。新来的转校生社长发现,只要完成怪人的“日常委托”,社员就会增加一人,而这些人,都来自被遗忘的动画世界。",
|
||||
"style": "京阿尼细腻日常 (Image 5参考)",
|
||||
"style": "京阿尼 (Image 5参考)",
|
||||
"tags": [
|
||||
"日常",
|
||||
"奇幻",
|
||||
@@ -167,7 +173,7 @@ const STORIES: Record<Gender, StoryContent[]> = {
|
||||
{
|
||||
"title": "黄昏归途",
|
||||
"outline": "他总在黄昏时分,于空无一人的车站遇见少女。她带他穿越时间的缝隙,回到故乡被毁灭前的最后一天。每一次循环,他都必须在拯救她与拯救世界之间做出选择。",
|
||||
"style": "新海诚唯美光影 (Image 2参考)",
|
||||
"style": "新海诚 (Image 2参考)",
|
||||
"tags": [
|
||||
"时间循环",
|
||||
"恋爱",
|
||||
@@ -459,7 +465,7 @@ const STORIES: Record<Gender, StoryContent[]> = {
|
||||
{
|
||||
"title": "夏日未完待续",
|
||||
"outline": "她在文化祭前夜,与青梅竹马的学长在空教室许下约定。第二天醒来,时间永远停在了文化祭前一周。只有她保留记忆,为守护他的笑容,她一遍遍重演青春,试图改写那个令他心碎的结局。",
|
||||
"style": "京阿尼细腻日常 (Image 5参考)",
|
||||
"style": "京阿尼 (Image 5参考)",
|
||||
"tags": [
|
||||
"时间循环",
|
||||
"青春",
|
||||
@@ -469,7 +475,7 @@ const STORIES: Record<Gender, StoryContent[]> = {
|
||||
{
|
||||
"title": "星之轨迹",
|
||||
"outline": "她总在雨天,于旧书店遇见来自未来的他。他说她是拯救未来的关键,赠予她能看到“命运线”的能力。当她终于能看清两人的轨迹,却发现他来自的时间线,正因她的存在而崩塌。",
|
||||
"style": "新海诚唯美光影 (Image 2参考)",
|
||||
"style": "新海诚 (Image 2参考)",
|
||||
"tags": [
|
||||
"穿越",
|
||||
"科幻",
|
||||
@@ -865,8 +871,6 @@ function StyleModal({
|
||||
onClose,
|
||||
customStyleGuide,
|
||||
setCustomStyleGuide,
|
||||
styleOverrides,
|
||||
setStyleOverrides,
|
||||
customStyleRefImage,
|
||||
setCustomStyleRefImage,
|
||||
}: {
|
||||
@@ -876,62 +880,68 @@ function StyleModal({
|
||||
onClose: () => void;
|
||||
customStyleGuide: string;
|
||||
setCustomStyleGuide: (s: string) => void;
|
||||
styleOverrides: Record<string, string>;
|
||||
setStyleOverrides: (o: Record<string, string>) => void;
|
||||
customStyleRefImage: string;
|
||||
setCustomStyleRefImage: (s: string) => void;
|
||||
}) {
|
||||
const [q, setQ] = useState("");
|
||||
const [shown, setShown] = useState(false);
|
||||
// Inline editing:editingIdx === i 时该卡片的 prompt 框变成可编辑 textarea。
|
||||
// 列表保持原位(不跳新页面),其他卡片继续可见——用户随时可以取消并切到别处。
|
||||
const [editingIdx, setEditingIdx] = useState<number | null>(null);
|
||||
const [view, setView] = useState<"grid" | "custom">("grid");
|
||||
const [draft, setDraft] = useState("");
|
||||
// 上传 / 解析参考图的瞬时状态——失败/进行中提示只在此次弹窗内可见。
|
||||
const [parsing, setParsing] = useState(false);
|
||||
const [parseError, setParseError] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const STYLE_THUMB: Record<string, string> = {
|
||||
"自动": "/home/styles/auto.webp",
|
||||
"自定义风格": "/home/styles/custom.webp",
|
||||
"京阿尼": "/home/styles/kyoani.webp",
|
||||
"新海诚": "/home/styles/shinkai.webp",
|
||||
"吉卜力": "/home/styles/ghibli.webp",
|
||||
"3D 动画": "/home/styles/3d.webp",
|
||||
"赛博朋克": "/home/styles/cyberpunk.webp",
|
||||
"哥特": "/home/styles/gothic.webp",
|
||||
"废土": "/home/styles/wasteland.webp",
|
||||
"像素风": "/home/styles/pixel.webp",
|
||||
"真实": "/home/styles/real.webp",
|
||||
"古典油画": "/home/styles/oil.webp",
|
||||
"莫奈": "/home/styles/monet.webp",
|
||||
"水彩": "/home/styles/watercolor.webp",
|
||||
"水墨": "/home/styles/ink.webp",
|
||||
"浮世绘": "/home/styles/ukiyoe.webp",
|
||||
"彩铅": "/home/styles/pencil.webp",
|
||||
"手绘素描": "/home/styles/sketch.webp",
|
||||
"黑白漫画": "/home/styles/manga.webp",
|
||||
"儿童绘本": "/home/styles/children.webp",
|
||||
"儿童涂鸦": "/home/styles/crayon.webp",
|
||||
"黏土手工": "/home/styles/clay.webp",
|
||||
};
|
||||
useEffect(() => {
|
||||
const id = requestAnimationFrame(() => setShown(true));
|
||||
return () => cancelAnimationFrame(id);
|
||||
}, []);
|
||||
const closeRef = useRef<() => void>(null);
|
||||
const close = () => {
|
||||
setShown(false);
|
||||
setTimeout(onClose, 280);
|
||||
};
|
||||
const startEditing = (i: number, currentPrompt: string) => {
|
||||
setEditingIdx(i);
|
||||
setDraft(currentPrompt);
|
||||
closeRef.current = close;
|
||||
useEffect(() => {
|
||||
const h = (e: KeyboardEvent) => { if (e.key === "Escape") closeRef.current?.(); };
|
||||
document.addEventListener("keydown", h);
|
||||
return () => document.removeEventListener("keydown", h);
|
||||
}, []);
|
||||
const customIdx = items.indexOf("自定义风格");
|
||||
const openCustomView = (prefill: string) => {
|
||||
setDraft(prefill);
|
||||
setView("custom");
|
||||
};
|
||||
const cancelEditing = () => {
|
||||
setEditingIdx(null);
|
||||
setDraft("");
|
||||
};
|
||||
const saveEditing = () => {
|
||||
if (editingIdx === null) return;
|
||||
const targetName = items[editingIdx];
|
||||
const saveCustom = () => {
|
||||
const t = draft.trim();
|
||||
if (!targetName || !t) return;
|
||||
if (targetName === "自定义") {
|
||||
setCustomStyleGuide(t);
|
||||
} else {
|
||||
// STYLE_MAP 这个 source-of-truth 不动;只往 in-memory overrides 写一条。
|
||||
setStyleOverrides({ ...styleOverrides, [targetName]: t });
|
||||
}
|
||||
onPick(editingIdx);
|
||||
setEditingIdx(null);
|
||||
if (!t) return;
|
||||
setCustomStyleGuide(t);
|
||||
if (customIdx >= 0) onPick(customIdx);
|
||||
close();
|
||||
};
|
||||
const resetOverride = (name: string) => {
|
||||
const next = { ...styleOverrides };
|
||||
delete next[name];
|
||||
setStyleOverrides(next);
|
||||
setDraft(STYLE_MAP[name] ?? "");
|
||||
};
|
||||
|
||||
// 客户端把上传的图片缩到 512px 长边 + webp(0.85),base64 通常落在 30-80KB。
|
||||
// 必须客户端做:(1) 上传 / 后续 /api/scene 都会带这串,包不能太大;
|
||||
// (2) Runware referenceImages 支持 base64,无需另外加 upload 端点。
|
||||
const resizeImageToDataUrl = async (file: File): Promise<string> => {
|
||||
const dataUrl = await new Promise<string>((resolve, reject) => {
|
||||
const r = new FileReader();
|
||||
@@ -955,7 +965,6 @@ function StyleModal({
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) throw new Error("Canvas 2D context unavailable");
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
// webp 比 jpeg 体积更小一些;浏览器全支持。降级到 jpeg 作为兜底。
|
||||
let out = canvas.toDataURL("image/webp", 0.85);
|
||||
if (!out.startsWith("data:image/webp")) {
|
||||
out = canvas.toDataURL("image/jpeg", 0.85);
|
||||
@@ -982,8 +991,6 @@ function StyleModal({
|
||||
throw new Error(j.error ?? `${res.status}`);
|
||||
}
|
||||
const data = (await res.json()) as { stylePrompt: string };
|
||||
// 收到 AI 解析后的 prompt → 覆盖正在编辑的 draft + 持久化参考图。
|
||||
// 用户事后还可以手动改 draft(仍是 textarea)。
|
||||
setDraft(data.stylePrompt);
|
||||
setCustomStyleRefImage(resized);
|
||||
track("style_image_upload", { ok: true });
|
||||
@@ -1000,29 +1007,12 @@ function StyleModal({
|
||||
setCustomStyleRefImage("");
|
||||
setParseError(null);
|
||||
};
|
||||
// 标题取去掉括号后缀的"主名"——括号里的英文 / 「Image N参考」之类的脚注
|
||||
// 在标题位上显示噪声太大,挪到下方 prompt 行也已经覆盖到了。两种括号都
|
||||
// 兼容(中文「()」和英文「()」)。
|
||||
const stripSuffix = (s: string) => s.replace(/\s*[((].*?[))]\s*$/, "");
|
||||
|
||||
const q2 = q.trim();
|
||||
const list = items
|
||||
.map((name, i) => {
|
||||
const base = STYLE_MAP[name] ?? "";
|
||||
const override = styleOverrides[name];
|
||||
return {
|
||||
name,
|
||||
title: stripSuffix(name),
|
||||
// 列表里展示的是「有效 prompt」——优先 override,让用户看到自己改过的版本
|
||||
prompt: override ?? base,
|
||||
hasOverride: typeof override === "string" && override.length > 0,
|
||||
i,
|
||||
};
|
||||
})
|
||||
.filter((x) => {
|
||||
if (!q2) return true;
|
||||
const hay = (x.title + " " + x.name + " " + x.prompt).toLowerCase();
|
||||
return hay.includes(q2.toLowerCase());
|
||||
});
|
||||
const list = items.map((name, i) => ({ name, i })).filter((x) => {
|
||||
if (!q2) return true;
|
||||
return x.name.toLowerCase().includes(q2.toLowerCase());
|
||||
});
|
||||
return (
|
||||
<div
|
||||
onMouseDown={close}
|
||||
@@ -1034,27 +1024,43 @@ function StyleModal({
|
||||
<div
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
className={
|
||||
"flex w-[860px] max-w-[94vw] max-h-[86vh] flex-col overflow-hidden rounded-sm border border-clay-900/15 bg-cream-50 shadow-2xl shadow-clay-900/25 transition-all duration-300 " +
|
||||
"flex w-[1400px] max-w-[94vw] max-h-[86vh] flex-col overflow-hidden rounded-sm border border-clay-900/15 bg-cream-50 shadow-2xl shadow-clay-900/25 transition-all duration-300 " +
|
||||
(shown ? "opacity-100 scale-100" : "opacity-0 scale-95")
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-5 px-6 md:px-8 py-5 border-b border-clay-900/10">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-serif text-xl md:text-2xl text-clay-900">选择绘画风格</span>
|
||||
<span className="text-[11px] text-clay-500 mt-1 tracking-wide">
|
||||
默认「自动」· 点 prompt 框旁的 ✎ 可在该风格基础上修改(默认 prompt 不会被覆盖)
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative ml-auto w-[280px] max-w-[46vw]">
|
||||
<input
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
placeholder="搜索风格 / prompt…"
|
||||
autoFocus
|
||||
className="h-10 w-full rounded-sm border border-clay-900/15 bg-cream-100 pl-4 pr-10 font-sans text-sm text-clay-900 outline-none transition-colors focus:border-ember-500 placeholder:text-clay-400"
|
||||
/>
|
||||
<i className="fa-solid fa-magnifying-glass absolute right-3.5 top-1/2 -translate-y-1/2 text-sm text-clay-400 pointer-events-none" />
|
||||
</div>
|
||||
{view === "custom" ? (
|
||||
<div className="flex flex-1 items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView("grid")}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-sm text-clay-500 hover:bg-cream-100 hover:text-clay-900 transition-colors"
|
||||
aria-label="返回"
|
||||
>
|
||||
<i className="fa-solid fa-arrow-left text-sm" />
|
||||
</button>
|
||||
<span className="font-serif text-xl md:text-2xl text-clay-900">自定义风格</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-1 flex-col">
|
||||
<span className="font-serif text-xl md:text-2xl text-clay-900">选择绘画风格</span>
|
||||
<span className="text-[11px] text-clay-500 mt-1 tracking-wide">
|
||||
默认「自动」· 由 AI 根据故事自动匹配画风;选择「自定义风格」可输入描述或上传参考图
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative w-[280px] max-w-[46vw]">
|
||||
<input
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
placeholder="搜索风格…"
|
||||
autoFocus
|
||||
className="h-10 w-full rounded-sm border border-clay-900/15 bg-cream-100 pl-4 pr-10 font-sans text-sm text-clay-900 outline-none transition-colors focus:border-ember-500 placeholder:text-clay-400"
|
||||
/>
|
||||
<i className="fa-solid fa-magnifying-glass absolute right-3.5 top-1/2 -translate-y-1/2 text-sm text-clay-400 pointer-events-none" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={close}
|
||||
@@ -1064,300 +1070,187 @@ function StyleModal({
|
||||
<i className="fa-solid fa-xmark" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 overflow-y-auto px-4 py-5 md:px-6 md:py-6">
|
||||
{list.map(({ name, title, prompt, hasOverride, i }) => {
|
||||
const isCustom = name === "自定义";
|
||||
const selected = i === value;
|
||||
const editable = isCustom || Boolean(STYLE_MAP[name]);
|
||||
const isEditing = editingIdx === i;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
onClick={(e) => {
|
||||
// 编辑态下:让点击事件落在 textarea/按钮上即可,不要冒泡触发"选中关闭"。
|
||||
// 非编辑态下:点卡片选中此风格(自定义项点卡片直接进编辑)。
|
||||
if (isEditing) return;
|
||||
const tag = (e.target as HTMLElement).tagName;
|
||||
if (tag === "BUTTON" || tag === "TEXTAREA" || tag === "I") return;
|
||||
if (isCustom) {
|
||||
startEditing(i, customStyleGuide);
|
||||
} else {
|
||||
onPick(i);
|
||||
close();
|
||||
}
|
||||
|
||||
{view === "custom" ? (
|
||||
<div className="flex flex-col gap-4 overflow-y-auto px-6 py-6 md:px-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const f = e.target.files?.[0];
|
||||
if (f) handleUploadStyleImage(f);
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
}}
|
||||
className={
|
||||
"flex items-start gap-4 rounded-sm border px-3 py-3 md:px-4 md:py-3.5 text-left transition-all " +
|
||||
(isEditing
|
||||
? "border-ember-500 bg-cream-50 cursor-default"
|
||||
: selected
|
||||
? "border-ember-500 bg-ember-500/5 cursor-pointer"
|
||||
: "border-clay-900/12 hover:border-clay-900/35 hover:bg-cream-100 cursor-pointer")
|
||||
}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
/>
|
||||
{customStyleRefImage ? (
|
||||
<div className="flex items-center gap-3 rounded-sm border border-clay-900/12 bg-cream-100 px-3 py-2.5">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={customStyleRefImage}
|
||||
alt="画风参考图"
|
||||
className="h-14 w-14 shrink-0 rounded-sm border border-clay-900/10 object-cover"
|
||||
/>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<span className="font-sans text-[12px] text-clay-900">
|
||||
<i className="fa-solid fa-check mr-1.5 text-ember-500" />
|
||||
参考图已上传
|
||||
</span>
|
||||
<span className="font-sans text-[11px] leading-snug text-clay-500">
|
||||
AI 已解析为下方风格描述;每一幕画师都会参考这张图
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={parsing}
|
||||
className="font-sans text-[11px] text-clay-500 hover:text-ember-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
换一张
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeStyleRefImage()}
|
||||
className="font-sans text-[11px] text-clay-400 hover:text-clay-900 transition-colors"
|
||||
>
|
||||
移除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={parsing}
|
||||
className={
|
||||
"flex h-12 w-12 shrink-0 items-center justify-center rounded-sm border text-base " +
|
||||
(isCustom
|
||||
? "border-ember-500/40 bg-ember-500/10 text-ember-500"
|
||||
: "border-clay-900/10 bg-cream-100 text-clay-400")
|
||||
"flex items-center justify-center gap-2 rounded-sm border border-dashed px-3 py-2.5 font-sans text-[12px] transition-colors " +
|
||||
(parsing
|
||||
? "border-clay-900/15 bg-cream-100 text-clay-400 cursor-wait"
|
||||
: "border-clay-900/25 text-clay-700 hover:border-ember-500 hover:bg-ember-500/5 hover:text-ember-500")
|
||||
}
|
||||
>
|
||||
<i
|
||||
className={
|
||||
isCustom ? "fa-solid fa-pen-to-square" : "fa-regular fa-image"
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
{/* 标题(标题永远不可编辑) */}
|
||||
<span
|
||||
className={
|
||||
"font-serif text-base md:text-lg leading-snug flex items-center gap-2 " +
|
||||
(selected || isEditing ? "text-ember-500" : "text-clay-900")
|
||||
}
|
||||
>
|
||||
{isCustom ? "自定义 prompt" : title}
|
||||
{hasOverride && !isEditing && (
|
||||
<span
|
||||
className="rounded-sm border border-ember-500/40 bg-ember-500/10 px-1.5 py-0.5 font-sans text-[10px] tracking-wide text-ember-500"
|
||||
title="你修改过这条 prompt(仅本次会话生效,默认 prompt 不变)"
|
||||
>
|
||||
已改
|
||||
</span>
|
||||
)}
|
||||
{isCustom && customStyleRefImage && !isEditing && (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 rounded-sm border border-ember-500/40 bg-ember-500/10 px-1.5 py-0.5 font-sans text-[10px] tracking-wide text-ember-500"
|
||||
title="参考图已附带——每一幕画师都会参考这张图"
|
||||
>
|
||||
<i className="fa-regular fa-image text-[9px]" />
|
||||
附参考图
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{/* 「自动」语义就是「让 AI 自己判断画风」,没有 prompt 可显示也无从编辑;
|
||||
标题下方直接放一句解释,不渲染空文本框 / 铅笔。 */}
|
||||
{name === "自动" ? (
|
||||
<span className="font-sans text-[12px] md:text-[13px] leading-relaxed text-clay-500 mt-1">
|
||||
由 AI 依据世界观自动选择合适画风(无需手动指定 prompt)
|
||||
</span>
|
||||
) : /* prompt 区域:非编辑态是看起来像文本框的只读容器;编辑态是真的 textarea */
|
||||
isEditing ? (
|
||||
<div className="mt-1.5 flex flex-col gap-2">
|
||||
{/* 自定义卡专属:上传画风参考图。上传后会:(1) 用 vision LLM
|
||||
解析成 prompt 覆盖到下方 textarea;(2) 图片本身随会话送到
|
||||
画师,每幕都作为 reference 锚定画风。 */}
|
||||
{isCustom && (
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex flex-col gap-2"
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const f = e.target.files?.[0];
|
||||
if (f) handleUploadStyleImage(f);
|
||||
// reset 让同一文件重选能再次触发 onChange
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
}}
|
||||
/>
|
||||
{customStyleRefImage ? (
|
||||
<div className="flex items-center gap-3 rounded-sm border border-clay-900/12 bg-cream-100 px-3 py-2.5">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={customStyleRefImage}
|
||||
alt="画风参考图"
|
||||
className="h-14 w-14 shrink-0 rounded-sm border border-clay-900/10 object-cover"
|
||||
/>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<span className="font-sans text-[12px] text-clay-900">
|
||||
<i className="fa-solid fa-check mr-1.5 text-ember-500" />
|
||||
参考图已上传
|
||||
</span>
|
||||
<span className="font-sans text-[11px] leading-snug text-clay-500">
|
||||
AI 已解析为下方 prompt;每一幕画师都会参考这张图
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
fileInputRef.current?.click();
|
||||
}}
|
||||
disabled={parsing}
|
||||
className="font-sans text-[11px] text-clay-500 hover:text-ember-500 transition-colors disabled:opacity-50"
|
||||
>
|
||||
换一张
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeStyleRefImage();
|
||||
}}
|
||||
className="font-sans text-[11px] text-clay-400 hover:text-clay-900 transition-colors"
|
||||
>
|
||||
移除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
fileInputRef.current?.click();
|
||||
}}
|
||||
disabled={parsing}
|
||||
className={
|
||||
"flex items-center justify-center gap-2 rounded-sm border border-dashed px-3 py-2.5 font-sans text-[12px] transition-colors " +
|
||||
(parsing
|
||||
? "border-clay-900/15 bg-cream-100 text-clay-400 cursor-wait"
|
||||
: "border-clay-900/25 text-clay-700 hover:border-ember-500 hover:bg-ember-500/5 hover:text-ember-500")
|
||||
}
|
||||
>
|
||||
{parsing ? (
|
||||
<>
|
||||
<i className="fa-solid fa-circle-notch fa-spin text-[11px]" />
|
||||
AI 正在解析参考图…
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="fa-regular fa-image text-[13px]" />
|
||||
上传画风参考图(可选)· AI 自动解析为 prompt
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{parseError && (
|
||||
<span className="font-sans text-[11px] text-rose-500">
|
||||
<i className="fa-solid fa-circle-exclamation mr-1" />
|
||||
{parseError}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
autoFocus
|
||||
rows={6}
|
||||
placeholder={
|
||||
isCustom
|
||||
? "示例:\nA dreamy watercolor illustration, soft pastel washes, gentle line art, nostalgic atmosphere."
|
||||
: ""
|
||||
}
|
||||
className="w-full resize-y rounded-sm border border-ember-500 bg-cream-50 px-3 py-2.5 font-sans text-[13px] leading-relaxed text-clay-900 outline-none placeholder:text-clay-400"
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
{!isCustom && styleOverrides[name] ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
resetOverride(name);
|
||||
}}
|
||||
className="font-sans text-xs text-clay-500 hover:text-ember-500 transition-colors"
|
||||
>
|
||||
<i className="fa-solid fa-rotate-left mr-1.5" />
|
||||
还原默认 prompt
|
||||
</button>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
cancelEditing();
|
||||
}}
|
||||
className="px-3 py-1.5 font-sans text-xs text-clay-500 hover:text-clay-900 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!draft.trim()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
saveEditing();
|
||||
}}
|
||||
className={
|
||||
"rounded-sm px-4 py-1.5 font-sans text-xs text-cream-50 transition-colors " +
|
||||
(draft.trim()
|
||||
? "bg-clay-900 hover:bg-ember-500"
|
||||
: "bg-clay-300 cursor-not-allowed")
|
||||
}
|
||||
>
|
||||
保存并选用
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{parsing ? (
|
||||
<>
|
||||
<i className="fa-solid fa-circle-notch fa-spin text-[11px]" />
|
||||
AI 正在解析参考图…
|
||||
</>
|
||||
) : (
|
||||
/* 只读 prompt 行——无边框、纯文字,铅笔靠 padding-right 留位 */
|
||||
<div className="mt-1 relative">
|
||||
<div
|
||||
className={
|
||||
"pr-8 font-sans text-[12px] md:text-[13px] leading-relaxed line-clamp-2 " +
|
||||
(isCustom && !customStyleGuide
|
||||
? "italic text-clay-400"
|
||||
: "text-clay-500")
|
||||
}
|
||||
>
|
||||
{isCustom
|
||||
? customStyleGuide || "点击此卡片或铅笔编辑你自己的画风 prompt"
|
||||
: prompt || "(这个风格没有默认 prompt——点 ✎ 添加)"}
|
||||
</div>
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
startEditing(
|
||||
i,
|
||||
isCustom
|
||||
? customStyleGuide
|
||||
: styleOverrides[name] ?? STYLE_MAP[name] ?? "",
|
||||
);
|
||||
}}
|
||||
title={
|
||||
hasOverride
|
||||
? "再次编辑此 prompt"
|
||||
: "在此 prompt 基础上修改(默认 prompt 不会被覆盖)"
|
||||
}
|
||||
aria-label="编辑此风格 prompt"
|
||||
className={
|
||||
"absolute right-0 top-0 flex h-5 w-5 items-center justify-center rounded-sm text-[11px] transition-colors " +
|
||||
(hasOverride
|
||||
? "text-ember-500 hover:bg-ember-500/10"
|
||||
: "text-clay-400 hover:bg-cream-100 hover:text-clay-700")
|
||||
}
|
||||
>
|
||||
<i className="fa-solid fa-pencil" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
<i className="fa-regular fa-image text-[13px]" />
|
||||
上传画风参考图(可选)· AI 自动解析画面风格
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{list.length === 0 && (
|
||||
<div className="py-12 text-center font-serif text-sm text-clay-400">
|
||||
没有匹配的风格
|
||||
</button>
|
||||
)}
|
||||
{parseError && (
|
||||
<span className="font-sans text-[11px] text-rose-500">
|
||||
<i className="fa-solid fa-circle-exclamation mr-1" />
|
||||
{parseError}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
autoFocus
|
||||
rows={6}
|
||||
placeholder={"描述你想要的画面风格,例如:\n梦幻水彩风格,柔和的色调,怀旧的氛围"}
|
||||
className="w-full resize-y rounded-sm border border-clay-900/15 bg-cream-50 px-3 py-2.5 font-sans text-[13px] leading-relaxed text-clay-900 outline-none transition-colors focus:border-ember-500 placeholder:text-clay-400"
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setView("grid")}
|
||||
className="px-3 py-1.5 font-sans text-xs text-clay-500 hover:text-clay-900 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!draft.trim()}
|
||||
onClick={saveCustom}
|
||||
className={
|
||||
"rounded-sm px-4 py-1.5 font-sans text-xs text-cream-50 transition-colors " +
|
||||
(draft.trim()
|
||||
? "bg-clay-900 hover:bg-ember-500"
|
||||
: "bg-clay-300 cursor-not-allowed")
|
||||
}
|
||||
>
|
||||
保存并选用
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-3 overflow-y-auto px-6 py-6 md:grid-cols-4 md:gap-4 md:px-8">
|
||||
{list.map(({ name, i }) => {
|
||||
const isCustom = name === "自定义风格";
|
||||
const isAuto = name === "自动";
|
||||
const hasStyleMap = Boolean(STYLE_MAP[name]);
|
||||
const thumb = STYLE_THUMB[name];
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (isCustom) {
|
||||
openCustomView(customStyleGuide);
|
||||
return;
|
||||
}
|
||||
onPick(i);
|
||||
close();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
if (isCustom) { openCustomView(customStyleGuide); return; }
|
||||
onPick(i);
|
||||
close();
|
||||
}
|
||||
}}
|
||||
className={
|
||||
"group cursor-pointer rounded-sm border transition-all outline-none " +
|
||||
(i === value
|
||||
? "border-ember-500 ring-2 ring-ember-500"
|
||||
: "border-clay-900/12 hover:border-ember-500/50 hover:ring-2 hover:ring-ember-500/25")
|
||||
}
|
||||
>
|
||||
<div className="relative w-full overflow-hidden" style={{ paddingBottom: "100%" }}>
|
||||
{thumb ? (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img src={thumb} alt={name} loading="lazy" className="absolute inset-0 h-full w-full object-cover" />
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-cream-100" />
|
||||
)}
|
||||
{!isAuto && !isCustom && hasStyleMap && (
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openCustomView(STYLE_MAP[name] ?? "");
|
||||
}}
|
||||
title="基于此风格自定义"
|
||||
className="absolute right-1.5 top-1.5 z-20 flex h-6 w-6 items-center justify-center rounded-sm text-[11px] text-cream-50/70 opacity-0 transition-all group-hover:opacity-100 hover:bg-ember-500/20 hover:text-cream-50"
|
||||
>
|
||||
<i className="fa-solid fa-wand-magic-sparkles" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className={"block px-2 py-2 text-center font-serif text-sm " + (i === value ? "text-ember-500" : "text-clay-700")}>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{list.length === 0 && (
|
||||
<div className="col-span-full py-12 text-center font-serif text-sm text-clay-400">
|
||||
没有匹配的风格
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1372,16 +1265,7 @@ export default function HomePage() {
|
||||
const [open, setOpen] = useState<number>(-1);
|
||||
const [styleOpen, setStyleOpen] = useState(false);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
// 用户在「自定义」入口里填的 styleGuide 文本(中/英文都行,原样喂给 LLM)。
|
||||
// 仅在内存里持有——刷新即丢,符合「这就是一次性试玩」的语义。
|
||||
const [customStyleGuide, setCustomStyleGuide] = useState("");
|
||||
// 用户对某个预设的 prompt 改写——只覆盖该用户本次会话,绝不污染 STYLE_MAP
|
||||
// 这个 source-of-truth。键是预设名(如 "京阿尼细腻日常"),值是 override prompt。
|
||||
// 选中该预设 + 有 override → 把 override 当 styleGuide 喂给画师。
|
||||
const [styleOverrides, setStyleOverrides] = useState<Record<string, string>>({});
|
||||
// 用户在「自定义」里上传的参考图(已客户端缩到 512px、webp base64)。
|
||||
// 同时随 sessionStorage 透传到 /play → /api/start → session → painter,
|
||||
// 每一幕的 painter 都会把它作为 reference slot 0,锚定整局画风。
|
||||
const [customStyleRefImage, setCustomStyleRefImage] = useState<string>("");
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
@@ -1494,16 +1378,13 @@ export default function HomePage() {
|
||||
// TODO(自动路由): 后续实现真正的「自动」——由模型依据世界观 / 玩家 prompt
|
||||
// 选出最合适的画风,再映射到对应风格提示词,而非固定回退到 Galgame。届时
|
||||
// 同步更新风格弹窗副标题(「由模型根据 prompt 判断风格」)使文案与行为一致。
|
||||
const DEFAULT_STYLE = "Galgame CG";
|
||||
const DEFAULT_STYLE = "吉卜力";
|
||||
let styleGuide: string;
|
||||
if (artStyle === "自定义" && customStyleGuide.trim()) {
|
||||
if (artStyle === "自定义风格" && customStyleGuide.trim()) {
|
||||
styleGuide = customStyleGuide.trim();
|
||||
} else if (styleOverrides[artStyle]?.trim()) {
|
||||
// 用户对该预设做过 prompt 修改——优先用 override,不污染 STYLE_MAP。
|
||||
styleGuide = styleOverrides[artStyle]!.trim();
|
||||
} else {
|
||||
const effectiveStyle =
|
||||
artStyle === "自动" || artStyle === "自定义" ? DEFAULT_STYLE : artStyle;
|
||||
artStyle === "自动" || artStyle === "自定义风格" ? DEFAULT_STYLE : artStyle;
|
||||
styleGuide = STYLE_MAP[effectiveStyle] ?? STYLE_MAP[DEFAULT_STYLE]!;
|
||||
}
|
||||
const audioEnabled = voice === "开启";
|
||||
@@ -1512,7 +1393,7 @@ export default function HomePage() {
|
||||
// 占用 reference slot(也避免 styleGuide 已经是文本预设、画师收到不相关
|
||||
// 参考图反而产生干扰)。
|
||||
const styleReferenceImage =
|
||||
artStyle === "自定义" && customStyleRefImage ? customStyleRefImage : undefined;
|
||||
artStyle === "自定义风格" && customStyleRefImage ? customStyleRefImage : undefined;
|
||||
|
||||
track("game_start", {
|
||||
source: "prompt",
|
||||
@@ -1848,8 +1729,6 @@ export default function HomePage() {
|
||||
onClose={() => setStyleOpen(false)}
|
||||
customStyleGuide={customStyleGuide}
|
||||
setCustomStyleGuide={setCustomStyleGuide}
|
||||
styleOverrides={styleOverrides}
|
||||
setStyleOverrides={setStyleOverrides}
|
||||
customStyleRefImage={customStyleRefImage}
|
||||
setCustomStyleRefImage={setCustomStyleRefImage}
|
||||
/>
|
||||
|
||||
@@ -9,22 +9,27 @@ export const GENDERS = ["男性向", "女性向"] as const;
|
||||
|
||||
export const ART_STYLES = [
|
||||
"自动",
|
||||
"自定义",
|
||||
"京阿尼细腻日常",
|
||||
"新海诚唯美光影",
|
||||
"Galgame CG",
|
||||
"3D 动漫电影",
|
||||
"自定义风格",
|
||||
"京阿尼",
|
||||
"新海诚",
|
||||
"吉卜力",
|
||||
"3D 动画",
|
||||
"真实",
|
||||
"赛博朋克",
|
||||
"蒸汽波",
|
||||
"吉卜力治愈手绘",
|
||||
"哥特庄园",
|
||||
"废土科幻",
|
||||
// 以下为小众/区域性画风,留作长尾选项
|
||||
"古典厚涂油画",
|
||||
"极简中国水墨",
|
||||
"浮世绘木刻",
|
||||
"莫高窟壁画",
|
||||
"波斯细密画",
|
||||
"哥特",
|
||||
"废土",
|
||||
"像素风",
|
||||
"古典油画",
|
||||
"莫奈",
|
||||
"水彩",
|
||||
"水墨",
|
||||
"浮世绘",
|
||||
"彩铅",
|
||||
"手绘素描",
|
||||
"黑白漫画",
|
||||
"儿童绘本",
|
||||
"儿童涂鸦",
|
||||
"黏土手工",
|
||||
] as const;
|
||||
|
||||
export const PLOT_STYLES = ["平铺直叙", "多线转折", "悬疑烧脑", "治愈日常"] as const;
|
||||
|
||||
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 84 KiB |