9807295a7d
隐藏生成面板资源名称字段并恢复提示词输入边框 恢复图标图集快速编辑并仅对单图标禁用 补充工具栏右键菜单工作流回归测试与编辑器文档
1254 lines
48 KiB
TypeScript
1254 lines
48 KiB
TypeScript
import { Check, ChevronDown, Cpu, Film, ImageIcon, Music } from 'lucide-react';
|
|
import {
|
|
type CSSProperties,
|
|
type Dispatch,
|
|
type ReactNode,
|
|
type RefObject,
|
|
type SetStateAction,
|
|
useCallback,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
|
|
import { PlatformActionButton } from '../common/PlatformActionButton';
|
|
import {
|
|
PlatformFloatingMenu,
|
|
PlatformFloatingMenuItem,
|
|
} from '../common/PlatformFloatingMenu';
|
|
import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton';
|
|
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
|
import { PlatformTextField } from '../common/PlatformTextField';
|
|
import { ImageCanvasBasicGenerationComposerView } from './ImageCanvasBasicGenerationComposerView';
|
|
import { ImageCanvasCharacterAnimationPanelView } from './ImageCanvasCharacterAnimationPanelView';
|
|
import { ImageCanvasCharacterGenerationComposerView } from './ImageCanvasCharacterGenerationComposerView';
|
|
import { ImageCanvasCropExpandPanelView } from './ImageCanvasCropExpandPanelView';
|
|
import { ImageCanvasEditGenerationModalView } from './ImageCanvasEditGenerationModalView';
|
|
import type {
|
|
CanvasLayer,
|
|
CharacterAnimationPanelState,
|
|
CropExpandPanelState,
|
|
GenerateDialogState,
|
|
QuickEditPanelState,
|
|
SpecFormValues,
|
|
SpecGenerationType,
|
|
UploadTarget,
|
|
} from './ImageCanvasEditorTypes';
|
|
import {
|
|
calculateEditorBackgroundMusicPrice,
|
|
calculateEditorSoundEffectPrice,
|
|
calculateEditorVideoPrice,
|
|
DEFAULT_BACKGROUND_MUSIC_MODEL,
|
|
DEFAULT_SOUND_EFFECT_DURATION_SECONDS,
|
|
DEFAULT_SOUND_EFFECT_MODEL,
|
|
DEFAULT_VIDEO_ASPECT_RATIO,
|
|
DEFAULT_VIDEO_DURATION_SECONDS,
|
|
DEFAULT_VIDEO_SOUND,
|
|
DEFAULT_VIDEO_WEB_SEARCH_ENABLED,
|
|
EDITOR_VIDEO_ASPECT_RATIO_OPTIONS,
|
|
EDITOR_SOUND_EFFECT_MODEL_OPTIONS,
|
|
EDITOR_VIDEO_MODEL_OPTIONS,
|
|
EDITOR_VIDEO_RESOLUTION_OPTIONS,
|
|
VIDEO_MODEL_SEEDANCE_2_FAST,
|
|
resizeGenerationPlaceholderToVideoSelection,
|
|
SPEC_TYPE_LABEL,
|
|
} from './ImageCanvasGenerationModel';
|
|
import { ImageCanvasIconSpritesheetComposerView } from './ImageCanvasIconSpritesheetComposerView';
|
|
import { ImageCanvasPublicationMaterialsDemoPanelView } from './ImageCanvasPublicationMaterialsDemoPanelView';
|
|
import { getPublicationMaterialsWorkflow } from './ImageCanvasPublicationMaterialsModel';
|
|
import { ImageCanvasQuickEditPanelView } from './ImageCanvasQuickEditPanelView';
|
|
import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot';
|
|
import { ImageCanvasSpecGenerationPanelView } from './ImageCanvasSpecGenerationPanelView';
|
|
import { useImageCanvasFloatingOptionDismiss } from './useImageCanvasFloatingOptionDismiss';
|
|
|
|
type ImageCanvasGenerationComposerViewProps = {
|
|
specToolWrapRef: RefObject<HTMLSpanElement | null>;
|
|
publicationReferenceButtonRef: RefObject<HTMLButtonElement | null>;
|
|
characterSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
|
characterReferenceButtonRef: RefObject<HTMLButtonElement | null>;
|
|
generationReferenceButtonRef: RefObject<HTMLButtonElement | null>;
|
|
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
|
isSpecMenuOpen: boolean;
|
|
isGenerationReferenceMenuOpen: boolean;
|
|
isPublicationReferenceMenuOpen: boolean;
|
|
isCharacterSpecMenuOpen: boolean;
|
|
isCharacterReferenceMenuOpen: boolean;
|
|
isIconSpecMenuOpen: boolean;
|
|
isUiDesignSpecMenuOpen: boolean;
|
|
isPickingGenerationReferenceFromCanvas: boolean;
|
|
isPickingQuickEditReferenceFromCanvas: boolean;
|
|
isPickingPublicationReferenceFromCanvas: boolean;
|
|
isPickingCharacterSpecFromCanvas: boolean;
|
|
isPickingCharacterReferenceFromCanvas: boolean;
|
|
isPickingIconSpecFromCanvas: boolean;
|
|
isPickingUiDesignSpecFromCanvas: boolean;
|
|
generateDialog: GenerateDialogState | null;
|
|
generationComposerStyle: CSSProperties | null;
|
|
iconComposerStyle: CSSProperties | null;
|
|
quickEditPanel: QuickEditPanelState | null;
|
|
quickEditSourceLayer: CanvasLayer | null;
|
|
quickEditPanelStyle: CSSProperties | null;
|
|
cropExpandPanel: CropExpandPanelState | null;
|
|
cropExpandSourceLayer: CanvasLayer | null;
|
|
cropExpandPanelStyle: CSSProperties | null;
|
|
characterAnimationPanel: CharacterAnimationPanelState | null;
|
|
characterAnimationSourceLayer: CanvasLayer | null;
|
|
characterAnimationPanelStyle: CSSProperties | null;
|
|
characterAnimationPrice: number;
|
|
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
|
setQuickEditPanel: Dispatch<SetStateAction<QuickEditPanelState | null>>;
|
|
setCropExpandPanel: Dispatch<SetStateAction<CropExpandPanelState | null>>;
|
|
setCharacterAnimationPanel: Dispatch<
|
|
SetStateAction<CharacterAnimationPanelState | null>
|
|
>;
|
|
setIsGenerationReferenceMenuOpen: Dispatch<SetStateAction<boolean>>;
|
|
setIsPublicationReferenceMenuOpen: Dispatch<SetStateAction<boolean>>;
|
|
setIsCharacterSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
|
setIsCharacterReferenceMenuOpen: Dispatch<SetStateAction<boolean>>;
|
|
setIsIconSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
|
setIsUiDesignSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
|
setIsPickingGenerationReferenceFromCanvas: Dispatch<SetStateAction<boolean>>;
|
|
setIsPickingQuickEditReferenceFromCanvas: Dispatch<SetStateAction<boolean>>;
|
|
setIsPickingPublicationReferenceFromCanvas: Dispatch<
|
|
SetStateAction<boolean>
|
|
>;
|
|
setIsPickingCharacterSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
|
setIsPickingCharacterReferenceFromCanvas: Dispatch<SetStateAction<boolean>>;
|
|
setIsPickingIconSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
|
setIsPickingUiDesignSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
|
onOpenSpecDialog: (specType: SpecGenerationType) => void;
|
|
onRequestUpload: (target: UploadTarget) => void;
|
|
onSubmitImageGeneration: (dialog: GenerateDialogState) => void;
|
|
onSubmitIconSpritesheetGeneration: (dialog: GenerateDialogState) => void;
|
|
onSubmitQuickEdit: () => void;
|
|
onSubmitCropExpand: () => void;
|
|
onSubmitCharacterAnimation: () => void;
|
|
onUpdateSpecFormValue: (key: keyof SpecFormValues, value: string) => void;
|
|
onUpdateIconDescriptionText: (value: string) => void;
|
|
onUpdateCharacterAnimationDuration: (frameCountValue: string) => void;
|
|
onRememberImageModel: (model: string) => void;
|
|
onSpecMenuPointerEnter?: () => void;
|
|
onSpecMenuPointerLeave?: () => void;
|
|
};
|
|
|
|
function buildPortalMenuStyle(
|
|
anchor: HTMLElement | null,
|
|
placement: 'above' | 'below',
|
|
): CSSProperties {
|
|
const rect = anchor?.getBoundingClientRect();
|
|
if (!rect) {
|
|
return {
|
|
position: 'fixed',
|
|
left: 0,
|
|
top: 0,
|
|
right: 'auto',
|
|
bottom: 'auto',
|
|
zIndex: 70,
|
|
};
|
|
}
|
|
|
|
return {
|
|
position: 'fixed',
|
|
left: Math.round(rect.left),
|
|
top:
|
|
placement === 'above'
|
|
? Math.round(rect.top)
|
|
: Math.round(rect.bottom + 8),
|
|
right: 'auto',
|
|
bottom: 'auto',
|
|
zIndex: 70,
|
|
transform:
|
|
placement === 'above' ? 'translateY(calc(-100% - 0.45rem))' : undefined,
|
|
};
|
|
}
|
|
|
|
function renderEditorPortal(node: ReactNode) {
|
|
if (typeof document === 'undefined') {
|
|
return node;
|
|
}
|
|
return createPortal(node, document.body);
|
|
}
|
|
|
|
function resetFailedVideoDialogStatus(dialog: GenerateDialogState) {
|
|
return {
|
|
...dialog,
|
|
status: dialog.status === 'failed' ? 'idle' : dialog.status,
|
|
errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage,
|
|
};
|
|
}
|
|
|
|
function isSeedanceVideoModel(model: string | undefined) {
|
|
return model === 'seedance2.0' || model === 'seedance2.0-fast';
|
|
}
|
|
|
|
function VideoReferenceChip({
|
|
reference,
|
|
index,
|
|
onRemove,
|
|
}: {
|
|
reference: {
|
|
id: string;
|
|
label: string;
|
|
src: string;
|
|
mediaType?: string;
|
|
objectKey?: string | null;
|
|
};
|
|
index: number;
|
|
onRemove: () => void;
|
|
}) {
|
|
const mediaType = reference.mediaType ?? 'image';
|
|
const labelPrefix =
|
|
mediaType === 'video'
|
|
? '参考视频'
|
|
: mediaType === 'audio'
|
|
? '参考音频'
|
|
: '参考图';
|
|
const label = reference.label || `${labelPrefix}${index + 1}`;
|
|
const icon =
|
|
mediaType === 'video' ? (
|
|
<Film className="h-4 w-4" aria-hidden="true" />
|
|
) : mediaType === 'audio' ? (
|
|
<Music className="h-4 w-4" aria-hidden="true" />
|
|
) : (
|
|
<ImageIcon className="h-4 w-4" aria-hidden="true" />
|
|
);
|
|
return (
|
|
<ImageCanvasReferenceSlot
|
|
tone={mediaType === 'audio' ? 'audio' : 'video'}
|
|
icon={icon}
|
|
imageSrc={mediaType === 'image' ? reference.src : undefined}
|
|
objectKey={mediaType === 'image' ? reference.objectKey : undefined}
|
|
label={label}
|
|
ariaLabel={label}
|
|
title={reference.label}
|
|
onRemove={onRemove}
|
|
removeLabel={`删除${label}`}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function VideoOptionChoice({
|
|
children,
|
|
selected,
|
|
className,
|
|
ariaLabel,
|
|
disabled,
|
|
onClick,
|
|
}: {
|
|
children: ReactNode;
|
|
selected: boolean;
|
|
className?: string;
|
|
ariaLabel?: string;
|
|
disabled?: boolean;
|
|
onClick?: () => void;
|
|
}) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
className={['image-canvas-editor__option-popover-choice', className]
|
|
.filter(Boolean)
|
|
.join(' ')}
|
|
aria-label={ariaLabel}
|
|
aria-pressed={selected}
|
|
disabled={disabled}
|
|
onClick={onClick}
|
|
>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function ImageCanvasVideoGenerationComposerView({
|
|
dialog,
|
|
style,
|
|
generationReferenceButtonRef,
|
|
isGenerationReferenceMenuOpen,
|
|
setGenerateDialog,
|
|
setIsGenerationReferenceMenuOpen,
|
|
setIsPickingGenerationReferenceFromCanvas,
|
|
renderEditorPortal,
|
|
buildPortalMenuStyle,
|
|
onRequestUpload,
|
|
onSubmit,
|
|
}: {
|
|
dialog: GenerateDialogState;
|
|
style: CSSProperties;
|
|
generationReferenceButtonRef: RefObject<HTMLButtonElement | null>;
|
|
isGenerationReferenceMenuOpen: boolean;
|
|
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
|
setIsGenerationReferenceMenuOpen: Dispatch<SetStateAction<boolean>>;
|
|
setIsPickingGenerationReferenceFromCanvas: Dispatch<SetStateAction<boolean>>;
|
|
renderEditorPortal: (node: ReactNode) => ReactNode;
|
|
buildPortalMenuStyle: (
|
|
anchor: HTMLElement | null,
|
|
placement: 'above' | 'below',
|
|
) => CSSProperties;
|
|
onRequestUpload: (target: UploadTarget) => void;
|
|
onSubmit: (dialog: GenerateDialogState) => void;
|
|
}) {
|
|
const [openPanel, setOpenPanel] = useState<'params' | 'model' | null>(null);
|
|
const paramsButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
const modelButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
const normalizeVideoDuration = (value: number) =>
|
|
Math.min(15, Math.max(4, Math.round(value)));
|
|
const resolution = dialog.videoResolution ?? '480p';
|
|
const durationSeconds = normalizeVideoDuration(
|
|
dialog.videoDurationSeconds ?? DEFAULT_VIDEO_DURATION_SECONDS,
|
|
);
|
|
const soundMode = dialog.videoSound ?? DEFAULT_VIDEO_SOUND;
|
|
const aspectRatio = dialog.videoAspectRatio ?? DEFAULT_VIDEO_ASPECT_RATIO;
|
|
const currentModel =
|
|
EDITOR_VIDEO_MODEL_OPTIONS.find(
|
|
(item) => item.value === dialog.videoModel,
|
|
) ?? EDITOR_VIDEO_MODEL_OPTIONS[0];
|
|
const supportsReferences = isSeedanceVideoModel(currentModel.value);
|
|
const price = calculateEditorVideoPrice(
|
|
currentModel.value,
|
|
resolution,
|
|
durationSeconds,
|
|
);
|
|
const isGenerating = dialog.status === 'generating';
|
|
const closeVideoFloatingPanels = useCallback(() => {
|
|
setOpenPanel(null);
|
|
setIsGenerationReferenceMenuOpen(false);
|
|
}, [setIsGenerationReferenceMenuOpen]);
|
|
|
|
useImageCanvasFloatingOptionDismiss({
|
|
isOpen: openPanel !== null || isGenerationReferenceMenuOpen,
|
|
boundaryRefs: [
|
|
paramsButtonRef,
|
|
modelButtonRef,
|
|
generationReferenceButtonRef,
|
|
],
|
|
onDismiss: closeVideoFloatingPanels,
|
|
});
|
|
|
|
const updateVideoDialog = (patch: Partial<GenerateDialogState>) => {
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'video'
|
|
? resizeGenerationPlaceholderToVideoSelection({
|
|
...resetFailedVideoDialogStatus(currentDialog),
|
|
...patch,
|
|
})
|
|
: currentDialog,
|
|
);
|
|
};
|
|
|
|
const updateVideoModel = (model: GenerateDialogState['videoModel']) => {
|
|
updateVideoDialog({
|
|
videoModel: model,
|
|
videoResolution:
|
|
model === VIDEO_MODEL_SEEDANCE_2_FAST && dialog.videoResolution === '1080p'
|
|
? '720p'
|
|
: dialog.videoResolution,
|
|
...(isSeedanceVideoModel(model) ? {} : { generationReferences: [] }),
|
|
});
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<form
|
|
className="image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image image-canvas-editor__generation-composer--video"
|
|
style={style}
|
|
role="dialog"
|
|
aria-label="生成视频"
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
if (!isGenerating) {
|
|
onSubmit(dialog);
|
|
}
|
|
}}
|
|
>
|
|
{supportsReferences ? (
|
|
<div className="image-canvas-editor__reference-strip">
|
|
{(dialog.generationReferences ?? []).map((reference, index) => (
|
|
<VideoReferenceChip
|
|
key={reference.id}
|
|
reference={reference}
|
|
index={index}
|
|
onRemove={() =>
|
|
updateVideoDialog({
|
|
generationReferences: (
|
|
dialog.generationReferences ?? []
|
|
).filter((item) => item.id !== reference.id),
|
|
})
|
|
}
|
|
/>
|
|
))}
|
|
<ImageCanvasReferenceSlot
|
|
buttonRef={generationReferenceButtonRef}
|
|
tone="video"
|
|
icon={<ImageIcon className="h-4 w-4" aria-hidden="true" />}
|
|
label="参考"
|
|
ariaLabel="添加视频参考素材"
|
|
disabled={isGenerating}
|
|
isAdd
|
|
onClick={() => setIsGenerationReferenceMenuOpen((open) => !open)}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
<PlatformTextField
|
|
variant="textarea"
|
|
aria-label="视频描述"
|
|
value={dialog.prompt}
|
|
disabled={isGenerating}
|
|
placeholder="你希望生成什么视频?"
|
|
size="sm"
|
|
density="compact"
|
|
className="image-canvas-editor__generation-prompt"
|
|
onChange={(event) =>
|
|
updateVideoDialog({ prompt: event.target.value })
|
|
}
|
|
/>
|
|
<div className="image-canvas-editor__generation-composer-footer">
|
|
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--dimensions">
|
|
<PlatformInlineOptionButton
|
|
ref={paramsButtonRef}
|
|
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--dimensions"
|
|
aria-label={`视频参数 ${aspectRatio} · ${durationSeconds}秒 · ${resolution}`}
|
|
aria-expanded={openPanel === 'params'}
|
|
disabled={isGenerating}
|
|
trailingIcon={<ChevronDown className="h-3 w-3" />}
|
|
onClick={() =>
|
|
setOpenPanel((currentPanel) =>
|
|
currentPanel === 'params' ? null : 'params',
|
|
)
|
|
}
|
|
>
|
|
{aspectRatio} · {durationSeconds}秒 · {resolution} ·{' '}
|
|
{soundMode === 'off' ? '静音' : '有声'}
|
|
</PlatformInlineOptionButton>
|
|
{openPanel === 'params'
|
|
? renderEditorPortal(
|
|
<PlatformFloatingMenu
|
|
className="image-canvas-editor__option-popover image-canvas-editor__portal-menu"
|
|
label="视频参数选项"
|
|
placement="top-start"
|
|
style={buildPortalMenuStyle(
|
|
paramsButtonRef.current,
|
|
'above',
|
|
)}
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
>
|
|
<div className="image-canvas-editor__option-popover-sections">
|
|
<section className="image-canvas-editor__option-popover-section">
|
|
<span className="image-canvas-editor__option-popover-title">
|
|
比例
|
|
</span>
|
|
<div className="image-canvas-editor__option-popover-items image-canvas-editor__option-popover-items--card">
|
|
{EDITOR_VIDEO_ASPECT_RATIO_OPTIONS.map((option) => (
|
|
<VideoOptionChoice
|
|
key={option}
|
|
selected={aspectRatio === option}
|
|
className="image-canvas-editor__option-popover-choice--ratio"
|
|
disabled={isGenerating}
|
|
ariaLabel={`比例 ${option}`}
|
|
onClick={() =>
|
|
updateVideoDialog({
|
|
videoAspectRatio: option,
|
|
})
|
|
}
|
|
>
|
|
<span
|
|
className="image-canvas-editor__ratio-wireframe"
|
|
data-ratio={option}
|
|
aria-hidden="true"
|
|
/>
|
|
<span>{option}</span>
|
|
</VideoOptionChoice>
|
|
))}
|
|
</div>
|
|
</section>
|
|
<section className="image-canvas-editor__option-popover-section">
|
|
<span className="image-canvas-editor__option-popover-title">
|
|
时长
|
|
</span>
|
|
<div className="image-canvas-editor__video-duration-slider">
|
|
<input
|
|
type="range"
|
|
min={4}
|
|
max={15}
|
|
step={1}
|
|
value={durationSeconds}
|
|
disabled={isGenerating}
|
|
aria-label="视频时长"
|
|
onChange={(event) =>
|
|
updateVideoDialog({
|
|
videoDurationSeconds: normalizeVideoDuration(
|
|
Number(event.target.value),
|
|
),
|
|
})
|
|
}
|
|
/>
|
|
<span>{durationSeconds}秒</span>
|
|
</div>
|
|
</section>
|
|
<section className="image-canvas-editor__option-popover-section">
|
|
<span className="image-canvas-editor__option-popover-title">
|
|
清晰度
|
|
</span>
|
|
<div className="image-canvas-editor__option-popover-items">
|
|
{EDITOR_VIDEO_RESOLUTION_OPTIONS.map((option) => {
|
|
const available =
|
|
currentModel.value !== VIDEO_MODEL_SEEDANCE_2_FAST ||
|
|
option !== '1080p';
|
|
return (
|
|
<VideoOptionChoice
|
|
key={option}
|
|
selected={resolution === option}
|
|
disabled={isGenerating || !available}
|
|
ariaLabel={`清晰度 ${option}`}
|
|
onClick={() =>
|
|
available
|
|
? updateVideoDialog({
|
|
videoResolution: option,
|
|
})
|
|
: undefined
|
|
}
|
|
>
|
|
{option}
|
|
</VideoOptionChoice>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
<section className="image-canvas-editor__option-popover-section">
|
|
<span className="image-canvas-editor__option-popover-title">
|
|
声音
|
|
</span>
|
|
<div className="image-canvas-editor__video-toggle-row">
|
|
<button
|
|
type="button"
|
|
className="image-canvas-editor__video-toggle"
|
|
aria-pressed={soundMode === 'off'}
|
|
disabled={isGenerating}
|
|
onClick={() =>
|
|
updateVideoDialog({
|
|
videoSound:
|
|
soundMode === 'off' ? 'on' : 'off',
|
|
})
|
|
}
|
|
>
|
|
<span aria-hidden="true" />
|
|
<strong>静音</strong>
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</PlatformFloatingMenu>,
|
|
)
|
|
: null}
|
|
</div>
|
|
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--model">
|
|
<PlatformInlineOptionButton
|
|
ref={modelButtonRef}
|
|
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--model"
|
|
aria-label={`模型 ${currentModel.label}`}
|
|
aria-expanded={openPanel === 'model'}
|
|
disabled={isGenerating}
|
|
trailingIcon={<ChevronDown className="h-3 w-3" />}
|
|
onClick={() =>
|
|
setOpenPanel((currentPanel) =>
|
|
currentPanel === 'model' ? null : 'model',
|
|
)
|
|
}
|
|
>
|
|
<span className="image-canvas-editor__model-trigger-label">
|
|
<span
|
|
className="image-canvas-editor__model-icon"
|
|
aria-hidden="true"
|
|
>
|
|
<Cpu />
|
|
</span>
|
|
<span>{currentModel.label}</span>
|
|
</span>
|
|
</PlatformInlineOptionButton>
|
|
{openPanel === 'model'
|
|
? renderEditorPortal(
|
|
<PlatformFloatingMenu
|
|
className="image-canvas-editor__option-popover image-canvas-editor__option-popover--model image-canvas-editor__portal-menu"
|
|
label="视频模型选项"
|
|
placement="top-start"
|
|
style={buildPortalMenuStyle(
|
|
modelButtonRef.current,
|
|
'above',
|
|
)}
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
>
|
|
<div className="image-canvas-editor__option-popover-items image-canvas-editor__option-popover-items--model">
|
|
{EDITOR_VIDEO_MODEL_OPTIONS.map((option) => {
|
|
const checked = currentModel.value === option.value;
|
|
return (
|
|
<VideoOptionChoice
|
|
key={option.value}
|
|
selected={checked}
|
|
disabled={isGenerating}
|
|
className="image-canvas-editor__option-popover-choice--model"
|
|
ariaLabel={option.label}
|
|
onClick={() => updateVideoModel(option.value)}
|
|
>
|
|
<span
|
|
className="image-canvas-editor__model-icon"
|
|
aria-hidden="true"
|
|
>
|
|
<Cpu />
|
|
</span>
|
|
<span>{option.label}</span>
|
|
<Check
|
|
className="image-canvas-editor__option-selected-check"
|
|
data-visible={checked}
|
|
aria-hidden="true"
|
|
/>
|
|
</VideoOptionChoice>
|
|
);
|
|
})}
|
|
</div>
|
|
</PlatformFloatingMenu>,
|
|
)
|
|
: null}
|
|
</div>
|
|
<PlatformActionButton
|
|
type="submit"
|
|
tone="secondary"
|
|
size="xs"
|
|
shape="pill"
|
|
className="image-canvas-editor__generation-submit"
|
|
disabled={isGenerating}
|
|
aria-label="生成视频"
|
|
>
|
|
{isGenerating ? (
|
|
'生成中'
|
|
) : (
|
|
<>
|
|
<span>生成</span>
|
|
<span className="image-canvas-editor__mud-point-inline">
|
|
{price}泥点
|
|
</span>
|
|
</>
|
|
)}
|
|
</PlatformActionButton>
|
|
</div>
|
|
</form>
|
|
{isGenerationReferenceMenuOpen && supportsReferences
|
|
? renderEditorPortal(
|
|
<PlatformFloatingMenu
|
|
className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu"
|
|
label="参考素材来源"
|
|
placement="top-start"
|
|
style={buildPortalMenuStyle(
|
|
generationReferenceButtonRef.current,
|
|
'above',
|
|
)}
|
|
>
|
|
<PlatformFloatingMenuItem
|
|
onClick={() => {
|
|
setIsGenerationReferenceMenuOpen(false);
|
|
setIsPickingGenerationReferenceFromCanvas(true);
|
|
}}
|
|
>
|
|
从画布中选择
|
|
</PlatformFloatingMenuItem>
|
|
<PlatformFloatingMenuItem
|
|
onClick={() => {
|
|
setIsGenerationReferenceMenuOpen(false);
|
|
setIsPickingGenerationReferenceFromCanvas(false);
|
|
onRequestUpload('video-reference-image');
|
|
}}
|
|
>
|
|
上传图片
|
|
</PlatformFloatingMenuItem>
|
|
<PlatformFloatingMenuItem
|
|
onClick={() => {
|
|
setIsGenerationReferenceMenuOpen(false);
|
|
setIsPickingGenerationReferenceFromCanvas(false);
|
|
onRequestUpload('video-reference-video');
|
|
}}
|
|
>
|
|
上传视频
|
|
</PlatformFloatingMenuItem>
|
|
<PlatformFloatingMenuItem
|
|
onClick={() => {
|
|
setIsGenerationReferenceMenuOpen(false);
|
|
setIsPickingGenerationReferenceFromCanvas(false);
|
|
onRequestUpload('video-reference-audio');
|
|
}}
|
|
>
|
|
上传音频
|
|
</PlatformFloatingMenuItem>
|
|
</PlatformFloatingMenu>,
|
|
)
|
|
: null}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function normalizeSoundDuration(value: number | null | undefined) {
|
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
return DEFAULT_SOUND_EFFECT_DURATION_SECONDS;
|
|
}
|
|
return Math.min(10, Math.max(2, Math.round(value)));
|
|
}
|
|
|
|
function resetFailedAudioDialogStatus(dialog: GenerateDialogState) {
|
|
return {
|
|
...dialog,
|
|
status: dialog.status === 'failed' ? 'idle' : dialog.status,
|
|
errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage,
|
|
};
|
|
}
|
|
|
|
function ImageCanvasAudioGenerationComposerView({
|
|
dialog,
|
|
style,
|
|
setGenerateDialog,
|
|
renderEditorPortal,
|
|
buildPortalMenuStyle,
|
|
onSubmit,
|
|
}: {
|
|
dialog: GenerateDialogState;
|
|
style: CSSProperties;
|
|
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
|
renderEditorPortal: (node: ReactNode) => ReactNode;
|
|
buildPortalMenuStyle: (
|
|
anchor: HTMLElement | null,
|
|
placement: 'above' | 'below',
|
|
) => CSSProperties;
|
|
onSubmit: (dialog: GenerateDialogState) => void;
|
|
}) {
|
|
const [isSoundOptionsOpen, setIsSoundOptionsOpen] = useState(false);
|
|
const soundOptionsButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
const soundModelButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
const isSoundEffect = dialog.mode === 'audio-sound-effect';
|
|
const isGenerating = dialog.status === 'generating';
|
|
const soundModel = dialog.soundModel ?? DEFAULT_SOUND_EFFECT_MODEL;
|
|
const currentSoundModel =
|
|
EDITOR_SOUND_EFFECT_MODEL_OPTIONS.find(
|
|
(option) => option.value === soundModel,
|
|
) ?? EDITOR_SOUND_EFFECT_MODEL_OPTIONS[0];
|
|
const soundDuration = normalizeSoundDuration(dialog.soundDurationSeconds);
|
|
const soundOptionLabel = `${soundDuration}秒`;
|
|
const dialogLabel = isSoundEffect ? '生成游戏音效' : '生成游戏背景音乐';
|
|
const fixedModelLabel = isSoundEffect ? currentSoundModel.label : 'Suno';
|
|
const fixedModelAriaLabel = isSoundEffect
|
|
? `音效模型 ${fixedModelLabel}`
|
|
: `背景音乐模型 ${fixedModelLabel}`;
|
|
const cost = isSoundEffect
|
|
? calculateEditorSoundEffectPrice(soundModel)
|
|
: calculateEditorBackgroundMusicPrice(DEFAULT_BACKGROUND_MUSIC_MODEL);
|
|
|
|
useImageCanvasFloatingOptionDismiss({
|
|
isOpen: isSoundOptionsOpen,
|
|
boundaryRefs: [soundOptionsButtonRef],
|
|
onDismiss: () => setIsSoundOptionsOpen(false),
|
|
});
|
|
|
|
const updateAudioDialog = (patch: Partial<GenerateDialogState>) => {
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === dialog.mode
|
|
? {
|
|
...resetFailedAudioDialogStatus(currentDialog),
|
|
...patch,
|
|
}
|
|
: currentDialog,
|
|
);
|
|
};
|
|
|
|
const updateSoundDuration = (durationSeconds: number) => {
|
|
updateAudioDialog({ soundDurationSeconds: normalizeSoundDuration(durationSeconds) });
|
|
};
|
|
|
|
return (
|
|
<form
|
|
className="image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image image-canvas-editor__generation-composer--audio"
|
|
style={style}
|
|
role="dialog"
|
|
aria-label={dialogLabel}
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
if (!isGenerating) {
|
|
onSubmit(dialog);
|
|
}
|
|
}}
|
|
>
|
|
<PlatformTextField
|
|
variant="textarea"
|
|
aria-label={isSoundEffect ? 'prompt' : 'gpt_description_prompt'}
|
|
value={dialog.prompt}
|
|
disabled={isGenerating}
|
|
placeholder={
|
|
isSoundEffect
|
|
? '你希望生成什么音效?'
|
|
: '你希望生成什么音乐?'
|
|
}
|
|
size="sm"
|
|
density="compact"
|
|
className="image-canvas-editor__generation-prompt"
|
|
onChange={(event) => updateAudioDialog({ prompt: event.target.value })}
|
|
/>
|
|
{dialog.status === 'failed' ? (
|
|
<PlatformStatusMessage
|
|
tone="error"
|
|
surface="platform"
|
|
size="xs"
|
|
className="image-canvas-editor__generate-status"
|
|
role="alert"
|
|
>
|
|
{dialog.errorMessage}
|
|
</PlatformStatusMessage>
|
|
) : null}
|
|
<div className="image-canvas-editor__generation-composer-footer">
|
|
{isSoundEffect ? (
|
|
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--dimensions">
|
|
<PlatformInlineOptionButton
|
|
ref={soundOptionsButtonRef}
|
|
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--dimensions"
|
|
aria-label={`音效时长 ${soundOptionLabel}`}
|
|
aria-expanded={isSoundOptionsOpen}
|
|
disabled={isGenerating}
|
|
trailingIcon={<ChevronDown className="h-3 w-3" />}
|
|
onClick={() => setIsSoundOptionsOpen((isOpen) => !isOpen)}
|
|
>
|
|
{soundOptionLabel}
|
|
</PlatformInlineOptionButton>
|
|
{isSoundOptionsOpen
|
|
? renderEditorPortal(
|
|
<PlatformFloatingMenu
|
|
className="image-canvas-editor__option-popover image-canvas-editor__option-popover--audio-options image-canvas-editor__portal-menu"
|
|
label="音效时长选项"
|
|
placement="top-start"
|
|
style={buildPortalMenuStyle(
|
|
soundOptionsButtonRef.current,
|
|
'above',
|
|
)}
|
|
>
|
|
<div className="image-canvas-editor__option-popover-sections">
|
|
<section className="image-canvas-editor__option-popover-section">
|
|
<span className="image-canvas-editor__option-popover-title">
|
|
时长
|
|
</span>
|
|
<div className="image-canvas-editor__video-duration-slider">
|
|
<input
|
|
type="range"
|
|
min={2}
|
|
max={10}
|
|
step={1}
|
|
value={soundDuration}
|
|
disabled={isGenerating}
|
|
aria-label="音效时长"
|
|
onChange={(event) =>
|
|
updateSoundDuration(Number(event.target.value))
|
|
}
|
|
/>
|
|
<span>{soundDuration}秒</span>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</PlatformFloatingMenu>,
|
|
)
|
|
: null}
|
|
</div>
|
|
) : null}
|
|
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--model image-canvas-editor__readonly-generation-option">
|
|
<PlatformInlineOptionButton
|
|
ref={soundModelButtonRef}
|
|
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--model"
|
|
aria-label={fixedModelAriaLabel}
|
|
disabled
|
|
>
|
|
<span className="image-canvas-editor__model-trigger-label">
|
|
<span
|
|
className="image-canvas-editor__model-icon"
|
|
aria-hidden="true"
|
|
>
|
|
<Music />
|
|
</span>
|
|
<span>{fixedModelLabel}</span>
|
|
</span>
|
|
</PlatformInlineOptionButton>
|
|
</div>
|
|
<PlatformActionButton
|
|
type="submit"
|
|
tone="secondary"
|
|
size="xs"
|
|
shape="pill"
|
|
className="image-canvas-editor__generation-submit"
|
|
disabled={isGenerating}
|
|
aria-label={dialogLabel}
|
|
>
|
|
{isGenerating ? (
|
|
'生成中'
|
|
) : (
|
|
<>
|
|
<span>生成</span>
|
|
<span className="image-canvas-editor__mud-point-inline">
|
|
{cost}泥点
|
|
</span>
|
|
</>
|
|
)}
|
|
</PlatformActionButton>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|
|
|
|
export function ImageCanvasGenerationComposerView({
|
|
specToolWrapRef,
|
|
publicationReferenceButtonRef,
|
|
characterSpecButtonRef,
|
|
characterReferenceButtonRef,
|
|
generationReferenceButtonRef,
|
|
iconSpecButtonRef,
|
|
isSpecMenuOpen,
|
|
isGenerationReferenceMenuOpen,
|
|
isPublicationReferenceMenuOpen,
|
|
isCharacterSpecMenuOpen,
|
|
isCharacterReferenceMenuOpen,
|
|
isIconSpecMenuOpen,
|
|
isUiDesignSpecMenuOpen,
|
|
isPickingGenerationReferenceFromCanvas,
|
|
isPickingQuickEditReferenceFromCanvas,
|
|
isPickingPublicationReferenceFromCanvas,
|
|
isPickingCharacterSpecFromCanvas,
|
|
isPickingCharacterReferenceFromCanvas,
|
|
isPickingIconSpecFromCanvas,
|
|
isPickingUiDesignSpecFromCanvas,
|
|
generateDialog,
|
|
generationComposerStyle,
|
|
iconComposerStyle,
|
|
quickEditPanel,
|
|
quickEditSourceLayer,
|
|
quickEditPanelStyle,
|
|
cropExpandPanel,
|
|
cropExpandSourceLayer,
|
|
cropExpandPanelStyle,
|
|
characterAnimationPanel,
|
|
characterAnimationSourceLayer,
|
|
characterAnimationPanelStyle,
|
|
characterAnimationPrice,
|
|
setGenerateDialog,
|
|
setQuickEditPanel,
|
|
setCropExpandPanel,
|
|
setCharacterAnimationPanel,
|
|
setIsGenerationReferenceMenuOpen,
|
|
setIsPublicationReferenceMenuOpen,
|
|
setIsCharacterSpecMenuOpen,
|
|
setIsCharacterReferenceMenuOpen,
|
|
setIsIconSpecMenuOpen,
|
|
setIsUiDesignSpecMenuOpen,
|
|
setIsPickingGenerationReferenceFromCanvas,
|
|
setIsPickingQuickEditReferenceFromCanvas,
|
|
setIsPickingPublicationReferenceFromCanvas,
|
|
setIsPickingCharacterSpecFromCanvas,
|
|
setIsPickingCharacterReferenceFromCanvas,
|
|
setIsPickingIconSpecFromCanvas,
|
|
setIsPickingUiDesignSpecFromCanvas,
|
|
onOpenSpecDialog,
|
|
onRequestUpload,
|
|
onSubmitImageGeneration,
|
|
onSubmitIconSpritesheetGeneration,
|
|
onSubmitQuickEdit,
|
|
onSubmitCropExpand,
|
|
onSubmitCharacterAnimation,
|
|
onUpdateSpecFormValue,
|
|
onUpdateIconDescriptionText,
|
|
onUpdateCharacterAnimationDuration,
|
|
onRememberImageModel,
|
|
onSpecMenuPointerEnter,
|
|
onSpecMenuPointerLeave,
|
|
}: ImageCanvasGenerationComposerViewProps) {
|
|
return (
|
|
<>
|
|
{isSpecMenuOpen
|
|
? renderEditorPortal(
|
|
<PlatformFloatingMenu
|
|
className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu"
|
|
label="生成规范类型"
|
|
placement="top-start"
|
|
style={buildPortalMenuStyle(specToolWrapRef.current, 'above')}
|
|
onPointerEnter={onSpecMenuPointerEnter}
|
|
onPointerLeave={onSpecMenuPointerLeave}
|
|
onFocus={onSpecMenuPointerEnter}
|
|
onBlur={onSpecMenuPointerLeave}
|
|
>
|
|
{(['character', 'ui', 'custom'] as const).map((specType) => (
|
|
<PlatformFloatingMenuItem
|
|
key={specType}
|
|
className="image-canvas-editor__spec-menu-item"
|
|
onClick={() => onOpenSpecDialog(specType)}
|
|
>
|
|
{SPEC_TYPE_LABEL[specType]}
|
|
</PlatformFloatingMenuItem>
|
|
))}
|
|
</PlatformFloatingMenu>,
|
|
)
|
|
: null}
|
|
|
|
{(generateDialog?.mode === 'generate' ||
|
|
generateDialog?.mode === 'quick-edit') &&
|
|
generateDialog.composerOpen !== false &&
|
|
generationComposerStyle ? (
|
|
<ImageCanvasBasicGenerationComposerView
|
|
dialog={generateDialog}
|
|
style={generationComposerStyle}
|
|
setGenerateDialog={setGenerateDialog}
|
|
generationReferenceButtonRef={generationReferenceButtonRef}
|
|
isGenerationReferenceMenuOpen={isGenerationReferenceMenuOpen}
|
|
setIsGenerationReferenceMenuOpen={setIsGenerationReferenceMenuOpen}
|
|
setIsPickingGenerationReferenceFromCanvas={
|
|
setIsPickingGenerationReferenceFromCanvas
|
|
}
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
onRequestUpload={onRequestUpload}
|
|
onToggleReferenceMenu={() =>
|
|
setIsGenerationReferenceMenuOpen((open) => !open)
|
|
}
|
|
onRememberImageModel={onRememberImageModel}
|
|
onSubmit={onSubmitImageGeneration}
|
|
/>
|
|
) : null}
|
|
|
|
{generateDialog?.mode === 'spec' &&
|
|
generateDialog.composerOpen !== false &&
|
|
generationComposerStyle ? (
|
|
<ImageCanvasSpecGenerationPanelView
|
|
dialog={generateDialog}
|
|
style={generationComposerStyle}
|
|
isGenerationReferenceMenuOpen={isGenerationReferenceMenuOpen}
|
|
generationReferenceButtonRef={generationReferenceButtonRef}
|
|
setIsGenerationReferenceMenuOpen={setIsGenerationReferenceMenuOpen}
|
|
setIsPickingGenerationReferenceFromCanvas={
|
|
setIsPickingGenerationReferenceFromCanvas
|
|
}
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
setGenerateDialog={setGenerateDialog}
|
|
onOpenSpecDialog={onOpenSpecDialog}
|
|
onUpdateSpecFormValue={onUpdateSpecFormValue}
|
|
onRequestUpload={onRequestUpload}
|
|
onSubmit={onSubmitImageGeneration}
|
|
/>
|
|
) : null}
|
|
|
|
{generateDialog?.mode === 'publication' &&
|
|
generateDialog.composerOpen !== false &&
|
|
generationComposerStyle ? (
|
|
<ImageCanvasPublicationMaterialsDemoPanelView
|
|
dialog={generateDialog}
|
|
workflow={getPublicationMaterialsWorkflow(
|
|
generateDialog.publicationWorkflowId ?? 'publication-cover-image',
|
|
)}
|
|
style={generationComposerStyle}
|
|
publicationReferenceButtonRef={publicationReferenceButtonRef}
|
|
isPublicationReferenceMenuOpen={isPublicationReferenceMenuOpen}
|
|
setGenerateDialog={setGenerateDialog}
|
|
setIsPublicationReferenceMenuOpen={
|
|
setIsPublicationReferenceMenuOpen
|
|
}
|
|
setIsPickingPublicationReferenceFromCanvas={
|
|
setIsPickingPublicationReferenceFromCanvas
|
|
}
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
onRequestUpload={onRequestUpload}
|
|
onRememberImageModel={onRememberImageModel}
|
|
onSubmit={onSubmitImageGeneration}
|
|
/>
|
|
) : null}
|
|
|
|
{generateDialog?.mode === 'ui-design' &&
|
|
generateDialog.composerOpen !== false &&
|
|
generationComposerStyle ? (
|
|
<ImageCanvasSpecGenerationPanelView
|
|
dialog={generateDialog}
|
|
style={generationComposerStyle}
|
|
isGenerationReferenceMenuOpen={isUiDesignSpecMenuOpen}
|
|
generationReferenceButtonRef={generationReferenceButtonRef}
|
|
setIsGenerationReferenceMenuOpen={setIsUiDesignSpecMenuOpen}
|
|
setIsPickingGenerationReferenceFromCanvas={
|
|
setIsPickingUiDesignSpecFromCanvas
|
|
}
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
setGenerateDialog={setGenerateDialog}
|
|
onOpenSpecDialog={onOpenSpecDialog}
|
|
onUpdateSpecFormValue={onUpdateSpecFormValue}
|
|
onRequestUpload={onRequestUpload}
|
|
onRememberImageModel={onRememberImageModel}
|
|
onSubmit={onSubmitImageGeneration}
|
|
/>
|
|
) : null}
|
|
|
|
{generateDialog?.mode === 'video' &&
|
|
generateDialog.composerOpen !== false &&
|
|
generationComposerStyle ? (
|
|
<ImageCanvasVideoGenerationComposerView
|
|
dialog={generateDialog}
|
|
style={generationComposerStyle}
|
|
generationReferenceButtonRef={generationReferenceButtonRef}
|
|
isGenerationReferenceMenuOpen={isGenerationReferenceMenuOpen}
|
|
setGenerateDialog={setGenerateDialog}
|
|
setIsGenerationReferenceMenuOpen={setIsGenerationReferenceMenuOpen}
|
|
setIsPickingGenerationReferenceFromCanvas={
|
|
setIsPickingGenerationReferenceFromCanvas
|
|
}
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
onRequestUpload={onRequestUpload}
|
|
onSubmit={onSubmitImageGeneration}
|
|
/>
|
|
) : null}
|
|
|
|
{(generateDialog?.mode === 'audio-sound-effect' ||
|
|
generateDialog?.mode === 'audio-background-music') &&
|
|
generateDialog.composerOpen !== false &&
|
|
generationComposerStyle ? (
|
|
<ImageCanvasAudioGenerationComposerView
|
|
dialog={generateDialog}
|
|
style={generationComposerStyle}
|
|
setGenerateDialog={setGenerateDialog}
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
onSubmit={onSubmitImageGeneration}
|
|
/>
|
|
) : null}
|
|
|
|
{generateDialog?.mode === 'character' && generationComposerStyle ? (
|
|
<ImageCanvasCharacterGenerationComposerView
|
|
dialog={generateDialog}
|
|
style={generationComposerStyle}
|
|
characterSpecButtonRef={characterSpecButtonRef}
|
|
characterReferenceButtonRef={characterReferenceButtonRef}
|
|
isCharacterSpecMenuOpen={isCharacterSpecMenuOpen}
|
|
isCharacterReferenceMenuOpen={isCharacterReferenceMenuOpen}
|
|
setGenerateDialog={setGenerateDialog}
|
|
setIsCharacterSpecMenuOpen={setIsCharacterSpecMenuOpen}
|
|
setIsCharacterReferenceMenuOpen={setIsCharacterReferenceMenuOpen}
|
|
setIsPickingCharacterSpecFromCanvas={
|
|
setIsPickingCharacterSpecFromCanvas
|
|
}
|
|
setIsPickingCharacterReferenceFromCanvas={
|
|
setIsPickingCharacterReferenceFromCanvas
|
|
}
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
onOpenSpecDialog={onOpenSpecDialog}
|
|
onRequestUpload={onRequestUpload}
|
|
onRememberImageModel={onRememberImageModel}
|
|
onSubmit={onSubmitImageGeneration}
|
|
/>
|
|
) : null}
|
|
|
|
{generateDialog?.mode === 'icon' &&
|
|
generateDialog.composerOpen !== false &&
|
|
iconComposerStyle ? (
|
|
<ImageCanvasIconSpritesheetComposerView
|
|
dialog={generateDialog}
|
|
style={iconComposerStyle}
|
|
iconSpecButtonRef={iconSpecButtonRef}
|
|
isIconSpecMenuOpen={isIconSpecMenuOpen}
|
|
setGenerateDialog={setGenerateDialog}
|
|
setIsIconSpecMenuOpen={setIsIconSpecMenuOpen}
|
|
setIsPickingIconSpecFromCanvas={setIsPickingIconSpecFromCanvas}
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
onOpenSpecDialog={onOpenSpecDialog}
|
|
onRequestUpload={onRequestUpload}
|
|
onUpdateIconDescriptionText={onUpdateIconDescriptionText}
|
|
onRememberImageModel={onRememberImageModel}
|
|
onSubmit={onSubmitIconSpritesheetGeneration}
|
|
/>
|
|
) : null}
|
|
|
|
{isPickingCharacterSpecFromCanvas ? (
|
|
<div className="image-canvas-editor__canvas-pick-hint">
|
|
请选择画布中的图片作为角色规范,按 Esc 退出
|
|
</div>
|
|
) : null}
|
|
{isPickingCharacterReferenceFromCanvas ? (
|
|
<div className="image-canvas-editor__canvas-pick-hint">
|
|
请选择画布中的图片作为常规参考图,按 Esc 退出
|
|
</div>
|
|
) : null}
|
|
{isPickingGenerationReferenceFromCanvas ? (
|
|
<div className="image-canvas-editor__canvas-pick-hint">
|
|
请选择画布中的图片作为参考图,按 Esc 退出
|
|
</div>
|
|
) : null}
|
|
{isPickingQuickEditReferenceFromCanvas ? (
|
|
<div className="image-canvas-editor__canvas-pick-hint">
|
|
请选择画布中的图片作为参考图,按 Esc 退出
|
|
</div>
|
|
) : null}
|
|
{isPickingPublicationReferenceFromCanvas ? (
|
|
<div className="image-canvas-editor__canvas-pick-hint">
|
|
请选择画布中的图片作为宣发素材参考图,按 Esc 退出
|
|
</div>
|
|
) : null}
|
|
{isPickingIconSpecFromCanvas ? (
|
|
<div className="image-canvas-editor__canvas-pick-hint">
|
|
请选择画布中的图标规范,按 Esc 退出
|
|
</div>
|
|
) : null}
|
|
{isPickingUiDesignSpecFromCanvas ? (
|
|
<div className="image-canvas-editor__canvas-pick-hint">
|
|
请选择画布中的图标规范,按 Esc 退出
|
|
</div>
|
|
) : null}
|
|
|
|
{quickEditPanel && quickEditSourceLayer && quickEditPanelStyle ? (
|
|
<ImageCanvasQuickEditPanelView
|
|
panel={quickEditPanel}
|
|
sourceLayer={quickEditSourceLayer}
|
|
style={quickEditPanelStyle}
|
|
referenceButtonRef={generationReferenceButtonRef}
|
|
isReferenceMenuOpen={isGenerationReferenceMenuOpen}
|
|
setIsReferenceMenuOpen={setIsGenerationReferenceMenuOpen}
|
|
setIsPickingQuickEditReferenceFromCanvas={
|
|
setIsPickingQuickEditReferenceFromCanvas
|
|
}
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
onRequestUpload={onRequestUpload}
|
|
onRememberImageModel={onRememberImageModel}
|
|
setQuickEditPanel={setQuickEditPanel}
|
|
onSubmit={onSubmitQuickEdit}
|
|
/>
|
|
) : null}
|
|
|
|
{cropExpandPanel &&
|
|
cropExpandSourceLayer &&
|
|
cropExpandPanelStyle ? (
|
|
<ImageCanvasCropExpandPanelView
|
|
panel={cropExpandPanel}
|
|
style={cropExpandPanelStyle}
|
|
setCropExpandPanel={setCropExpandPanel}
|
|
onSubmit={onSubmitCropExpand}
|
|
/>
|
|
) : null}
|
|
|
|
{characterAnimationPanel &&
|
|
characterAnimationSourceLayer &&
|
|
characterAnimationPanelStyle ? (
|
|
<ImageCanvasCharacterAnimationPanelView
|
|
panel={characterAnimationPanel}
|
|
style={characterAnimationPanelStyle}
|
|
price={characterAnimationPrice}
|
|
sourceLayer={characterAnimationSourceLayer}
|
|
setCharacterAnimationPanel={setCharacterAnimationPanel}
|
|
onUpdateDuration={onUpdateCharacterAnimationDuration}
|
|
onSubmit={onSubmitCharacterAnimation}
|
|
/>
|
|
) : null}
|
|
|
|
<ImageCanvasEditGenerationModalView
|
|
dialog={generateDialog}
|
|
setGenerateDialog={setGenerateDialog}
|
|
onSubmit={onSubmitImageGeneration}
|
|
/>
|
|
</>
|
|
);
|
|
}
|