Anchor preset controls to selected media
This commit is contained in:
parent
8784432f23
commit
6f64164109
@ -161,6 +161,12 @@ type NodeContextState = {
|
||||
screenY: number;
|
||||
} | null;
|
||||
|
||||
type PresetControlAnchor = {
|
||||
bottomY: number;
|
||||
centerX: number;
|
||||
topY: number;
|
||||
};
|
||||
|
||||
type CanvasCreateKind =
|
||||
| "image-input"
|
||||
| "video-input"
|
||||
@ -1302,7 +1308,17 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
}, []);
|
||||
|
||||
const isBusy = isLoading || isProjectLoading;
|
||||
const selectedPresetMode = getPresetMode(selectedNode);
|
||||
const selectedPresetControlNode =
|
||||
selectedNode?.kind === "media" && selectedNode.inputType !== "audio" ? selectedNode : null;
|
||||
const selectedPresetMode = getPresetMode(selectedPresetControlNode);
|
||||
const selectedPresetAnchor = useMemo<PresetControlAnchor | null>(() => {
|
||||
if (!selectedPresetControlNode) return null;
|
||||
return {
|
||||
bottomY: Math.round(viewport.y + (selectedPresetControlNode.y + selectedPresetControlNode.height) * viewport.zoom),
|
||||
centerX: Math.round(viewport.x + (selectedPresetControlNode.x + selectedPresetControlNode.width / 2) * viewport.zoom),
|
||||
topY: Math.round(viewport.y + selectedPresetControlNode.y * viewport.zoom),
|
||||
};
|
||||
}, [selectedPresetControlNode, viewport.x, viewport.y, viewport.zoom]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -1401,7 +1417,9 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{selectedPresetMode ? <PresetWorkflowControls mode={selectedPresetMode} /> : null}
|
||||
{selectedPresetMode && selectedPresetAnchor ? (
|
||||
<PresetWorkflowControls anchor={selectedPresetAnchor} mode={selectedPresetMode} />
|
||||
) : null}
|
||||
|
||||
<TopBar
|
||||
draftTitle={draftTitle}
|
||||
@ -1930,12 +1948,18 @@ function MagnetGlyph() {
|
||||
);
|
||||
}
|
||||
|
||||
function PresetWorkflowControls({ mode }: { mode: Exclude<ActiveStarterScene, null> }) {
|
||||
function PresetWorkflowControls({
|
||||
anchor,
|
||||
mode,
|
||||
}: {
|
||||
anchor: PresetControlAnchor;
|
||||
mode: Exclude<ActiveStarterScene, null>;
|
||||
}) {
|
||||
if (mode === "character-three-view") {
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
|
||||
<CharacterPresetToolbar />
|
||||
<PromptComposer />
|
||||
<CharacterPresetToolbar anchor={anchor} />
|
||||
<PromptComposer anchor={anchor} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1944,6 +1968,7 @@ function PresetWorkflowControls({ mode }: { mode: Exclude<ActiveStarterScene, nu
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
|
||||
<PresetFloatingToolbar
|
||||
anchor={anchor}
|
||||
items={[
|
||||
["拆分场景", "按镜号拆分脚本接口待接入。"],
|
||||
["生成分镜", "分镜生成接口待接入。"],
|
||||
@ -1952,6 +1977,7 @@ function PresetWorkflowControls({ mode }: { mode: Exclude<ActiveStarterScene, nu
|
||||
]}
|
||||
/>
|
||||
<PresetComposerPanel
|
||||
anchor={anchor}
|
||||
credit="8"
|
||||
prompt={STORY_SCRIPT_PROMPT}
|
||||
submitMessage="故事脚本生成接口待接入。"
|
||||
@ -1965,6 +1991,7 @@ function PresetWorkflowControls({ mode }: { mode: Exclude<ActiveStarterScene, nu
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
|
||||
<PresetFloatingToolbar
|
||||
anchor={anchor}
|
||||
items={[
|
||||
["高清", "首帧高清接口待接入。"],
|
||||
["扩图", "首帧扩图接口待接入。"],
|
||||
@ -1973,6 +2000,7 @@ function PresetWorkflowControls({ mode }: { mode: Exclude<ActiveStarterScene, nu
|
||||
]}
|
||||
/>
|
||||
<PresetComposerPanel
|
||||
anchor={anchor}
|
||||
credit="28"
|
||||
prompt="以当前图为首帧生成视频。"
|
||||
submitMessage="首帧图生视频接口待接入。"
|
||||
@ -1986,6 +2014,7 @@ function PresetWorkflowControls({ mode }: { mode: Exclude<ActiveStarterScene, nu
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-30" data-canvas-overlay="true">
|
||||
<PresetFloatingToolbar
|
||||
anchor={anchor}
|
||||
items={[
|
||||
["节奏匹配", "音频节奏解析接口待接入。"],
|
||||
["镜头语言", "镜头语言配置接口待接入。"],
|
||||
@ -1994,6 +2023,7 @@ function PresetWorkflowControls({ mode }: { mode: Exclude<ActiveStarterScene, nu
|
||||
]}
|
||||
/>
|
||||
<PresetComposerPanel
|
||||
anchor={anchor}
|
||||
credit="32"
|
||||
prompt={AUDIO_TO_VIDEO_PROMPT}
|
||||
submitMessage="音频生视频接口待接入。"
|
||||
@ -2004,12 +2034,20 @@ function PresetWorkflowControls({ mode }: { mode: Exclude<ActiveStarterScene, nu
|
||||
);
|
||||
}
|
||||
|
||||
function CharacterPresetToolbar() {
|
||||
function CharacterPresetToolbar({ anchor }: { anchor?: PresetControlAnchor }) {
|
||||
const [toolbarMenu, setToolbarMenu] = useState<SceneToolbarMenu>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pointer-events-auto absolute left-1/2 top-[168px] flex h-[58px] origin-top -translate-x-1/2 scale-[0.7] items-center gap-5 rounded-[14px] border border-white/8 bg-[#262626]/96 px-5 text-[16px] text-white/82 shadow-[0_20px_64px_rgba(0,0,0,0.42)] backdrop-blur-[10px]">
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-auto absolute flex h-[58px] items-center gap-5 rounded-[14px] border border-white/8 bg-[#262626]/96 px-5 text-[16px] text-white/82 shadow-[0_20px_64px_rgba(0,0,0,0.42)] backdrop-blur-[10px]",
|
||||
anchor
|
||||
? "origin-bottom -translate-x-1/2 -translate-y-full scale-[0.7]"
|
||||
: "left-1/2 top-[168px] origin-top -translate-x-1/2 scale-[0.7]"
|
||||
)}
|
||||
style={anchor ? { left: anchor.centerX, top: anchor.topY - 14 } : undefined}
|
||||
>
|
||||
<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")} />
|
||||
@ -2028,7 +2066,7 @@ function CharacterPresetToolbar() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{toolbarMenu ? <SceneToolbarPopover menu={toolbarMenu} onClose={() => setToolbarMenu(null)} /> : null}
|
||||
{toolbarMenu ? <SceneToolbarPopover anchor={anchor} menu={toolbarMenu} onClose={() => setToolbarMenu(null)} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -2116,9 +2154,23 @@ function AudioToVideoScene() {
|
||||
);
|
||||
}
|
||||
|
||||
function PresetFloatingToolbar({ items }: { items: Array<[string, string]> }) {
|
||||
function PresetFloatingToolbar({
|
||||
anchor,
|
||||
items,
|
||||
}: {
|
||||
anchor?: PresetControlAnchor;
|
||||
items: Array<[string, string]>;
|
||||
}) {
|
||||
return (
|
||||
<div className="pointer-events-auto absolute left-1/2 top-[94px] flex h-[58px] -translate-x-1/2 items-center gap-2 rounded-[16px] border border-white/8 bg-[#262626]/96 px-4 text-white/78 shadow-[0_20px_64px_rgba(0,0,0,0.42)] backdrop-blur-[10px]">
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-auto absolute flex h-[58px] items-center gap-2 rounded-[16px] border border-white/8 bg-[#262626]/96 px-4 text-white/78 shadow-[0_20px_64px_rgba(0,0,0,0.42)] backdrop-blur-[10px]",
|
||||
anchor
|
||||
? "origin-bottom -translate-x-1/2 -translate-y-full scale-[0.7]"
|
||||
: "left-1/2 top-[94px] -translate-x-1/2"
|
||||
)}
|
||||
style={anchor ? { left: anchor.centerX, top: anchor.topY - 14 } : undefined}
|
||||
>
|
||||
{items.map(([label, message]) => (
|
||||
<button
|
||||
className="flex h-10 items-center gap-2 rounded-[10px] px-3 text-[15px] hover:bg-white/8 hover:text-white"
|
||||
@ -2312,12 +2364,14 @@ function AudioResourcePanel() {
|
||||
}
|
||||
|
||||
function PresetComposerPanel({
|
||||
anchor,
|
||||
credit,
|
||||
prompt,
|
||||
submitMessage,
|
||||
thumbnailUrl,
|
||||
title,
|
||||
}: {
|
||||
anchor?: PresetControlAnchor;
|
||||
credit: string;
|
||||
prompt: string;
|
||||
submitMessage: string;
|
||||
@ -2331,7 +2385,15 @@ function PresetComposerPanel({
|
||||
}, [prompt]);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto absolute bottom-[64px] left-1/2 h-[238px] w-[895px] origin-bottom -translate-x-1/2 scale-[0.5] rounded-[18px] border border-white/8 bg-[#262626]/98 p-5 text-white shadow-[0_24px_84px_rgba(0,0,0,0.48)]">
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-auto absolute h-[238px] w-[895px] rounded-[18px] border border-white/8 bg-[#262626]/98 p-5 text-white shadow-[0_24px_84px_rgba(0,0,0,0.48)]",
|
||||
anchor
|
||||
? "origin-top -translate-x-1/2 scale-[0.5]"
|
||||
: "bottom-[64px] left-1/2 origin-bottom -translate-x-1/2 scale-[0.5]"
|
||||
)}
|
||||
style={anchor ? { left: anchor.centerX, top: anchor.bottomY + 18 } : undefined}
|
||||
>
|
||||
<button className="absolute right-5 top-5 text-[26px] text-white/45" onClick={() => toast.info("展开提示词编辑器待接入。")} type="button">↗</button>
|
||||
<div className="mb-5 flex items-center gap-3">
|
||||
<span className="rounded-[10px] border border-white/10 bg-[#292929] px-3 py-2 text-[14px] text-white/62">{title}</span>
|
||||
@ -2446,9 +2508,11 @@ function SceneTool({
|
||||
}
|
||||
|
||||
function SceneToolbarPopover({
|
||||
anchor,
|
||||
menu,
|
||||
onClose,
|
||||
}: {
|
||||
anchor?: PresetControlAnchor;
|
||||
menu: Exclude<SceneToolbarMenu, null>;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
@ -2462,7 +2526,15 @@ function SceneToolbarPopover({
|
||||
|
||||
if (menu === "lighting") {
|
||||
return (
|
||||
<div className="pointer-events-auto absolute left-1/2 top-[214px] z-40 w-[420px] origin-top -translate-x-1/2 scale-[0.7] 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={cn(
|
||||
"pointer-events-auto absolute z-40 w-[420px] 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]",
|
||||
anchor
|
||||
? "origin-top -translate-x-1/2 scale-[0.7]"
|
||||
: "left-1/2 top-[214px] origin-top -translate-x-1/2 scale-[0.7]"
|
||||
)}
|
||||
style={anchor ? { left: anchor.centerX, top: anchor.topY + 8 } : undefined}
|
||||
>
|
||||
<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>
|
||||
@ -2483,7 +2555,15 @@ function SceneToolbarPopover({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto absolute left-1/2 top-[214px] z-40 w-[260px] origin-top -translate-x-1/2 scale-[0.7] 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]">
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-auto absolute z-40 w-[260px] 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]",
|
||||
anchor
|
||||
? "origin-top -translate-x-1/2 scale-[0.7]"
|
||||
: "left-1/2 top-[214px] origin-top -translate-x-1/2 scale-[0.7]"
|
||||
)}
|
||||
style={anchor ? { left: anchor.centerX, top: anchor.topY + 8 } : undefined}
|
||||
>
|
||||
{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"
|
||||
@ -2570,12 +2650,20 @@ function CharacterImagePanel({ imageUrl, label, wide = false }: { imageUrl: stri
|
||||
);
|
||||
}
|
||||
|
||||
function PromptComposer() {
|
||||
function PromptComposer({ anchor }: { anchor?: PresetControlAnchor }) {
|
||||
const [menu, setMenu] = useState<ComposerMenu>(null);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto absolute bottom-[64px] left-1/2 h-[286px] w-[895px] origin-bottom -translate-x-1/2 scale-[0.5] rounded-[18px] border border-white/8 bg-[#262626]/98 p-5 text-white shadow-[0_24px_84px_rgba(0,0,0,0.48)]">
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-auto absolute h-[286px] w-[895px] rounded-[18px] border border-white/8 bg-[#262626]/98 p-5 text-white shadow-[0_24px_84px_rgba(0,0,0,0.48)]",
|
||||
anchor
|
||||
? "origin-top -translate-x-1/2 scale-[0.5]"
|
||||
: "bottom-[64px] left-1/2 origin-bottom -translate-x-1/2 scale-[0.5]"
|
||||
)}
|
||||
style={anchor ? { left: anchor.centerX, top: anchor.bottomY + 18 } : undefined}
|
||||
>
|
||||
<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">
|
||||
{[
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user