Match LibTV added module node cards
This commit is contained in:
parent
46cd4a5899
commit
d2199975a5
@ -19,6 +19,7 @@ import {
|
||||
Scissors,
|
||||
Settings2,
|
||||
Share2,
|
||||
Sparkles,
|
||||
Trash2,
|
||||
Upload,
|
||||
UserRound,
|
||||
@ -216,6 +217,15 @@ const ADD_NODE_MODULES: Array<{
|
||||
{ kind: "script", label: "脚本", description: "创意脚本、生成故事板", icon: Layers, badge: "Beta" },
|
||||
];
|
||||
|
||||
const TARGET_MODULE_CENTER_OFFSETS: Record<AddModuleKind, { x: number; y: number }> = {
|
||||
audio: { x: 705, y: -257 },
|
||||
image: { x: 53, y: -257 },
|
||||
script: { x: 556, y: 253 },
|
||||
text: { x: -742, y: -257 },
|
||||
video: { x: -574, y: 253 },
|
||||
"video-clip": { x: 80, y: 253 },
|
||||
};
|
||||
|
||||
const STARTER_CARDS = [
|
||||
{
|
||||
title: "故事脚本生成",
|
||||
@ -655,13 +665,13 @@ function createTargetModuleNode(kind: AddModuleKind, x: number, y: number, index
|
||||
return {
|
||||
id: makeId("script"),
|
||||
kind: "script",
|
||||
title: `脚本 ${index}`,
|
||||
title: "脚本生成器",
|
||||
subtitle: "脚本生成",
|
||||
inputType: "text",
|
||||
x,
|
||||
y,
|
||||
width: 520,
|
||||
height: 360,
|
||||
width: 438,
|
||||
height: 470,
|
||||
accent: "#f0d06b",
|
||||
draftValues: {
|
||||
action: "SCRIPT_GENERATE",
|
||||
@ -683,8 +693,8 @@ function createTargetModuleNode(kind: AddModuleKind, x: number, y: number, index
|
||||
inputType: "video",
|
||||
x,
|
||||
y,
|
||||
width: 210,
|
||||
height: 116,
|
||||
width: 436,
|
||||
height: 470,
|
||||
accent: "#ffb8c8",
|
||||
draftValues: {
|
||||
action: "VIDEO_CLIP_RESOURCE",
|
||||
@ -737,6 +747,7 @@ function createTargetModuleNode(kind: AddModuleKind, x: number, y: number, index
|
||||
},
|
||||
};
|
||||
const item = meta[kind];
|
||||
const isWideModule = item.nodeType === "image" || item.nodeType === "video";
|
||||
return {
|
||||
id: makeId(kind),
|
||||
kind: item.kind,
|
||||
@ -745,8 +756,8 @@ function createTargetModuleNode(kind: AddModuleKind, x: number, y: number, index
|
||||
inputType: item.nodeType,
|
||||
x,
|
||||
y,
|
||||
width: item.nodeType === "text" ? 210 : 194,
|
||||
height: item.nodeType === "text" ? 122 : 116,
|
||||
width: isWideModule ? 773 : 436,
|
||||
height: 470,
|
||||
accent: item.accent,
|
||||
draftValues: {
|
||||
action: item.action,
|
||||
@ -814,6 +825,22 @@ function getPresetMode(node: CloneNode | null | undefined): ActiveStarterScene {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getTargetModuleKind(node: CloneNode): AddModuleKind | null {
|
||||
if (node.kind === "media" || getPresetMode(node)) return null;
|
||||
const nodeType = getDraftString(node, "nodeType");
|
||||
if (
|
||||
nodeType === "text" ||
|
||||
nodeType === "image" ||
|
||||
nodeType === "video" ||
|
||||
nodeType === "video-clip" ||
|
||||
nodeType === "audio" ||
|
||||
nodeType === "script"
|
||||
) {
|
||||
return nodeType;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function createPresetMediaNode({
|
||||
action,
|
||||
generated = false,
|
||||
@ -1156,7 +1183,13 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
|
||||
|
||||
const addTargetModule = useCallback(
|
||||
(kind: AddModuleKind, point?: { x: number; y: number }) => {
|
||||
const center = point || toCanvasPoint(window.innerWidth / 2, window.innerHeight / 2);
|
||||
const viewportCenter = toCanvasPoint(window.innerWidth / 2, window.innerHeight / 2);
|
||||
const offset = TARGET_MODULE_CENTER_OFFSETS[kind];
|
||||
const sameKindCount = nodes.filter((node) => getTargetModuleKind(node) === kind).length;
|
||||
const center = point || {
|
||||
x: viewportCenter.x + offset.x + sameKindCount * 28,
|
||||
y: viewportCenter.y + offset.y + sameKindCount * 28,
|
||||
};
|
||||
const created = createTargetModuleNode(kind, 0, 0, nodes.length + 1);
|
||||
const node = {
|
||||
...created,
|
||||
@ -3376,6 +3409,15 @@ function CloneCanvasNode({
|
||||
onPointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
selected: boolean;
|
||||
}) {
|
||||
const targetModuleKind = getTargetModuleKind(node);
|
||||
if (targetModuleKind) {
|
||||
return (
|
||||
<CanvasNodeShell node={node} onContextMenu={onContextMenu} onPointerDown={onPointerDown} selected={selected}>
|
||||
<TargetModuleCanvasNode kind={targetModuleKind} node={node} selected={selected} />
|
||||
</CanvasNodeShell>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.kind === "media") {
|
||||
return (
|
||||
<CanvasNodeShell node={node} onContextMenu={onContextMenu} onPointerDown={onPointerDown} selected={selected}>
|
||||
@ -3451,6 +3493,133 @@ function CloneCanvasNode({
|
||||
);
|
||||
}
|
||||
|
||||
function TargetModuleCanvasNode({
|
||||
kind,
|
||||
node,
|
||||
selected,
|
||||
}: {
|
||||
kind: AddModuleKind;
|
||||
node: CloneNode;
|
||||
selected: boolean;
|
||||
}) {
|
||||
const HeaderIcon = kind === "video-clip" ? Scissors : kind === "audio" ? Music2 : kind === "video" ? Video : kind === "image" ? ImageIcon : FileText;
|
||||
const actions: Record<AddModuleKind, Array<{ icon: LucideIcon | "hd"; label: string }>> = {
|
||||
audio: [{ icon: Play, label: "音频生视频" }],
|
||||
image: [
|
||||
{ icon: Upload, label: "图生图" },
|
||||
{ icon: "hd", label: "图片高清" },
|
||||
],
|
||||
script: [
|
||||
{ icon: FileText, label: "脚本生成分镜脚本" },
|
||||
{ icon: Video, label: "视频参考生成分镜脚本" },
|
||||
{ icon: UserRound, label: "角色生成分镜脚本" },
|
||||
],
|
||||
text: [
|
||||
{ icon: FileText, label: "自己编写内容" },
|
||||
{ icon: Play, label: "文生视频" },
|
||||
{ icon: ImageIcon, label: "图片反推提示词" },
|
||||
{ icon: Music2, label: "文字生音乐" },
|
||||
],
|
||||
video: [
|
||||
{ icon: Layers, label: "首尾帧生成视频" },
|
||||
{ icon: Sparkles, label: "首帧生成视频" },
|
||||
],
|
||||
"video-clip": [],
|
||||
};
|
||||
const actionTop =
|
||||
kind === "text"
|
||||
? "top-[166px]"
|
||||
: kind === "image" || kind === "video"
|
||||
? "top-[220px]"
|
||||
: kind === "script"
|
||||
? "top-[190px]"
|
||||
: "top-[190px]";
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<div className="mb-[10px] flex h-7 items-center gap-2 text-[22px] font-semibold leading-none text-white/44">
|
||||
<HeaderIcon className="h-[22px] w-[22px]" />
|
||||
<span>{node.title}</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"relative h-[432px] overflow-hidden rounded-[8px] border border-white/8 bg-[#252525] shadow-[0_16px_38px_rgba(0,0,0,0.24)]",
|
||||
selected && "outline outline-1 outline-white/22"
|
||||
)}
|
||||
>
|
||||
<div className={cn("absolute inset-x-0 flex justify-center text-white/18", kind === "text" || kind === "script" ? "top-[62px]" : "top-[116px]")}>
|
||||
<TargetModuleHeroGraphic kind={kind} />
|
||||
</div>
|
||||
{kind === "video-clip" ? (
|
||||
<div className="absolute inset-x-0 top-[254px] text-center text-[16px] text-white/34">
|
||||
空空如也,请连接多个视频节点后操作
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn("absolute left-8", actionTop)}>
|
||||
<div className="mb-5 text-[17px] text-white/42">尝试:</div>
|
||||
<div className="space-y-5">
|
||||
{actions[kind].map((action) => (
|
||||
<button
|
||||
className="flex items-center gap-3 text-[18px] leading-none text-white/82 hover:text-white"
|
||||
key={action.label}
|
||||
onClick={() => toast.info(`${action.label} 接口待接入。`)}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
type="button"
|
||||
>
|
||||
<ModuleActionIcon icon={action.icon} />
|
||||
<span>{action.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TargetModuleHeroGraphic({ kind }: { kind: AddModuleKind }) {
|
||||
if (kind === "text" || kind === "script") {
|
||||
return (
|
||||
<div className="flex w-[92px] flex-col gap-[9px]">
|
||||
{[70, 70, 70, 70, 43].map((width, index) => (
|
||||
<span className="h-[7px] rounded-full bg-white/16" key={`${width}-${index}`} style={{ width }} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (kind === "image") {
|
||||
return <ImageIcon className="h-[92px] w-[92px]" strokeWidth={1.5} />;
|
||||
}
|
||||
|
||||
if (kind === "audio") {
|
||||
return <Music2 className="h-[86px] w-[86px]" strokeWidth={1.5} />;
|
||||
}
|
||||
|
||||
if (kind === "video") {
|
||||
return (
|
||||
<span className="flex h-20 w-20 items-center justify-center rounded-[8px] bg-white/18 text-[#242424]">
|
||||
<Play className="ml-1 h-11 w-11 fill-current" strokeWidth={0} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <Scissors className="h-[92px] w-[92px]" strokeWidth={1.4} />;
|
||||
}
|
||||
|
||||
function ModuleActionIcon({ icon }: { icon: LucideIcon | "hd" }) {
|
||||
if (icon === "hd") {
|
||||
return (
|
||||
<span className="flex h-[18px] min-w-[24px] items-center justify-center rounded-[3px] border border-white/72 px-1 text-[10px] font-bold leading-none text-white/86">
|
||||
HD
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const Icon = icon;
|
||||
return <Icon className="h-[18px] w-[18px] shrink-0 text-white/86" strokeWidth={2} />;
|
||||
}
|
||||
|
||||
function CanvasNodeShell({
|
||||
children,
|
||||
node,
|
||||
@ -3464,10 +3633,12 @@ function CanvasNodeShell({
|
||||
onPointerDown: (event: ReactPointerEvent<HTMLDivElement>) => void;
|
||||
selected: boolean;
|
||||
}) {
|
||||
const targetModuleKind = getTargetModuleKind(node);
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute cursor-grab select-none shadow-[0_14px_34px_rgba(0,0,0,0.35)] active:cursor-grabbing",
|
||||
"absolute cursor-grab select-none active:cursor-grabbing",
|
||||
targetModuleKind ? "shadow-none" : "shadow-[0_14px_34px_rgba(0,0,0,0.35)]",
|
||||
selected && "z-20"
|
||||
)}
|
||||
data-canvas-node="true"
|
||||
@ -3476,8 +3647,21 @@ function CanvasNodeShell({
|
||||
onPointerDown={onPointerDown}
|
||||
style={{ height: node.height, left: node.x, top: node.y, width: node.width }}
|
||||
>
|
||||
<span className="absolute left-[-10px] top-1/2 z-10 h-5 w-5 -translate-y-1/2 rounded-full border border-white/30 bg-white" />
|
||||
<span className="absolute right-[-10px] top-1/2 z-10 h-5 w-5 -translate-y-1/2 rounded-full border border-white/30 bg-white" />
|
||||
{targetModuleKind ? (
|
||||
<>
|
||||
<span className="absolute left-[-13px] top-1/2 z-10 mt-[14px] flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full border border-white/18 bg-[#242424] text-[20px] leading-none text-white/50">
|
||||
+
|
||||
</span>
|
||||
<span className="absolute right-[-13px] top-1/2 z-10 mt-[14px] flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full border border-white/18 bg-[#242424] text-[20px] leading-none text-white/50">
|
||||
+
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="absolute left-[-10px] top-1/2 z-10 h-5 w-5 -translate-y-1/2 rounded-full border border-white/30 bg-white" />
|
||||
<span className="absolute right-[-10px] top-1/2 z-10 h-5 w-5 -translate-y-1/2 rounded-full border border-white/30 bg-white" />
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user