From 91bcddce97a85d6d843c2bd15f657cf9bb20933b Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 26 Jan 2026 00:57:06 +0800 Subject: [PATCH] first commit --- .claude/settings.local.json | 12 + .gitignore | 24 + App.tsx | 1301 ++++++++++++++++ README.md | 20 + components/AnimationPreview.tsx | 175 +++ components/UploadZone.tsx | 71 + index.html | 36 + index.tsx | 15 + metadata.json | 5 + package-lock.json | 2507 +++++++++++++++++++++++++++++++ package.json | 26 + services/geminiService.ts | 675 +++++++++ tsconfig.json | 29 + types.ts | 22 + utils/imageUtils.ts | 358 +++++ vite.config.ts | 24 + 16 files changed, 5300 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .gitignore create mode 100644 App.tsx create mode 100644 README.md create mode 100644 components/AnimationPreview.tsx create mode 100644 components/UploadZone.tsx create mode 100644 index.html create mode 100644 index.tsx create mode 100644 metadata.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 services/geminiService.ts create mode 100644 tsconfig.json create mode 100644 types.ts create mode 100644 utils/imageUtils.ts create mode 100644 vite.config.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..8f47e58 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(npm install)", + "Bash(ls:*)", + "Bash(cat:*)", + "Bash(npx tsc:*)", + "Bash(npm run dev:*)", + "Bash(timeout:*)" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..5021d07 --- /dev/null +++ b/App.tsx @@ -0,0 +1,1301 @@ +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); + }} + /> +
+ ) : ( +
+
+

+ 角色描述 +

+
+ +