import React, { useState } from 'react'; import { UploadZone } from './components/UploadZone'; import { AnimationPreview } from './components/AnimationPreview'; import { generateSpriteSheet, generateCharacterPortrait, generateDirectionalSpriteSheet, generateGameMap, MapType, generateGameItem, ItemType } from './services/geminiService'; import { sliceSpriteSheet } from './utils/imageUtils'; import { AppState } from './types'; import { Wand2, Zap, LayoutGrid, Loader2, RotateCcw, Image, Type, User, Film, Download, Move, Map, Sword } from 'lucide-react'; const App: React.FC = () => { const [appState, setAppState] = useState(AppState.IDLE); const [originalImage, setOriginalImage] = useState(null); const [actionPrompt, setActionPrompt] = useState(""); const [generatedFrames, setGeneratedFrames] = useState([]); const [fps, setFps] = useState(6); const [errorMsg, setErrorMsg] = useState(null); // Configuration state - Default to 4 for stability const [frameCount, setFrameCount] = useState(4); // Mode: 'image' = with reference image, 'text' = text-only generation const [mode, setMode] = useState<'image' | 'text'>('text'); const [characterDescription, setCharacterDescription] = useState(""); // Feature: 'animation', 'portrait', 'directional', 'map', or 'item' const [feature, setFeature] = useState<'animation' | 'portrait' | 'directional' | 'map' | 'item'>('animation'); const [portraitImage, setPortraitImage] = useState(null); const [portraitStyle, setPortraitStyle] = useState('2D卡通风格'); // Directional sprite sheet state const [directionalSpriteSheet, setDirectionalSpriteSheet] = useState(null); const [directionalFrames, setDirectionalFrames] = useState([[], [], [], []]); const [selectedDirection, setSelectedDirection] = useState(0); const [directionalAction, setDirectionalAction] = useState('行走'); const [directionalFps, setDirectionalFps] = useState(6); // Map generation state const [mapImage, setMapImage] = useState(null); const [mapType, setMapType] = useState('rpg'); const [mapDescription, setMapDescription] = useState(''); const [mapStyle, setMapStyle] = useState('像素风格'); // Item generation state const [itemImage, setItemImage] = useState(null); const [itemType, setItemType] = useState('weapon'); const [itemDescription, setItemDescription] = useState(''); const [itemStyle, setItemStyle] = useState('像素风格'); const [itemQuantity, setItemQuantity] = useState<'single' | 'set'>('single'); // Step 1: Handle Image Upload const handleImageSelected = (base64: string) => { setOriginalImage(base64); setAppState(AppState.READY_TO_GENERATE); setErrorMsg(null); // Pre-fill a default action if empty to encourage user if (!actionPrompt) setActionPrompt("闲置动作"); }; // Step 2: Generate Animation const handleGenerate = async () => { if (mode === 'image' && !originalImage) { setErrorMsg("请先上传图片"); return; } if (mode === 'text' && !characterDescription) { setErrorMsg("请输入角色描述"); return; } if (!actionPrompt) { setErrorMsg("请输入动作描述"); return; } setAppState(AppState.GENERATING); setErrorMsg(null); setGeneratedFrames([]); // Calculate rows and cols based on frame count // NOTE: This must match the logic in geminiService implicitly via params let rows = 2; let cols = 2; // Default 4 frames (2x2) if (frameCount === 4) { rows = 2; cols = 2; } if (frameCount === 6) { rows = 2; cols = 3; } if (frameCount === 8) { rows = 2; cols = 4; } if (frameCount === 9) { rows = 3; cols = 3; } if (frameCount === 16) { rows = 4; cols = 4; } try { const spriteSheetBase64 = await generateSpriteSheet( mode === 'image' ? originalImage : null, actionPrompt, rows, cols, mode === 'text' ? characterDescription : undefined ); // Slice the grid using Fixed Grid slicing const frames = await sliceSpriteSheet(spriteSheetBase64, rows, cols, true); if (!frames || frames.length === 0) { throw new Error("图像处理失败,请重试。"); } setGeneratedFrames(frames); setAppState(AppState.COMPLETE); } catch (err: any) { console.error("Generation Error:", err); setErrorMsg(err.message || "生成失败,请尝试不同的提示词。"); setAppState(AppState.ERROR); } }; const handleReset = () => { setAppState(AppState.IDLE); setGeneratedFrames([]); setPortraitImage(null); setDirectionalSpriteSheet(null); setDirectionalFrames([[], [], [], []]); setMapImage(null); setItemImage(null); setErrorMsg(null); }; // Generate Portrait const handleGeneratePortrait = async () => { if (!characterDescription) { setErrorMsg("请输入角色描述"); return; } setAppState(AppState.GENERATING); setErrorMsg(null); setPortraitImage(null); try { const portrait = await generateCharacterPortrait( characterDescription, portraitStyle, '3:4' ); setPortraitImage(portrait); setAppState(AppState.COMPLETE); } catch (err: any) { console.error("Portrait Generation Error:", err); setErrorMsg(err.message || "立绘生成失败,请重试。"); setAppState(AppState.ERROR); } }; // Download portrait const handleDownloadPortrait = () => { if (!portraitImage) return; const link = document.createElement('a'); link.href = portraitImage; link.download = 'character-portrait.png'; link.click(); }; // Generate Directional Sprite Sheet const handleGenerateDirectional = async () => { if (mode === 'image' && !originalImage) { setErrorMsg("请先上传图片"); return; } if (mode === 'text' && !characterDescription) { setErrorMsg("请输入角色描述"); return; } setAppState(AppState.GENERATING); setErrorMsg(null); setDirectionalSpriteSheet(null); setDirectionalFrames([[], [], [], []]); try { const spriteSheetBase64 = await generateDirectionalSpriteSheet( mode === 'image' ? originalImage : null, directionalAction, mode === 'text' ? characterDescription : undefined ); setDirectionalSpriteSheet(spriteSheetBase64); // Slice into 4 rows (directions), each with 4 frames const allFrames = await sliceSpriteSheet(spriteSheetBase64, 4, 4, true); // Split into 4 directions const dirFrames: string[][] = [ allFrames.slice(0, 4), // Down allFrames.slice(4, 8), // Left allFrames.slice(8, 12), // Right allFrames.slice(12, 16) // Up ]; setDirectionalFrames(dirFrames); setAppState(AppState.COMPLETE); } catch (err: any) { console.error("Directional Generation Error:", err); setErrorMsg(err.message || "序列帧生成失败,请重试。"); setAppState(AppState.ERROR); } }; // Download directional sprite sheet const handleDownloadDirectional = () => { if (!directionalSpriteSheet) return; const link = document.createElement('a'); link.href = directionalSpriteSheet; link.download = 'directional-sprite-sheet.png'; link.click(); }; const directionLabels = ['向下 (正面)', '向左', '向右', '向上 (背面)']; // Generate Map const handleGenerateMap = async () => { if (!mapDescription) { setErrorMsg("请输入地图描述"); return; } setAppState(AppState.GENERATING); setErrorMsg(null); setMapImage(null); try { const map = await generateGameMap(mapType, mapDescription, mapStyle); setMapImage(map); setAppState(AppState.COMPLETE); } catch (err: any) { console.error("Map Generation Error:", err); setErrorMsg(err.message || "地图生成失败,请重试。"); setAppState(AppState.ERROR); } }; // Download map const handleDownloadMap = () => { if (!mapImage) return; const link = document.createElement('a'); link.href = mapImage; link.download = `game-map-${mapType}.png`; link.click(); }; const mapTypeLabels: Record = { rpg: 'RPG 场景', tilemap: '瓦片素材', platformer: '横版关卡', strategy: '战棋地图', world: '世界地图' }; const mapStyleOptions = ['像素风格', '卡通风格', '写实风格', '手绘风格']; // Generate Item const handleGenerateItem = async () => { if (!itemDescription) { setErrorMsg("请输入道具描述"); return; } setAppState(AppState.GENERATING); setErrorMsg(null); setItemImage(null); try { const item = await generateGameItem(itemType, itemDescription, itemStyle, itemQuantity); setItemImage(item); setAppState(AppState.COMPLETE); } catch (err: any) { console.error("Item Generation Error:", err); setErrorMsg(err.message || "道具生成失败,请重试。"); setAppState(AppState.ERROR); } }; // Download item const handleDownloadItem = () => { if (!itemImage) return; const link = document.createElement('a'); link.href = itemImage; link.download = `game-item-${itemType}.png`; link.click(); }; const itemTypeLabels: Record = { weapon: '武器', armor: '防具', potion: '药水/消耗品', material: '材料', treasure: '宝箱/财宝', accessory: '饰品' }; const itemStyleOptions = ['像素风格', '卡通风格', '写实风格', '手绘风格']; return (
{/* Header */}
ToonMotion
Powered by Gemini 2.5
{/* Feature Tabs */}
{/* Intro */}

{feature === 'animation' ? '让你的角色动起来' : feature === 'portrait' ? '生成角色立绘' : feature === 'directional' ? '四方向序列帧' : feature === 'map' ? '游戏地图生成' : '武器道具生成'}

{feature === 'portrait' ? '描述你想要的角色,Gemini 将为你生成高质量的角色立绘。' : feature === 'directional' ? '生成角色的四个方向(上下左右)动画序列帧,适用于 RPG 游戏开发。' : feature === 'map' ? '生成各类游戏地图,包括 RPG 场景、瓦片素材、横版关卡、战棋地图和世界地图。' : feature === 'item' ? '生成游戏中的武器、防具、药水、材料等道具图标,支持单个或批量生成。' : mode === 'image' ? '上传静态角色图片,输入动作描述,Gemini 将为你生成逐帧动画。' : '描述你想要的角色和动作,Gemini 将从零开始生成逐帧动画。'}

{feature === 'animation' ? (
{/* Left Column: Input */}
{/* Mode Switch */}
{/* Conditional: Upload or Character Description */} {mode === 'image' ? (

上传角色图片

{ setOriginalImage(null); setAppState(AppState.IDLE); setGeneratedFrames([]); setErrorMsg(null); }} />
) : (

角色描述