Match LibTV character preset canvas
This commit is contained in:
parent
ce98b117b7
commit
4551eee9d1
@ -2,7 +2,6 @@
|
||||
|
||||
import {
|
||||
Bell,
|
||||
Box,
|
||||
ChevronDown,
|
||||
Clock3,
|
||||
Copy,
|
||||
@ -14,7 +13,6 @@ import {
|
||||
Image as ImageIcon,
|
||||
Layers,
|
||||
Loader2,
|
||||
MousePointer2,
|
||||
Music2,
|
||||
Plus,
|
||||
Save,
|
||||
@ -24,7 +22,6 @@ import {
|
||||
UserRound,
|
||||
Video,
|
||||
X,
|
||||
Zap,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
@ -56,6 +53,22 @@ type ActivePanel =
|
||||
| "account"
|
||||
| null;
|
||||
|
||||
type ActiveStarterScene = "character-three-view" | null;
|
||||
type SceneToolbarMenu = "panorama" | "orbit" | "lighting" | "grid" | "hd" | "crop" | null;
|
||||
type ComposerMenu =
|
||||
| "style"
|
||||
| "mark"
|
||||
| "focus"
|
||||
| "model"
|
||||
| "ratio"
|
||||
| "resolution"
|
||||
| "camera"
|
||||
| "panorama"
|
||||
| "translate"
|
||||
| "settings"
|
||||
| "count"
|
||||
| null;
|
||||
|
||||
type CapturedTool = {
|
||||
type?: string;
|
||||
toolKey?: string;
|
||||
@ -154,25 +167,30 @@ const STARTER_CARDS = [
|
||||
{
|
||||
title: "故事脚本生成",
|
||||
subtitle: "脚本到分镜",
|
||||
coverIndex: 0,
|
||||
coverUrl:
|
||||
"https://liblibai-online.liblib.cloud/web/ef0097ae9da74189a0f5d604e98c77cd/f9d23335-4ec2-412d-9954-91c58f4b5eb3.png",
|
||||
steps: ["故事脚本", "多模态文本模型Pro", "可灵 3.0", "预览"],
|
||||
},
|
||||
{
|
||||
title: "角色三视图",
|
||||
subtitle: "角色资产沉淀",
|
||||
coverIndex: 4,
|
||||
coverUrl:
|
||||
"https://liblibai-online.liblib.cloud/web/ef0097ae9da74189a0f5d604e98c77cd/e3731aeb-a95b-46e9-99e7-3cd7f22a1e3f.png",
|
||||
steps: ["角色描述", "角色三视图", "全能图片模型V2", "保存"],
|
||||
},
|
||||
{
|
||||
title: "首帧图生视频",
|
||||
subtitle: "参考图到成片",
|
||||
coverIndex: 10,
|
||||
coverUrl:
|
||||
"https://liblibai-online.liblib.cloud/web/ef0097ae9da74189a0f5d604e98c77cd/f407cb9c-8dd7-436b-8e80-88da1cebf25d.png",
|
||||
steps: ["上传图片", "首帧图", "可灵 3.0", "预览"],
|
||||
},
|
||||
{
|
||||
title: "音频生视频",
|
||||
subtitle: "音频驱动画面",
|
||||
coverIndex: 17,
|
||||
coverUrl:
|
||||
"https://liblibai-online.liblib.cloud/web/ef0097ae9da74189a0f5d604e98c77cd/bd88df14-04bd-473d-9016-ec72a5273bfb.png",
|
||||
icon: "music",
|
||||
steps: ["上传音频", "音频主视频", "Wan 2.6", "预览"],
|
||||
},
|
||||
];
|
||||
@ -188,6 +206,19 @@ const TOOLBAR_ITEMS: Array<{ id: Exclude<ActivePanel, null>; label: string; icon
|
||||
|
||||
const DEFAULT_VIEWPORT = { x: 0, y: 0, zoom: 1 };
|
||||
|
||||
const TARGET_AVATAR =
|
||||
"https://liblibai-online.liblib.cloud/web/1656e2f1b3c74f1ba823b610f1b2cbdf/0c3eb5e2-1dc0-405c-a823-07b2570237f8.jpg?x-oss-process=image/resize,w_100,m_lfit/format,webp";
|
||||
const CHARACTER_IMAGE_URL =
|
||||
"https://liblibai-online.liblib.cloud/web/ea95cb51f3d94addbb92dc003cd33b49/cca9e085-dccc-4f72-aef7-d68dc6688bcf.png";
|
||||
const CHARACTER_THREE_VIEW_URL =
|
||||
"https://liblibai-online.liblib.cloud/web/ea95cb51f3d94addbb92dc003cd33b49/7a32ba0b-7b5b-46b1-a86c-e0904482adf7.png";
|
||||
|
||||
function normalizeCanvasTitle(title?: string | null) {
|
||||
const value = String(title || "").trim();
|
||||
if (!value || value === "未命名视频画布") return "未命名";
|
||||
return value;
|
||||
}
|
||||
|
||||
function getToolType(tool?: CapturedTool) {
|
||||
return String(tool?.type || "video").trim() || "video";
|
||||
}
|
||||
@ -434,6 +465,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
const [draftTitle, setDraftTitle] = useState("未命名");
|
||||
const [isTitleEditing, setIsTitleEditing] = useState(false);
|
||||
const [activePanel, setActivePanel] = useState<ActivePanel>(null);
|
||||
const [activeStarterScene, setActiveStarterScene] = useState<ActiveStarterScene>(null);
|
||||
const [toolTab, setToolTab] = useState<(typeof PANEL_TABS)[number]>("全部");
|
||||
const [toolSearch, setToolSearch] = useState("");
|
||||
const [isProjectLoading, setIsProjectLoading] = useState(true);
|
||||
@ -475,8 +507,9 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
const result = await canvasService.getProject(canvasId);
|
||||
if (!active) return;
|
||||
if (result.success && result.data) {
|
||||
setProjectTitle(result.data.title || "未命名");
|
||||
setDraftTitle(result.data.title || "未命名");
|
||||
const title = normalizeCanvasTitle(result.data.title);
|
||||
setProjectTitle(title);
|
||||
setDraftTitle(title);
|
||||
const restoredNodes = restoreNodes(result.data.canvasData);
|
||||
const restoredEdges = restoreEdges(result.data.canvasData);
|
||||
setNodes(restoredNodes);
|
||||
@ -577,6 +610,16 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
|
||||
const createStarter = useCallback(
|
||||
(starter: (typeof STARTER_CARDS)[number]) => {
|
||||
if (starter.title === "角色三视图") {
|
||||
setNodes([]);
|
||||
setEdges([]);
|
||||
setSelectedNodeId(null);
|
||||
setActivePanel(null);
|
||||
setActiveStarterScene("character-three-view");
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveStarterScene(null);
|
||||
const baseX = 380;
|
||||
const baseY = 325;
|
||||
const newNodes = starter.steps.map((title, index) => {
|
||||
@ -686,14 +729,13 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative h-dvh w-full overflow-hidden bg-[#060708] text-white"
|
||||
className="relative h-dvh w-full overflow-hidden bg-[#151515] text-white"
|
||||
onPointerDown={startCanvasDrag}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
ref={canvasRef}
|
||||
>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle,rgba(255,255,255,0.16)_1px,transparent_1px)] bg-[length:16px_16px] opacity-[0.22]" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,transparent_0,rgba(0,0,0,0.34)_72%)]" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle,rgba(255,255,255,0.18)_1px,transparent_1px)] bg-[length:16px_16px] bg-[position:2px_2px] opacity-[0.28]" />
|
||||
|
||||
<div
|
||||
className="absolute left-0 top-0 h-[4000px] w-[4000px] origin-top-left"
|
||||
@ -710,31 +752,41 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
))}
|
||||
</div>
|
||||
|
||||
{nodes.length === 0 && !activePanel ? (
|
||||
{activeStarterScene === "character-three-view" ? <CharacterThreeViewScene /> : null}
|
||||
|
||||
{nodes.length === 0 && !activePanel && !activeStarterScene ? (
|
||||
<div className="-translate-x-1/2 -translate-y-1/2 absolute left-1/2 top-[51%] z-20 w-[900px] max-w-[calc(100vw-180px)]">
|
||||
<div className="mb-8 flex items-center justify-center gap-2 text-[13px] text-white/58">
|
||||
<MousePointer2 className="h-4 w-4" />
|
||||
双击画布 自由生成节点
|
||||
<div className="mb-6 flex items-center justify-center gap-2 text-[13px] leading-none text-white/58">
|
||||
<PointerBadge />
|
||||
<span className="font-bold text-white/60">双击画布</span>
|
||||
<span>自由生成节点</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{STARTER_CARDS.map((card) => {
|
||||
const cover = capturedWorkflows[card.coverIndex]?.cover || capturedWorkflows[0]?.cover || "";
|
||||
const cover = card.coverUrl || capturedWorkflows[0]?.cover || "";
|
||||
return (
|
||||
<button
|
||||
className="relative h-[56px] overflow-hidden rounded-[8px] border border-white/10 bg-[#18191b] px-6 text-left text-[13px] shadow-[0_18px_48px_rgba(0,0,0,0.34)] transition hover:border-white/26 hover:bg-[#202124]"
|
||||
className="group relative flex h-[56px] min-w-12 cursor-pointer items-center gap-2 overflow-hidden rounded-[8px] border border-neutral-800 px-4 py-1 text-left shadow-[0_4px_10px_rgba(0,0,0,0.04)] transition"
|
||||
key={card.title}
|
||||
onClick={() => createStarter(card)}
|
||||
type="button"
|
||||
>
|
||||
{cover ? (
|
||||
<span
|
||||
className="absolute inset-y-0 right-0 w-[58%] bg-cover bg-center opacity-70"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(90deg, rgba(24,25,27,0), rgba(24,25,27,0.12)), url(${cover})`,
|
||||
}}
|
||||
<img
|
||||
alt=""
|
||||
className="absolute left-0 top-0 h-full w-full max-w-none object-cover transition-opacity duration-300 ease-out group-hover:opacity-100"
|
||||
src={cover}
|
||||
/>
|
||||
) : null}
|
||||
<span className="relative z-10 block font-medium text-white">{card.title}</span>
|
||||
<span className="absolute inset-0 rounded-[8px] opacity-0 backdrop-blur-[6px] transition-opacity duration-300 ease-out group-hover:opacity-100 bg-white/12" />
|
||||
<span className="relative z-10 flex items-center gap-2 transition-opacity duration-200 ease-out group-hover:opacity-0">
|
||||
{card.icon === "music" ? <MusicStarterIcon /> : <span />}
|
||||
<span className="text-xs leading-none text-white">{card.title}</span>
|
||||
</span>
|
||||
<span className="pointer-events-none absolute left-1/2 top-1/2 z-20 flex h-7 -translate-x-1/2 -translate-y-1/2 items-center justify-center gap-1 rounded-full bg-black/85 px-3 py-2 text-[12px] text-white opacity-0 transition-all duration-300 ease-out group-hover:opacity-100">
|
||||
去生成
|
||||
<ArrowGlyph />
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@ -742,26 +794,31 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<TopBar
|
||||
draftTitle={draftTitle}
|
||||
isSaving={isSaving}
|
||||
isTitleEditing={isTitleEditing}
|
||||
onAccount={() => setActivePanel("account")}
|
||||
onBell={() => setActivePanel("notifications")}
|
||||
onCommitTitle={commitTitle}
|
||||
onDraftTitleChange={setDraftTitle}
|
||||
onMember={() => setActivePanel("member")}
|
||||
onSave={saveCanvas}
|
||||
onShare={() => setActivePanel("share")}
|
||||
onSkills={() => setActivePanel("skills")}
|
||||
onStartTitleEdit={() => setIsTitleEditing(true)}
|
||||
projectTitle={projectTitle}
|
||||
routerBack={() => router.push("/")}
|
||||
/>
|
||||
{!activeStarterScene ? (
|
||||
<>
|
||||
<TopBar
|
||||
draftTitle={draftTitle}
|
||||
isSaving={isSaving}
|
||||
isTitleEditing={isTitleEditing}
|
||||
onAccount={() => setActivePanel("account")}
|
||||
onBell={() => setActivePanel("notifications")}
|
||||
onCommitTitle={commitTitle}
|
||||
onDraftTitleChange={setDraftTitle}
|
||||
onMember={() => setActivePanel("member")}
|
||||
onSave={saveCanvas}
|
||||
onShare={() => setActivePanel("share")}
|
||||
onSkills={() => setActivePanel("skills")}
|
||||
onStartTitleEdit={() => setIsTitleEditing(true)}
|
||||
projectTitle={projectTitle}
|
||||
routerBack={() => router.push("/")}
|
||||
/>
|
||||
|
||||
<LeftToolbar activePanel={activePanel} onOpen={(panel) => setActivePanel((current) => current === panel ? null : panel)} />
|
||||
<LeftToolbar activePanel={activePanel} onOpen={(panel) => setActivePanel((current) => current === panel ? null : panel)} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<ZoomDock
|
||||
compact={Boolean(activeStarterScene)}
|
||||
zoom={viewport.zoom}
|
||||
onCenter={() => setViewport(DEFAULT_VIEWPORT)}
|
||||
onZoomIn={() => zoomBy(0.1)}
|
||||
@ -969,19 +1026,20 @@ function TopBar({
|
||||
routerBack: () => void;
|
||||
}) {
|
||||
return (
|
||||
<header className="pointer-events-none absolute left-0 right-0 top-0 z-40 flex h-[78px] items-start justify-between px-4 pt-5">
|
||||
<div className="pointer-events-auto flex items-center gap-4">
|
||||
<button className="flex items-center gap-2" onClick={routerBack} type="button">
|
||||
<span className="relative h-[15px] w-[23px] rounded-[2px] border border-white/90">
|
||||
<span className="absolute -right-[5px] top-[4px] h-[7px] w-[11px] rounded-[1px] bg-white" />
|
||||
</span>
|
||||
<span className="font-bold italic tracking-wide text-white">LibTV</span>
|
||||
<header className="pointer-events-none absolute left-0 right-0 top-4 z-40 flex h-12 items-center justify-between pl-2 pr-4">
|
||||
<div className="pointer-events-auto flex items-center gap-3">
|
||||
<button
|
||||
className="flex h-10 cursor-pointer items-center justify-center rounded-xl px-2 text-white transition-colors hover:bg-white/10"
|
||||
onClick={routerBack}
|
||||
type="button"
|
||||
>
|
||||
<LibTVLogo />
|
||||
</button>
|
||||
<div className="h-6 w-px bg-white/14" />
|
||||
<div className="h-4 w-px bg-white/14" />
|
||||
{isTitleEditing ? (
|
||||
<input
|
||||
autoFocus
|
||||
className="h-9 w-[220px] rounded-[8px] border border-white/14 bg-[#1f1f1f] px-3 text-[14px] text-white outline-none"
|
||||
className="h-8 min-w-[60px] max-w-[160px] rounded-[8px] border border-white/14 bg-transparent px-2 text-sm font-medium text-white outline-none"
|
||||
onBlur={onCommitTitle}
|
||||
onChange={(event) => onDraftTitleChange(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
@ -990,7 +1048,11 @@ function TopBar({
|
||||
value={draftTitle}
|
||||
/>
|
||||
) : (
|
||||
<button className="text-[16px] font-semibold text-white/82" onClick={onStartTitleEdit} type="button">
|
||||
<button
|
||||
className="max-w-[160px] truncate text-sm font-medium text-white/78"
|
||||
onClick={onStartTitleEdit}
|
||||
type="button"
|
||||
>
|
||||
{projectTitle || "未命名"}
|
||||
</button>
|
||||
)}
|
||||
@ -1004,20 +1066,15 @@ function TopBar({
|
||||
<TopButton onClick={onShare} title="分享">
|
||||
<Share2 className="h-4 w-4" />
|
||||
</TopButton>
|
||||
<TopButton onClick={onBell} title="通知">
|
||||
<TopButton className="relative" onClick={onBell} title="通知">
|
||||
<span className="absolute right-px top-px h-[5px] w-[5px] rounded-full bg-[#f53f3f]" />
|
||||
<Bell className="h-4 w-4" />
|
||||
</TopButton>
|
||||
<TopButton className="gap-2 px-4 text-cyan-300" onClick={onMember}>
|
||||
<Crown className="h-4 w-4" />
|
||||
<TopButton className="gap-1 px-3 text-[#05dff6]" onClick={onMember}>
|
||||
<StoreGlyph />
|
||||
会员超市
|
||||
</TopButton>
|
||||
<TopButton className="gap-2 px-4 text-yellow-200" onClick={onMember}>
|
||||
<Zap className="h-4 w-4" />
|
||||
会员特惠
|
||||
</TopButton>
|
||||
<TopButton onClick={onAccount} title="账号">
|
||||
<UserRound className="h-4 w-4" />
|
||||
</TopButton>
|
||||
<MemberDealPill onAccount={onAccount} onMember={onMember} />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
@ -1037,7 +1094,7 @@ function TopButton({
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"flex h-[46px] min-w-[46px] items-center justify-center rounded-[8px] border border-white/8 bg-[#202020] px-3 text-[14px] text-white/72 shadow-[0_12px_30px_rgba(0,0,0,0.28)] transition hover:bg-[#2a2a2a] hover:text-white",
|
||||
"flex h-10 min-w-10 cursor-pointer items-center justify-center rounded-xl border border-white/8 bg-[#202020] px-3 text-[13px] font-normal text-white/72 shadow-[0_12px_30px_rgba(0,0,0,0.28)] transition hover:bg-[#2a2a2a] hover:text-white",
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
@ -1058,25 +1115,33 @@ function LeftToolbar({
|
||||
}) {
|
||||
return (
|
||||
<aside
|
||||
className="-translate-y-1/2 absolute left-4 top-1/2 z-40 flex flex-col gap-2 rounded-[8px] border border-white/9 bg-[#202020] p-2 shadow-[0_22px_60px_rgba(0,0,0,0.45)]"
|
||||
className="-translate-y-1/2 absolute left-4 top-1/2 z-40 flex flex-col items-center gap-2 rounded-xl border border-white/9 bg-[#202020]/95 p-2 shadow-[0_22px_60px_rgba(0,0,0,0.45)] backdrop-blur-[8px]"
|
||||
data-canvas-overlay="true"
|
||||
>
|
||||
{TOOLBAR_ITEMS.map((item, index) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = activePanel === item.id || (!activePanel && item.id === "add");
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"flex h-11 w-11 items-center justify-center rounded-[8px] text-white/70 transition hover:bg-white/10 hover:text-white",
|
||||
activePanel === item.id && "bg-white text-black hover:bg-white hover:text-black",
|
||||
index === 0 && "mb-1"
|
||||
)}
|
||||
key={item.id}
|
||||
onClick={() => onOpen(item.id)}
|
||||
title={item.label}
|
||||
type="button"
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
</button>
|
||||
<div className="contents" key={item.id}>
|
||||
<button
|
||||
className={cn(
|
||||
"group flex h-9 w-9 cursor-pointer items-center justify-center rounded-[12px] text-white/72 transition-colors hover:bg-white/10 hover:text-white",
|
||||
isActive && "bg-white text-black hover:bg-white hover:text-black"
|
||||
)}
|
||||
onClick={() => onOpen(item.id)}
|
||||
title={item.label}
|
||||
type="button"
|
||||
>
|
||||
{item.id === "assets" ? (
|
||||
<AssetGlyph />
|
||||
) : item.id === "workflow" ? (
|
||||
<WorkflowGlyph />
|
||||
) : (
|
||||
<Icon className={cn("h-[18px] w-[18px]", item.id === "add" && "transition-transform duration-200 group-hover:rotate-45")} />
|
||||
)}
|
||||
</button>
|
||||
{index === 3 ? <div className="my-0 h-px w-5 shrink-0 bg-white/10" /> : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</aside>
|
||||
@ -1084,44 +1149,579 @@ function LeftToolbar({
|
||||
}
|
||||
|
||||
function ZoomDock({
|
||||
compact = false,
|
||||
onCenter,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
zoom,
|
||||
}: {
|
||||
compact?: boolean;
|
||||
onCenter: () => void;
|
||||
onZoomIn: () => void;
|
||||
onZoomOut: () => void;
|
||||
zoom: number;
|
||||
}) {
|
||||
if (compact) {
|
||||
return (
|
||||
<div
|
||||
className="absolute bottom-4 left-0 z-40 flex items-center gap-1 rounded-r-xl border border-l-0 border-white/9 bg-[#202020]/95 p-1.5 text-white/70 shadow-[0_18px_48px_rgba(0,0,0,0.42)] backdrop-blur-[8px]"
|
||||
data-canvas-overlay="true"
|
||||
>
|
||||
<button className="flex h-7 w-10 items-center justify-center rounded-lg bg-white/10 transition-colors hover:bg-white/14" type="button">
|
||||
<MagnetGlyph />
|
||||
</button>
|
||||
<button className="flex h-7 w-10 items-center justify-center rounded-lg text-[18px] leading-none transition-colors hover:bg-white/10" onClick={onZoomOut} type="button">
|
||||
-
|
||||
</button>
|
||||
<button className="h-7 min-w-[72px] rounded-lg px-1 text-[13px] leading-normal transition-colors hover:bg-white/10" onClick={onCenter} type="button">
|
||||
77%
|
||||
</button>
|
||||
<button className="flex h-7 w-10 items-center justify-center rounded-lg text-[18px] leading-none transition-colors hover:bg-white/10" onClick={onZoomIn} type="button">
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute bottom-4 left-4 z-40 flex items-center gap-2 rounded-[8px] border border-white/9 bg-[#202020] p-2 text-white/70 shadow-[0_18px_48px_rgba(0,0,0,0.42)]"
|
||||
className="absolute bottom-4 left-4 z-40 flex items-center gap-1 rounded-xl border border-white/9 bg-[#202020]/95 p-1.5 text-white/70 shadow-[0_18px_48px_rgba(0,0,0,0.42)] backdrop-blur-[8px]"
|
||||
data-canvas-overlay="true"
|
||||
>
|
||||
<button className="flex h-10 w-10 items-center justify-center rounded-[8px] bg-white text-black" type="button">
|
||||
<MousePointer2 className="h-4 w-4" />
|
||||
<button className="flex h-7 w-7 items-center justify-center rounded-lg transition-colors hover:bg-white/10" type="button">
|
||||
<GridTilesGlyph />
|
||||
</button>
|
||||
<button className="flex h-10 w-10 items-center justify-center rounded-[8px] hover:bg-white/10" type="button">
|
||||
<Box className="h-4 w-4" />
|
||||
<button className="flex h-7 w-7 items-center justify-center rounded-lg transition-colors hover:bg-white/10" type="button">
|
||||
<MapPinGlyph />
|
||||
</button>
|
||||
<button className="flex h-10 w-10 items-center justify-center rounded-[8px] hover:bg-white/10" type="button">
|
||||
<GitBranch className="h-4 w-4" />
|
||||
<button className="flex h-7 w-7 items-center justify-center rounded-lg bg-white/10 transition-colors hover:bg-white/14" type="button">
|
||||
<MagnetGlyph />
|
||||
</button>
|
||||
<div className="mx-1 h-6 w-px bg-white/10" />
|
||||
<button className="h-10 w-10 rounded-[8px] text-[18px] hover:bg-white/10" onClick={onZoomOut} type="button">
|
||||
<button className="flex h-7 w-7 items-center justify-center rounded-lg text-[18px] leading-none transition-colors hover:bg-white/10" onClick={onZoomOut} type="button">
|
||||
-
|
||||
</button>
|
||||
<button className="h-10 min-w-[74px] rounded-[8px] text-[15px] hover:bg-white/10" onClick={onCenter} type="button">
|
||||
<button className="h-7 min-w-[58px] rounded-lg px-1 text-[13px] leading-normal transition-colors hover:bg-white/10" onClick={onCenter} type="button">
|
||||
{Math.round(zoom * 100)}%
|
||||
</button>
|
||||
<button className="h-10 w-10 rounded-[8px] text-[18px] hover:bg-white/10" onClick={onZoomIn} type="button">
|
||||
<button className="flex h-7 w-7 items-center justify-center rounded-lg text-[18px] leading-none transition-colors hover:bg-white/10" onClick={onZoomIn} type="button">
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LibTVLogo() {
|
||||
return (
|
||||
<svg aria-hidden="true" className="h-6 text-white" fill="currentColor" height="17" viewBox="0 0 76.234 16.79" width="77">
|
||||
<path d="M16.576 16.616H0l.833-4.418H17.65z" />
|
||||
<path d="m0 16.616 2.314-12.27h4.448l-2.316 12.27zM8.27 0h16.936l-.832 4.416H7.544z" />
|
||||
<path d="m25.206 0-2.314 12.27-4.512.002L20.76 0zM30.857 14.816 33.09 2.217h2.7l-1.82 10.276h4.968l-.415 2.321h-7.666zM41.025 6.328h2.556l-1.639 8.488h-2.537l1.619-8.488zm.235-2.51c0-.882.701-1.547 1.566-1.547.81 0 1.367.54 1.367 1.315 0 .882-.702 1.547-1.566 1.547-.81 0-1.367-.54-1.367-1.314M53.696 9.488c0 3.077-2.232 5.435-4.933 5.435-1.458 0-2.268-.559-2.735-1.296l-.45 1.189h-2.232L45.56 2.217h2.538l-.738 4.265c.63-.63 1.44-1.025 2.555-1.025 2.214 0 3.78 1.71 3.78 4.03m-2.574.198c0-1.422-.756-2.16-1.87-2.16-1.657 0-2.7 1.547-2.7 3.168 0 1.421.774 2.16 1.89 2.16 1.638 0 2.682-1.548 2.682-3.168zM53.929 2.217h9.934l-.395 2.321H59.85L58.03 14.814h-2.699l1.819-10.276h-3.618zM72.934 2.217h2.879l-6.497 12.599h-3.222L64.042 2.217h2.772l1.368 9.322z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function PointerBadge() {
|
||||
return (
|
||||
<svg aria-hidden="true" className="h-[22px] w-[22px] shrink-0" fill="none" viewBox="0 0 22 22">
|
||||
<circle cx="8" cy="7.904" fill="#171717" r="7.3" />
|
||||
<circle cx="8" cy="7.904" fill="#525252" r="4.103" />
|
||||
<path
|
||||
d="M6.86 8.082c-.299-.824.499-1.621 1.322-1.323l11.394 4.12a.478.478 0 0 1 .026.89 14.72 14.72 0 0 0-7.733 7.732.478.478 0 0 1-.889-.025z"
|
||||
fill="#f7f7f7"
|
||||
stroke="#f7f7f7"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="0.945"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ArrowGlyph() {
|
||||
return (
|
||||
<svg aria-hidden="true" className="h-3 w-3 text-white" fill="currentColor" viewBox="0 0 20.1132 17.9127">
|
||||
<path d="M10.957 0.117086C11.1132 -0.0390761 11.3662 -0.0389815 11.5225 0.117086L19.9375 8.53213C20.1718 8.76644 20.1718 9.14644 19.9375 9.38076L11.5225 17.7958C11.3663 17.9516 11.1131 17.9518 10.957 17.7958L10.249 17.0878C10.0931 16.9317 10.0932 16.6786 10.249 16.5224L16.915 9.85634H0.399414C0.178762 9.85616 0.000169635 9.67759 0 9.45693V8.45595C0 8.23515 0.178657 8.05575 0.399414 8.05556H16.915L10.249 1.39052C10.0931 1.23442 10.0932 0.981285 10.249 0.825094L10.957 0.117086Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MusicStarterIcon() {
|
||||
return (
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-[8px] bg-white text-black">
|
||||
<Music2 className="h-5 w-5" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function StoreGlyph() {
|
||||
return (
|
||||
<svg aria-hidden="true" className="h-4 w-4 shrink-0" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M11.5703 7.32031C11.7041 7.40647 11.8427 7.49125 11.9863 7.56543C12.4351 7.79714 13.0016 7.99984 13.5938 8C13.9834 7.99995 14.5104 7.94128 15.0498 7.83203V10.25C15.0498 11.2914 15.0517 12.1405 14.9619 12.8086C14.8695 13.4961 14.6692 14.0925 14.1934 14.5684C13.7175 15.0442 13.1211 15.2445 12.4336 15.3369C11.7655 15.4267 10.9164 15.4248 9.875 15.4248H6.125C5.08356 15.4248 4.23451 15.4267 3.56641 15.3369C2.87886 15.2445 2.28246 15.0442 1.80664 14.5684C1.33083 14.0925 1.13053 13.4961 1.03809 12.8086C0.948294 12.1405 0.950198 11.2914 0.950198 10.25V7.83594C1.49346 7.94052 2.02537 7.99896 2.40625 7.99902C2.99823 7.99894 3.56495 7.79594 4.01367 7.56445C4.15699 7.49047 4.29517 7.40621 4.42871 7.32031C4.85021 7.66646 5.42619 7.99885 6.16504 7.99902C6.77646 7.99893 7.3045 7.77311 7.70996 7.52246C7.81016 7.46049 7.90621 7.39372 7.99805 7.32617C8.09066 7.39438 8.18892 7.45994 8.29004 7.52246C8.69562 7.77313 9.22373 7.99883 9.83496 7.99902C10.5728 7.99886 11.1488 7.66619 11.5703 7.32031ZM8 9.5C6.89544 9.5 6 10.3954 6 11.5V13.5C6 14.0523 6.44772 14.5 7 14.5H9C9.55229 14.5 10 14.0523 10 13.5V11.5C10 10.3954 9.10457 9.5 8 9.5ZM9.83496 6.99902L9.69922 6.99316C9.68917 6.99225 9.67896 6.98944 9.66895 6.98828C9.72373 6.9944 9.77923 6.99901 9.83496 6.99902Z"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
<path d="M8.00053 1.00014C11.0011 1.00014 12.6726 0.985326 13.3162 1.28875C13.9598 1.59225 14.3832 2.15654 14.9903 2.92752L15.2752 3.28903L15.2838 3.30002L15.4892 3.59462C15.6898 3.90524 15.8674 4.26195 15.9504 4.63421C16.065 5.14874 16.0048 5.74519 15.515 6.23304C15.0392 6.7069 14.3509 7 13.5936 7C12.8445 6.9999 11.9776 6.47094 11.5 6C11.0434 6.46652 10.567 6.999 9.83479 6.999C9.10776 6.99887 8.45603 6.46088 8 6C7.5439 6.4606 6.89205 6.999 6.16519 6.999C5.4321 6.99897 4.95655 6.46754 4.5 6C4.02234 6.47025 3.15497 6.999 2.40635 6.999C1.6491 6.99895 0.960752 6.70587 0.484999 6.23204C-0.00468519 5.74422 -0.0650214 5.1477 0.04955 4.63321C0.160132 4.1368 0.438679 3.66782 0.716163 3.29902L0.724765 3.28804L1.00969 2.92653C1.61692 2.1553 2.041 1.59122 2.68482 1.28775C3.32863 0.984389 5 1.00014 8.00053 1.00014Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function DiamondGlyph() {
|
||||
return (
|
||||
<svg aria-hidden="true" className="h-4 w-4 shrink-0" fill="none" viewBox="0 0 16 16">
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M12.19 1.333c.696 0 1.33.406 1.618 1.04L15.506 6.1a1.78 1.78 0 0 1-.319 1.952l-5.889 6.294c-.702.75-1.894.75-2.596 0L.813 8.052A1.78 1.78 0 0 1 .493 6.1l1.7-3.727c.289-.633.92-1.04 1.617-1.04zM6.467 5.971l-1.8-.005 3.12 3.12c.099.1.26.1.358 0l3.114-3.112c.007-.007.01-.01 0-.01h-1.8L8.075 7.349a.16.16 0 0 1-.226-.001L6.46 5.96c.002.002.01.01.007.01"
|
||||
fill="#FFC65D"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function LightningGlyph() {
|
||||
return (
|
||||
<svg aria-hidden="true" className="h-[10px] w-2 shrink-0 text-[#FFDBA4]" fill="none" viewBox="0 0 8 10">
|
||||
<path
|
||||
d="M7.306 4.36H5.79V.76c0-.84-.447-1.01-.992-.38l-.393.455-3.323 3.85c-.456.525-.265.955.422.955h1.517v3.6c0 .84.447 1.01.992.38l.392-.455 3.324-3.85c.456-.525.265-.955-.423-.955"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MemberDealPill({ onAccount, onMember }: { onAccount: () => void; onMember: () => void }) {
|
||||
return (
|
||||
<div className="flex h-10 items-center gap-2 rounded-xl border border-white/8 bg-[#202020] px-3 py-[6px] text-[12px] text-white/76 shadow-[0_12px_30px_rgba(0,0,0,0.28)]">
|
||||
<button className="relative flex cursor-pointer items-center gap-1 rounded-[8px] px-0.5 hover:text-white" onClick={onMember} type="button">
|
||||
<span className="absolute -top-5 left-0 z-10 flex h-4 whitespace-nowrap rounded-md rounded-bl-none bg-[#FAD6A4] px-2 py-0.5 text-center text-[10px] leading-[12px] text-[#0D0D0D]">
|
||||
限时 39 折
|
||||
</span>
|
||||
<DiamondGlyph />
|
||||
<span>
|
||||
会员特惠<span className="font-[800]">39折</span>
|
||||
</span>
|
||||
</button>
|
||||
<button className="flex cursor-pointer items-center gap-1 rounded-[8px] px-0.5 hover:text-white" onClick={onMember} type="button">
|
||||
<LightningGlyph />
|
||||
<span>100</span>
|
||||
</button>
|
||||
<button className="rounded-full" onClick={onAccount} type="button">
|
||||
<img alt="avatar" className="h-7 w-7 rounded-full object-cover" src={TARGET_AVATAR} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AssetGlyph() {
|
||||
return (
|
||||
<svg aria-hidden="true" className="h-[18px] w-[18px]" fill="none" viewBox="0 0 18 18">
|
||||
<path d="M9 1.65 15.15 5.1 9 8.55 2.85 5.1 9 1.65Z" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.7" />
|
||||
<path d="M2.85 8.05 9 11.5l6.15-3.45M2.85 11 9 14.45 15.15 11" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkflowGlyph() {
|
||||
return (
|
||||
<svg aria-hidden="true" className="h-[18px] w-[18px]" fill="none" viewBox="0 0 18 18">
|
||||
<path d="M5.25 4.5a1.9 1.9 0 1 1-3.8 0 1.9 1.9 0 0 1 3.8 0ZM16.55 4.5a1.9 1.9 0 1 1-3.8 0 1.9 1.9 0 0 1 3.8 0ZM5.25 13.5a1.9 1.9 0 1 1-3.8 0 1.9 1.9 0 0 1 3.8 0Z" stroke="currentColor" strokeWidth="1.7" />
|
||||
<path d="M5.25 4.5h4.4c1.3 0 2.35 1.05 2.35 2.35v0c0 1.3-1.05 2.35-2.35 2.35H8.3c-1.3 0-2.35 1.05-2.35 2.35v1.95" stroke="currentColor" strokeLinecap="round" strokeWidth="1.7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function GridTilesGlyph() {
|
||||
return (
|
||||
<svg aria-hidden="true" className="h-[14px] w-[14px]" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M5.13379 8.33887C6.00346 8.33899 6.70886 9.04342 6.70898 9.91309V11.4883C6.70887 12.358 6.00347 13.0634 5.13379 13.0635H2.50879C1.6391 13.0634 0.933703 12.358 0.933594 11.4883V9.91309C0.933717 9.04341 1.63911 8.33898 2.50879 8.33887H5.13379ZM11.4922 6.23828C12.3619 6.23839 13.0674 6.94369 13.0674 7.81348V11.4883C13.0673 12.358 12.3619 13.0634 11.4922 13.0635H8.86719C7.99746 13.0634 7.2921 12.358 7.29199 11.4883V7.81348C7.29199 6.94367 7.99739 6.23834 8.86719 6.23828H11.4922ZM2.45508 9.3916C2.19053 9.41859 1.98351 9.64144 1.9834 9.91309V11.4883C1.9835 11.7599 2.19051 11.9838 2.45508 12.0107L2.50879 12.0137H5.13379L5.1875 12.0107C5.45212 11.9838 5.6581 11.76 5.6582 11.4883V9.91309C5.65809 9.64141 5.4521 9.41854 5.1875 9.3916L5.13379 9.38867H2.50879L2.45508 9.3916ZM8.86719 7.28809C8.57729 7.28815 8.34277 7.52356 8.34277 7.81348V11.4883L8.34473 11.542C8.36987 11.789 8.56642 11.9857 8.81348 12.0107L8.86719 12.0137H11.4922L11.5459 12.0107C11.7929 11.9856 11.9895 11.789 12.0146 11.542L12.0176 11.4883V7.81348C12.0176 7.52359 11.782 7.2882 11.4922 7.28809H8.86719ZM5.13379 0.929688C6.00347 0.929811 6.70887 1.6352 6.70898 2.50488V6.17969C6.70892 7.04941 6.00349 7.75476 5.13379 7.75488H2.50879C1.63907 7.75477 0.933655 7.04942 0.933594 6.17969V2.50488C0.933704 1.6352 1.6391 0.929797 2.50879 0.929688H5.13379ZM2.45508 1.98242C2.19052 2.0094 1.9835 2.23322 1.9834 2.50488V6.17969L1.98633 6.2334C2.01141 6.48042 2.20808 6.67697 2.45508 6.70215L2.50879 6.70508H5.13379L5.1875 6.70215C5.43455 6.67702 5.63116 6.48045 5.65625 6.2334L5.6582 6.17969V2.50488C5.6581 2.23318 5.45212 2.00935 5.1875 1.98242L5.13379 1.97949H2.50879L2.45508 1.98242ZM11.4922 0.929688C12.3619 0.929797 13.0673 1.63519 13.0674 2.50488V4.08008C13.0671 4.94966 12.3618 5.65419 11.4922 5.6543H8.86719C7.99754 5.65424 7.29223 4.94968 7.29199 4.08008V2.50488C7.2921 1.63517 7.99746 0.929749 8.86719 0.929688H11.4922ZM8.86719 1.97949C8.57736 1.97955 8.34288 2.21506 8.34277 2.50488V4.08008L8.34473 4.13379C8.37172 4.39837 8.59549 4.60443 8.86719 4.60449H11.4922C11.7638 4.60439 11.9877 4.39835 12.0146 4.13379L12.0176 4.08008V2.50488C12.0175 2.21509 11.782 1.9796 11.4922 1.97949H8.86719Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MapPinGlyph() {
|
||||
return (
|
||||
<svg aria-hidden="true" className="h-[14px] w-[14px]" fill="none" viewBox="0 0 22 22">
|
||||
<path d="M10.9 1C7.59 1 4.9 3.67 4.9 6.96c0 4.35 5.15 8.65 5.37 8.83.36.3.89.3 1.25 0 .22-.18 5.37-4.48 5.37-8.83C16.9 3.67 14.21 1 10.9 1Z" stroke="currentColor" strokeWidth="1.8" />
|
||||
<path d="M10.9 9.25a2.2 2.2 0 1 0 0-4.4 2.2 2.2 0 0 0 0 4.4Z" stroke="currentColor" strokeWidth="1.8" />
|
||||
<path d="M6.9 13h-2.8a1.1 1.1 0 0 0-1.04.75L1.4 18.75A1.1 1.1 0 0 0 2.45 20.2h16.9a1.1 1.1 0 0 0 1.05-1.45l-1.66-5a1.1 1.1 0 0 0-1.04-.75h-2.8" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MagnetGlyph() {
|
||||
return (
|
||||
<svg aria-hidden="true" className="h-[14px] w-[14px]" fill="none" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="m12 15 4 4M2.352 10.648a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l6.029-6.029a1 1 0 1 1 3 3l-6.029 6.029a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l6.365-6.367A1 1 0 0 0 8.716 4.282zM5 8l4 4"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function CharacterThreeViewScene() {
|
||||
const [toolbarMenu, setToolbarMenu] = useState<SceneToolbarMenu>(null);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
|
||||
<div className="pointer-events-auto absolute left-1/2 top-[90px] flex h-[72px] -translate-x-1/2 items-center gap-6 rounded-[18px] border border-white/8 bg-[#262626]/96 px-7 text-[18px] text-white/82 shadow-[0_20px_64px_rgba(0,0,0,0.42)] backdrop-blur-[10px]">
|
||||
<SceneTool active={toolbarMenu === "panorama"} kind="panorama" label="全景" badge="NEW" onClick={() => setToolbarMenu((current) => current === "panorama" ? null : "panorama")} />
|
||||
<SceneTool active={toolbarMenu === "orbit"} kind="orbit" label="多角度" onClick={() => setToolbarMenu((current) => current === "orbit" ? null : "orbit")} />
|
||||
<SceneTool active={toolbarMenu === "lighting"} kind="lighting" label="打光" onClick={() => setToolbarMenu((current) => current === "lighting" ? null : "lighting")} />
|
||||
<SceneTool active={toolbarMenu === "grid"} kind="grid" label="九宫格" dropdown onClick={() => setToolbarMenu((current) => current === "grid" ? null : "grid")} />
|
||||
<SceneTool active={toolbarMenu === "hd"} kind="hd" label="高清" dropdown onClick={() => setToolbarMenu((current) => current === "hd" ? null : "hd")} />
|
||||
<SceneTool active={toolbarMenu === "crop"} kind="crop" label="宫格切分" dropdown onClick={() => setToolbarMenu((current) => current === "crop" ? null : "crop")} />
|
||||
<div className="h-9 w-px bg-white/8" />
|
||||
{["⌁", "↔", "↓", "↗"].map((item, index) => (
|
||||
<button
|
||||
className="flex h-10 w-10 items-center justify-center rounded-[8px] text-[28px] text-white/84 hover:bg-white/8"
|
||||
key={item}
|
||||
onClick={() => toast.info(["绘制编辑待接入。", "对比预览待接入。", "下载素材待接入。", "全屏预览待接入。"][index])}
|
||||
type="button"
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{toolbarMenu ? <SceneToolbarPopover menu={toolbarMenu} onClose={() => setToolbarMenu(null)} /> : null}
|
||||
|
||||
<div className="pointer-events-auto absolute left-1/2 top-[174px] flex -translate-x-1/2 items-start gap-[110px]">
|
||||
<CharacterImagePanel imageUrl={CHARACTER_IMAGE_URL} label="角色图" wide />
|
||||
<div className="relative mt-[214px] w-[4px]">
|
||||
<div className="absolute right-[4px] top-0 h-[2px] w-[110px] bg-gradient-to-r from-transparent via-[#59a8ff] to-[#9bd6ff]" />
|
||||
<button
|
||||
className="absolute -left-[16px] -top-[14px] flex h-7 w-7 items-center justify-center rounded-full border border-white/45 bg-[#222] text-[22px] leading-none text-white/72"
|
||||
onClick={() => toast.info("连接素材节点接口待接入。")}
|
||||
type="button"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<CharacterImagePanel imageUrl={CHARACTER_THREE_VIEW_URL} label="角色三视图" />
|
||||
</div>
|
||||
|
||||
<PromptComposer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SceneTool({
|
||||
active,
|
||||
badge,
|
||||
dropdown,
|
||||
kind,
|
||||
label,
|
||||
onClick,
|
||||
}: {
|
||||
active?: boolean;
|
||||
badge?: string;
|
||||
dropdown?: boolean;
|
||||
kind: "panorama" | "orbit" | "lighting" | "grid" | "hd" | "crop";
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
className={cn("flex h-10 items-center gap-2 rounded-[8px] px-1 text-white/82 hover:bg-white/8", active && "bg-white/10 text-white")}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
>
|
||||
<SceneToolIcon kind={kind} />
|
||||
<span>{label}</span>
|
||||
{badge ? (
|
||||
<span className="rounded-[7px] bg-[#0e6371] px-2 py-0.5 text-[14px] font-bold tracking-wide text-[#42eaff]">
|
||||
{badge}
|
||||
</span>
|
||||
) : null}
|
||||
{dropdown ? <span className="text-[16px] text-white/58">⌄</span> : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SceneToolbarPopover({
|
||||
menu,
|
||||
onClose,
|
||||
}: {
|
||||
menu: Exclude<SceneToolbarMenu, null>;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const quickOptions: Record<Exclude<SceneToolbarMenu, "lighting" | null>, string[]> = {
|
||||
crop: ["4 宫格切分", "9 宫格切分", "16 宫格切分", "25 宫格切分"],
|
||||
grid: ["2 x 2", "3 x 3", "4 x 4", "自定义宫格"],
|
||||
hd: ["2倍高清", "4倍高清"],
|
||||
orbit: ["正侧背三视角", "八方向参考", "环绕镜头"],
|
||||
panorama: ["全景", "半身", "近景"],
|
||||
};
|
||||
|
||||
if (menu === "lighting") {
|
||||
return (
|
||||
<div className="pointer-events-auto absolute left-1/2 top-[172px] z-40 w-[420px] -translate-x-1/2 rounded-[14px] border border-white/10 bg-[#262626]/98 p-4 text-white shadow-[0_24px_80px_rgba(0,0,0,0.46)] backdrop-blur-[10px]">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="font-semibold text-[15px] text-white/86">打光</div>
|
||||
<button className="rounded-[8px] px-2 py-1 text-white/42 hover:bg-white/8 hover:text-white" onClick={onClose} type="button">×</button>
|
||||
</div>
|
||||
<textarea
|
||||
className="h-24 w-full resize-none rounded-[10px] border border-white/10 bg-[#1d1d1d] px-3 py-2 text-[13px] text-white/78 outline-none placeholder:text-white/34"
|
||||
placeholder="简单描述你想实现的打光效果,或者情绪风格"
|
||||
/>
|
||||
<button
|
||||
className="mt-3 h-10 w-full rounded-[10px] bg-white font-semibold text-black hover:bg-white/90"
|
||||
onClick={() => toast.info("打光生成接口待接入。")}
|
||||
type="button"
|
||||
>
|
||||
应用打光
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto absolute left-1/2 top-[172px] z-40 w-[260px] -translate-x-1/2 rounded-[14px] border border-white/10 bg-[#262626]/98 p-2 text-white shadow-[0_24px_80px_rgba(0,0,0,0.46)] backdrop-blur-[10px]">
|
||||
{quickOptions[menu].map((option) => (
|
||||
<button
|
||||
className="flex h-10 w-full items-center justify-between rounded-[10px] px-3 text-left text-[13px] text-white/76 hover:bg-white/8 hover:text-white"
|
||||
key={option}
|
||||
onClick={() => toast.info(`${option} 接口待接入。`)}
|
||||
type="button"
|
||||
>
|
||||
{option}
|
||||
<span className="text-white/28">›</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SceneToolIcon({ kind }: { kind: "panorama" | "orbit" | "lighting" | "grid" | "hd" | "crop" }) {
|
||||
if (kind === "panorama") {
|
||||
return <span className="font-['Barlow_Condensed',sans-serif] text-[16px] font-bold leading-none text-white/86">720</span>;
|
||||
}
|
||||
if (kind === "hd") {
|
||||
return (
|
||||
<span className="flex h-[22px] min-w-[26px] items-center justify-center rounded-[4px] border border-white/72 px-1 text-[12px] font-bold leading-none text-white/86">
|
||||
HD
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (kind === "grid") {
|
||||
return (
|
||||
<svg aria-hidden="true" className="h-[22px] w-[22px]" fill="none" viewBox="0 0 22 22">
|
||||
<path d="M3.5 4.5h15M3.5 11h15M3.5 17.5h15M4.5 3.5v15M11 3.5v15M17.5 3.5v15" stroke="currentColor" strokeLinecap="round" strokeWidth="1.7" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (kind === "crop") {
|
||||
return (
|
||||
<svg aria-hidden="true" className="h-[22px] w-[22px]" fill="none" viewBox="0 0 22 22">
|
||||
<path d="M6 3.5v12.5h12.5M3.5 6h12.5v12.5M8 8h6" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (kind === "lighting") {
|
||||
return (
|
||||
<svg aria-hidden="true" className="h-[22px] w-[22px]" fill="none" viewBox="0 0 22 22">
|
||||
<path d="M4 7.5h14M6 11h10M8.5 14.5h5" stroke="currentColor" strokeLinecap="round" strokeWidth="1.8" />
|
||||
<path d="M16.5 4.5v3M15 6h3" stroke="currentColor" strokeLinecap="round" strokeWidth="1.5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg aria-hidden="true" className="h-[22px] w-[22px]" fill="none" viewBox="0 0 22 22">
|
||||
<path d="M16.8 7.1a6.2 6.2 0 0 0-11.6 1.8M5.2 8.9H2.9l1.7 1.7M5.2 14.9a6.2 6.2 0 0 0 11.6-1.8M16.8 13.1h2.3l-1.7-1.7" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" />
|
||||
<circle cx="11" cy="11" r="2.2" stroke="currentColor" strokeWidth="1.6" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function CharacterImagePanel({ imageUrl, label, wide = false }: { imageUrl: string; label: string; wide?: boolean }) {
|
||||
return (
|
||||
<div className={cn("relative", wide ? "w-[734px]" : "w-[556px]")}>
|
||||
<div className="mb-2 flex items-center gap-1 text-[20px] font-semibold text-white/50">
|
||||
<ImageIcon className="h-5 w-5" />
|
||||
{label}
|
||||
</div>
|
||||
<div className="relative h-[410px] overflow-hidden rounded-[10px] border border-white/14 bg-white shadow-[0_18px_60px_rgba(0,0,0,0.36)]">
|
||||
<img alt={label} className={cn("h-full w-full", wide ? "object-cover" : "object-contain")} src={imageUrl} />
|
||||
{wide ? (
|
||||
<button
|
||||
className="absolute right-3 top-3 flex h-10 w-10 items-center justify-center rounded-[8px] bg-black/56 text-white/90"
|
||||
onClick={() => toast.info("上传替换素材接口待接入。")}
|
||||
type="button"
|
||||
>
|
||||
↥
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
className="absolute -right-[28px] top-[222px] flex h-7 w-7 items-center justify-center rounded-full border border-white/45 bg-[#222] text-[22px] leading-none text-white/72"
|
||||
onClick={() => toast.info(`${label}连接点接口待接入。`)}
|
||||
type="button"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PromptComposer() {
|
||||
const [menu, setMenu] = useState<ComposerMenu>(null);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto absolute bottom-[46px] left-1/2 h-[310px] w-[972px] -translate-x-1/2 rounded-[18px] border border-white/8 bg-[#262626]/98 p-5 text-white shadow-[0_24px_84px_rgba(0,0,0,0.48)]">
|
||||
<button className="absolute right-5 top-5 text-[26px] text-white/45" onClick={() => toast.info("展开提示词编辑器待接入。")} type="button">↗</button>
|
||||
<div className="flex items-center gap-3">
|
||||
{[
|
||||
["style", "◇", "风格"],
|
||||
["mark", "⌖", "标记"],
|
||||
["focus", "▣", "聚焦"],
|
||||
].map(([type, icon, label]) => (
|
||||
<PromptActionButton
|
||||
active={menu === type}
|
||||
icon={icon}
|
||||
key={label}
|
||||
label={label}
|
||||
onClick={() => setMenu((current) => current === type ? null : type as ComposerMenu)}
|
||||
/>
|
||||
))}
|
||||
<div className="relative h-[70px] w-[70px] overflow-hidden rounded-[10px]">
|
||||
<img alt="角色图缩略图" className="h-full w-full object-cover" src={CHARACTER_IMAGE_URL} />
|
||||
<span className="absolute right-0 top-0 flex h-5 min-w-5 items-center justify-center rounded-bl-[8px] bg-black/50 px-1 text-xs">1</span>
|
||||
</div>
|
||||
</div>
|
||||
{menu ? <ComposerPopover menu={menu} /> : null}
|
||||
|
||||
<div className="mt-6 text-[22px] text-white/46">
|
||||
描述你想要生成的画面内容,按 / 呼出指令,@引用素材
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-5 left-5 right-5 flex items-center gap-6 text-[18px] text-white/78">
|
||||
<ComposerControl active={menu === "model"} label="Lib Nano Pro" icon="◉" dropdown onClick={() => setMenu((current) => current === "model" ? null : "model")} />
|
||||
<ComposerControl active={menu === "ratio"} label="16:9" icon="▭" dropdown onClick={() => setMenu((current) => current === "ratio" ? null : "ratio")} />
|
||||
<ComposerControl active={menu === "resolution"} label="2K" dropdown onClick={() => setMenu((current) => current === "resolution" ? null : "resolution")} />
|
||||
<ComposerControl active={menu === "camera"} label="摄像机" icon="▣" onClick={() => setMenu((current) => current === "camera" ? null : "camera")} />
|
||||
<ComposerControl active={menu === "panorama"} label="全景" icon="♫" onClick={() => setMenu((current) => current === "panorama" ? null : "panorama")} />
|
||||
<ComposerControl active={menu === "translate"} label="文" onClick={() => setMenu((current) => current === "translate" ? null : "translate")} />
|
||||
<ComposerControl active={menu === "settings"} label="" icon="⌘" onClick={() => setMenu((current) => current === "settings" ? null : "settings")} />
|
||||
<ComposerControl active={menu === "count"} label="1张" dropdown onClick={() => setMenu((current) => current === "count" ? null : "count")} />
|
||||
<ComposerControl label="14" icon="⚡" muted onClick={() => toast.info("积分估算接口待接入。")} />
|
||||
<button
|
||||
className="ml-auto flex h-11 w-11 items-center justify-center rounded-[12px] bg-white/76 text-[24px] text-black hover:bg-white"
|
||||
onClick={() => toast.info("角色三视图生成接口待接入。")}
|
||||
type="button"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PromptActionButton({
|
||||
active,
|
||||
icon,
|
||||
label,
|
||||
onClick,
|
||||
}: {
|
||||
active: boolean;
|
||||
icon: string;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"flex h-[72px] w-[74px] flex-col items-center justify-center rounded-[10px] border border-white/10 bg-[#292929] text-[15px] text-white/58",
|
||||
active && "border-white/20 bg-white/10 text-white"
|
||||
)}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
>
|
||||
<span className="text-[24px] leading-none">{icon}</span>
|
||||
<span className="mt-2">{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ComposerPopover({ menu }: { menu: Exclude<ComposerMenu, null> }) {
|
||||
const options: Record<Exclude<ComposerMenu, "focus" | null>, string[]> = {
|
||||
camera: ["摄像机", "手持镜头", "固定镜头"],
|
||||
count: ["1张", "2张", "4张"],
|
||||
mark: ["添加人物标记", "添加构图标记", "添加动作标记"],
|
||||
model: ["Lib Nano Pro", "Lib Seedream Pro", "Lib Universal"],
|
||||
panorama: ["全景", "半身", "近景"],
|
||||
ratio: ["16:9", "9:16", "1:1", "4:3"],
|
||||
resolution: ["1K", "2K", "4K"],
|
||||
settings: ["提示词增强", "参考图权重", "高级参数"],
|
||||
style: ["写实", "电影感", "国风", "柔和自然光"],
|
||||
translate: ["中文", "English", "自动翻译"],
|
||||
};
|
||||
|
||||
if (menu === "focus") {
|
||||
return (
|
||||
<div className="absolute left-[178px] top-[102px] z-50 w-[308px] rounded-[14px] border border-white/10 bg-[#1f1f1f] p-4 shadow-[0_24px_80px_rgba(0,0,0,0.5)]">
|
||||
<div className="font-semibold text-[14px] text-white/86">聚焦模式</div>
|
||||
<div className="mt-2 rounded-[10px] border border-dashed border-white/14 bg-white/5 px-3 py-4 text-[13px] text-white/46">
|
||||
请在图片上框选聚焦区域
|
||||
</div>
|
||||
<button className="mt-3 h-9 w-full rounded-[10px] bg-white text-[13px] font-semibold text-black" onClick={() => toast.info("聚焦区域编辑接口待接入。")} type="button">
|
||||
进入聚焦编辑
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute left-5 top-[102px] z-50 w-[260px] rounded-[14px] border border-white/10 bg-[#1f1f1f] p-2 shadow-[0_24px_80px_rgba(0,0,0,0.5)]">
|
||||
{options[menu].map((option) => (
|
||||
<button
|
||||
className="flex h-9 w-full items-center justify-between rounded-[9px] px-3 text-left text-[13px] text-white/72 hover:bg-white/8 hover:text-white"
|
||||
key={option}
|
||||
onClick={() => toast.info(`${option} 参数接口待接入。`)}
|
||||
type="button"
|
||||
>
|
||||
{option}
|
||||
<span className="text-white/28">›</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ComposerControl({
|
||||
active,
|
||||
dropdown,
|
||||
icon,
|
||||
label,
|
||||
muted = false,
|
||||
onClick,
|
||||
}: {
|
||||
active?: boolean;
|
||||
dropdown?: boolean;
|
||||
icon?: string;
|
||||
label: string;
|
||||
muted?: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center gap-2 whitespace-nowrap rounded-[8px] px-1.5 py-1 text-white/78 hover:bg-white/8",
|
||||
muted && "text-white/42",
|
||||
active && "bg-white/10 text-white"
|
||||
)}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
>
|
||||
{icon ? <span>{icon}</span> : null}
|
||||
{label ? <span>{label}</span> : null}
|
||||
{dropdown ? <span className="text-white/42">⌄</span> : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function CloneEdges({ edges, nodes }: { edges: CloneEdge[]; nodes: CloneNode[] }) {
|
||||
const nodeMap = new Map(nodes.map((node) => [node.id, node]));
|
||||
return (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user