import { ChevronDown, ImageIcon, X } from 'lucide-react'; import { type CSSProperties, type Dispatch, type ReactNode, type SetStateAction, useCallback, useRef, useState, } from 'react'; import type { EditorCharacterAnimationRatio, EditorCharacterAnimationResolution, } from '../../services/image-editor/editorProjectClient'; import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformFloatingMenu } from '../common/PlatformFloatingMenu'; import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformTextField } from '../common/PlatformTextField'; import { ImageCanvasEditorPortal } from './ImageCanvasEditorPortal'; import { EditorIconButton } from './ImageCanvasEditorPrimitives'; import type { CanvasLayer, CharacterAnimationPanelState, } from './ImageCanvasEditorTypes'; import { CHARACTER_ANIMATION_ACTION_PROMPTS, CHARACTER_ANIMATION_DURATION_OPTIONS, CHARACTER_ANIMATION_RATIO_OPTIONS, } from './ImageCanvasGenerationModel'; import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot'; import { useImageCanvasFloatingOptionDismiss } from './useImageCanvasFloatingOptionDismiss'; type ImageCanvasCharacterAnimationPanelViewProps = { panel: CharacterAnimationPanelState; sourceLayer: CanvasLayer; style: CSSProperties; price: number; setCharacterAnimationPanel: Dispatch< SetStateAction >; onUpdateDuration: (frameCountValue: string) => void; onSubmit: () => void; }; function resetFailedPanelStatus( panel: T, ) { return { ...panel, status: panel.status === 'failed' ? 'idle' : panel.status, errorMessage: panel.status === 'failed' ? undefined : panel.errorMessage, }; } function buildLocalMenuStyle(anchor: HTMLElement | null): 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: Math.round(rect.top), right: 'auto', bottom: 'auto', zIndex: 70, transform: 'translateY(calc(-100% - 0.45rem))', }; } function renderPanelPortal(node: ReactNode) { // 中文注释:参数菜单挂到页面级,避免被角色动画面板滚动边界裁切。 return {node}; } function getRatioLabel(value: EditorCharacterAnimationRatio) { return value === 'same' ? '同图尺寸' : value; } function AnimationOptionChoice({ children, selected, className, ariaLabel, disabled, onClick, }: { children: ReactNode; selected: boolean; className?: string; ariaLabel?: string; disabled?: boolean; onClick?: () => void; }) { return ( ); } export function ImageCanvasCharacterAnimationPanelView({ panel, sourceLayer, style, price, setCharacterAnimationPanel, onUpdateDuration, onSubmit, }: ImageCanvasCharacterAnimationPanelViewProps) { const [isParameterMenuOpen, setIsParameterMenuOpen] = useState(false); const parameterButtonRef = useRef(null); const isGenerating = panel.status === 'generating'; const ratioLabel = getRatioLabel(panel.ratio); const dismissParameterMenu = useCallback(() => { setIsParameterMenuOpen(false); }, []); useImageCanvasFloatingOptionDismiss({ isOpen: isParameterMenuOpen, boundaryRefs: [parameterButtonRef], onDismiss: dismissParameterMenu, }); const updatePanel = (patch: Partial) => { setCharacterAnimationPanel((currentPanel) => currentPanel ? { ...resetFailedPanelStatus(currentPanel), ...patch, } : currentPanel, ); }; const closePanel = () => { setCharacterAnimationPanel((currentPanel) => currentPanel && isGenerating ? currentPanel : null, ); }; return ( <>
event.stopPropagation()} onSubmit={(event) => { event.preventDefault(); if (!isGenerating) { onSubmit(); } }} >
updatePanel({ promptText: event.target.value.slice(0, 4000) }) } />
{CHARACTER_ANIMATION_ACTION_PROMPTS.map((preset) => ( ))}
} onClick={() => setIsParameterMenuOpen((open) => !open)} > {ratioLabel} · {panel.durationSeconds}秒 · {panel.resolution}
{isGenerating ? '生成中' : `生成${price}泥点`}
{panel.promptText.trim() ? panel.promptText.trim() : '动画描述'} {panel.status === 'completed' && panel.result ? ( 已生成 {panel.result.frameCount} 帧 ) : null} {panel.status === 'failed' ? ( {panel.errorMessage} ) : null} {isParameterMenuOpen ? renderPanelPortal( event.stopPropagation()} >
比例
{CHARACTER_ANIMATION_RATIO_OPTIONS.map((option) => ( updatePanel({ ratio: option.value })} > ))}
时长
{CHARACTER_ANIMATION_DURATION_OPTIONS.map((option) => ( onUpdateDuration(String(option.frameCount))} > {option.label} ))}
清晰度
{(['480p', '720p'] as EditorCharacterAnimationResolution[]).map( (resolution) => ( updatePanel({ resolution })} > {resolution} ), )}
, ) : null} ); }