import { ApiClientError } from '../../services/apiClient'; import type { EditorCharacterAnimationRatio, EditorCharacterAnimationResolution, } from '../../services/image-editor/editorProjectClient'; import type { CanvasGenerationDialogState, CanvasGenerationInputField, CanvasGenerationInputs, CanvasLayer, CharacterReferenceImage, GenerateDialogState, SpecFormValues, SpecGenerationType, } from './ImageCanvasEditorTypes'; import { isGeneratedLayer } from './ImageCanvasEditorModel'; export const SPEC_GENERATION_COST = 5; export const SPEC_GENERATION_SIZE = '2048x1152'; export const SPEC_FRAME_ORIGINAL_SIZE = { width: 2048, height: 1152 }; export const SPEC_FRAME_DISPLAY_SIZE = { width: 560, height: 315 }; export const CHARACTER_FRAME_ORIGINAL_SIZE = { width: 2048, height: 2048 }; export const CHARACTER_FRAME_DISPLAY_SIZE = { width: 420, height: 420 }; export const ICON_FRAME_ORIGINAL_SIZE = { width: 512, height: 512 }; export const ICON_FRAME_DISPLAY_SIZE = { width: 360, height: 360 }; export const DEFAULT_IMAGE_MODEL = 'gpt-image-2'; export const ICON_DESCRIPTION_LIMIT = 100; // 图标素材面板按描述项扩宽,避免在画布子面板里做滑动列表。 export const ICON_DESCRIPTION_CARD_WIDTH_REM = 8.4; export const ICON_COMPOSER_MIN_WIDTH_REM = 28; export const ICON_COMPOSER_HORIZONTAL_CHROME_REM = 2.4; export const DEFAULT_ICON_DESCRIPTIONS = [ '返回按钮', '设置按钮', '下一关按钮', '提示按钮', '原图按钮', '冻结按钮', ]; export const QUICK_EDIT_SIZE_PRESETS = [ '1024x1024', '1536x1024', '2048x1152', '1024x1536', ] as const; export const QUICK_EDIT_MODEL_OPTIONS = [ { label: 'GPT Image', value: DEFAULT_IMAGE_MODEL }, ] as const; export const CHARACTER_ANIMATION_MODEL = 'seedance2.0'; export const CHARACTER_ANIMATION_ACTION_PROMPTS = [ { label: '待机', text: '待机动作,轻微呼吸起伏。' }, { label: '行走', text: '循环行走动作,步伐稳定。' }, { label: '奔跑', text: '循环奔跑动作,动作清晰有力。' }, { label: '跳跃', text: '起跳、滞空、落地动作。' }, { label: '攻击', text: '攻击动作,前摇、出手、收招清晰。' }, { label: '受击', text: '受击后短暂后仰并恢复站姿。' }, { label: '倒下', text: '倒下动作,重心下落自然。' }, ] as const; export const CHARACTER_ANIMATION_RATIO_OPTIONS: Array<{ label: string; value: EditorCharacterAnimationRatio; }> = [ { label: '与角色图片保持同尺寸', value: 'same' }, { label: '1:1', value: '1:1' }, { label: '4:3', value: '4:3' }, { label: '16:9', value: '16:9' }, { label: '9:16', value: '9:16' }, { label: '3:4', value: '3:4' }, ]; export const CHARACTER_ANIMATION_DURATION_OPTIONS = [ { label: '32帧·4秒', frameCount: 32, durationSeconds: 4 }, { label: '40帧·5秒', frameCount: 40, durationSeconds: 5 }, { label: '48帧·6秒', frameCount: 48, durationSeconds: 6 }, ] as const; export const DEFAULT_SPEC_FORM_VALUES: Record< SpecGenerationType, SpecFormValues > = { character: { playSetting: '战棋类RPG玩法', artStyle: '像素风', bodyRatio: '3', characterView: '右向斜侧身站姿,保留少量正面信息,能读到面部轮廓与胸肩结构,禁止生成完全 90 度纯右视图,也禁止生成正面立绘。', customPrompt: '', }, ui: { playSetting: '抓娃娃题材的抓大鹅玩法', artStyle: '毛茸茸', bodyRatio: '3', characterView: '右向斜侧身站姿,保留少量正面信息,能读到面部轮廓与胸肩结构,禁止生成完全 90 度纯右视图,也禁止生成正面立绘。', customPrompt: '', }, icon: { playSetting: '休闲小游戏', artStyle: '清爽卡通', bodyRatio: '3', characterView: '右向斜侧身站姿,保留少量正面信息,能读到面部轮廓与胸肩结构,禁止生成完全 90 度纯右视图,也禁止生成正面立绘。', customPrompt: '', }, custom: { playSetting: '', artStyle: '', bodyRatio: '3', characterView: '右向斜侧身站姿,保留少量正面信息,能读到面部轮廓与胸肩结构,禁止生成完全 90 度纯右视图,也禁止生成正面立绘。', customPrompt: '', }, }; export const SPEC_TYPE_LABEL: Record = { character: '角色形象规范', ui: 'UI素材规范', icon: '图标素材规范', custom: '自定义规范', }; export const CHARACTER_SPEC_VIEW_OPTIONS = [ DEFAULT_SPEC_FORM_VALUES.character.characterView, '左向三分之二侧身站姿', '左向三分之二侧身站姿,保留少量正面信息,能读到面部轮廓与胸肩结构,禁止生成完全 90 度纯左视图,也禁止生成正面立绘。', '右向三分之二侧身站姿,保留少量正面信息,强调面部轮廓、胸肩结构与主要装备层次。', '背向斜侧身站姿,保留少量侧脸信息,突出背部服饰层次、武器挂载与轮廓识别。', ]; export function buildQuickEditSizeOptions(currentSize: string) { return Array.from(new Set([currentSize, ...QUICK_EDIT_SIZE_PRESETS])); } export function buildQuickEditModelOptions(currentModel: string) { const options = [...QUICK_EDIT_MODEL_OPTIONS]; return options.some((option) => option.value === currentModel) ? options : [{ label: currentModel, value: currentModel }, ...options]; } export function buildCharacterSpecPrompt(values: SpecFormValues) { return [ '生成2D 角色美术视觉规范设定图,纯白底板,整齐排布全身标准立绘;固定统一头身比例、勾线粗细恒定;展示待机行走攻击基础动作帧样例,重心对齐不变位,服饰配饰分层结构示意,搭配专属角色色卡标注色号,无多余杂物,精准尺寸标注,高清矢量规范稿', '禁止模糊、笔触杂乱、光影方向混乱、比例畸形、3D 渲染、实景照片、水印、花纹堆砌、画面抖动错位效果、噪点,', `玩法设计:${values.playSetting.trim() || DEFAULT_SPEC_FORM_VALUES.character.playSetting}`, `美术风格:${values.artStyle.trim() || DEFAULT_SPEC_FORM_VALUES.character.artStyle}`, `头身比:${values.bodyRatio.trim() || DEFAULT_SPEC_FORM_VALUES.character.bodyRatio}`, `视角要求:${values.characterView.trim() || DEFAULT_SPEC_FORM_VALUES.character.characterView}`, ].join('\n'); } export function buildUiSpecPrompt(values: SpecFormValues) { return [ '生成一张完整游戏UI规范汇总设定展板,纯白色干净背景,Figma专业设计稿质感,矢量锐利线条,页面划分九大区域:色彩规范、字体规范、图标规范、按钮规范、组件规范、布局规范、特效规范、IP规范、主视觉。主视觉居中较大显示,其他八个区域环绕主视觉', '', `玩法设定:${values.playSetting.trim() || DEFAULT_SPEC_FORM_VALUES.ui.playSetting}`, `美术风格:${values.artStyle.trim() || DEFAULT_SPEC_FORM_VALUES.ui.artStyle}`, ].join('\n'); } export function buildIconSpecPrompt(values: SpecFormValues) { return [ '生成一张游戏图标素材视觉规范展板,纯白色干净背景,展示按钮图标的统一视角、线条粗细、填充风格、描边、阴影、圆角、材质、状态层级和色彩规范,图标样例需要成组排列且风格高度统一。', '', `玩法设定:${values.playSetting.trim() || DEFAULT_SPEC_FORM_VALUES.icon.playSetting}`, `美术风格:${values.artStyle.trim() || DEFAULT_SPEC_FORM_VALUES.icon.artStyle}`, ].join('\n'); } export function buildSpecPrompt( type: SpecGenerationType, values: SpecFormValues, ) { if (type === 'character') { return buildCharacterSpecPrompt(values); } if (type === 'ui') { return buildUiSpecPrompt(values); } if (type === 'icon') { return buildIconSpecPrompt(values); } return values.customPrompt.trim(); } export function getLayerKindLabel(layer: CanvasLayer) { if (layer.assetKind === 'spec') { return '规范'; } if (layer.assetKind === 'character') { return '角色'; } if (layer.assetKind === 'icon') { return '图标'; } if (layer.assetKind === 'icon-spec') { return '图标规范'; } return null; } export function formatLayerImageType(layer: CanvasLayer) { if (layer.assetKind === 'spec') { return '规范图片'; } if (layer.assetKind === 'character') { return '角色图片'; } if (layer.assetKind === 'icon') { return '图标素材图片'; } if (layer.assetKind === 'icon-spec') { return '图标素材规范图片'; } return isGeneratedLayer(layer) ? '生成图片' : '上传图片'; } export function calculateCharacterAnimationPrice( resolution: EditorCharacterAnimationResolution, durationSeconds: number, ) { return (resolution === '720p' ? 20 : 10) * durationSeconds; } export function resolveCharacterAnimationSourceImageSrc(layer: CanvasLayer) { // 中文注释:角色图已持久化到 OSS 时优先传 objectKey,避免把大 Data URL 塞进 JSON 请求体触发 body limit。 return layer.objectKey?.trim() || layer.src; } export function createCanvasLayerReference( layer: CanvasLayer, ): CharacterReferenceImage { return { id: `canvas-${layer.id}`, label: layer.title, src: layer.src, }; } export function createGenerationInputField( title: string, value: string | null | undefined, ): CanvasGenerationInputField[] { const normalizedValue = value?.trim(); return normalizedValue ? [{ title, value: normalizedValue }] : []; } export function buildImageGenerationInputs(prompt: string): CanvasGenerationInputs { return { fields: createGenerationInputField('生成提示词', prompt), references: [], }; } export function buildSpecGenerationInputs( specType: SpecGenerationType, values: SpecFormValues, ): CanvasGenerationInputs { if (specType === 'custom') { return { fields: createGenerationInputField('自定义规范提示词', values.customPrompt), references: [], }; } const baseFields = [ ...createGenerationInputField('玩法设定', values.playSetting), ...createGenerationInputField('美术风格', values.artStyle), ]; if (specType === 'character') { baseFields.push( ...createGenerationInputField('头身比', values.bodyRatio), ...createGenerationInputField('角色视角', values.characterView), ); } return { fields: baseFields, references: [], }; } export function buildCharacterGenerationInputs( prompt: string, specReference: CharacterReferenceImage | null | undefined, references: CharacterReferenceImage[] | undefined, ): CanvasGenerationInputs { return { fields: createGenerationInputField('角色设定', prompt), references: [ ...(specReference ? [ { title: '角色形象规范', label: specReference.label, src: specReference.src, }, ] : []), ...(references ?? []).map((reference, index) => ({ title: `常规参考图 ${index + 1}`, label: reference.label, src: reference.src, })), ], }; } export function buildIconGenerationInputs( iconDescriptions: string[], specReference: CharacterReferenceImage, ): CanvasGenerationInputs { return { fields: iconDescriptions.map((description, index) => ({ title: `素材描述 ${index + 1}`, value: description, })), references: [ { title: '图标素材规范', label: specReference.label, src: specReference.src, }, ], }; } export function buildEditGenerationInputs( title: '修改要求' | '快速编辑提示词', prompt: string, sourceLayer: CanvasLayer, ): CanvasGenerationInputs { return { fields: createGenerationInputField(title, prompt), references: [ { title: '参考图', label: sourceLayer.title, src: sourceLayer.src, }, ], }; } export function isCanvasGenerationDialog( dialog: GenerateDialogState | null, ): dialog is CanvasGenerationDialogState { return Boolean( dialog?.id && (dialog.mode === 'generate' || dialog.mode === 'spec' || dialog.mode === 'character' || dialog.mode === 'icon'), ); } export function getGenerationFrameAriaLabel( dialog: CanvasGenerationDialogState, ) { if (dialog.mode === 'character') { return '角色生成占位图'; } if (dialog.mode === 'spec') { return '规范生成占位图'; } if (dialog.mode === 'icon') { return '图标素材生成占位图'; } return '图像生成占位图'; } export function getGenerationFrameLabel(dialog: CanvasGenerationDialogState) { if (dialog.mode === 'character') { return 'Character Generator'; } if (dialog.mode === 'spec') { return 'Spec Generator'; } if (dialog.mode === 'icon') { return 'Icon Generator'; } return 'Image Generator'; } export function resolveImageGenerationErrorMessage(error: unknown) { if ( error instanceof ApiClientError && (error.status === 401 || error.status === 403) ) { return '请先登录后再生成图片'; } return error instanceof Error && error.message.trim() ? error.message : '生成图片失败'; }