The Gemini service has been updated to generate a character sheet rather than a single avatar image. This sheet includes the main character and separate assets for closed eyes and an open mouth. The `AvatarConfig` type and `RiggingEditor` component have been extended to handle these new expression assets (`textureClosedEye`, `textureOpenMouth`). A new `Sprite` component has been added to `Studio.tsx` to correctly render these specific regions from the generated character sheet. The UI has been updated to reflect the new generation process.
134 lines
5.5 KiB
TypeScript
134 lines
5.5 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
|
import { AppState, AvatarConfig, Rect } from './types';
|
|
import AvatarCreator from './components/AvatarCreator';
|
|
import RiggingEditor from './components/RiggingEditor';
|
|
import Studio from './components/Studio';
|
|
|
|
const App: React.FC = () => {
|
|
const [appState, setAppState] = useState<AppState>(AppState.SETUP);
|
|
// Temp storage for the generated image before rigging
|
|
const [generatedData, setGeneratedData] = useState<{url: string, name: string, initialData?: any} | null>(null);
|
|
const [avatar, setAvatar] = useState<AvatarConfig | null>(null);
|
|
|
|
const handleStartCreation = async () => {
|
|
try {
|
|
if (window.aistudio) {
|
|
const hasKey = await window.aistudio.hasSelectedApiKey();
|
|
if (!hasKey) {
|
|
await window.aistudio.openSelectKey();
|
|
}
|
|
}
|
|
setAppState(AppState.CREATION);
|
|
} catch (error) {
|
|
console.error("Error during API key selection:", error);
|
|
setAppState(AppState.CREATION);
|
|
}
|
|
};
|
|
|
|
const handleAvatarGenerated = (url: string, name: string, initialData?: any) => {
|
|
setGeneratedData({ url, name, initialData });
|
|
setAppState(AppState.RIGGING);
|
|
};
|
|
|
|
const handleRiggingComplete = (data: {
|
|
leftEye: Rect, rightEye: Rect, mouth: Rect, skinColor: string,
|
|
textureClosedEye: Rect, textureOpenMouth: Rect
|
|
}) => {
|
|
if (generatedData) {
|
|
setAvatar({
|
|
imageUrl: generatedData.url,
|
|
name: generatedData.name,
|
|
description: '',
|
|
leftEye: data.leftEye,
|
|
rightEye: data.rightEye,
|
|
mouth: data.mouth,
|
|
skinColor: data.skinColor,
|
|
textureClosedEye: data.textureClosedEye,
|
|
textureOpenMouth: data.textureOpenMouth,
|
|
});
|
|
setAppState(AppState.STUDIO);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-900 text-white">
|
|
{appState === AppState.SETUP && (
|
|
<div className="container mx-auto px-4 py-12 flex flex-col items-center justify-center min-h-screen">
|
|
<div className="text-center mb-12 space-y-4">
|
|
<h1 className="text-6xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-blue-500 to-purple-600 brand-font tracking-tighter">
|
|
GEMINI V-STUDIO
|
|
</h1>
|
|
<p className="text-xl text-slate-400 max-w-2xl mx-auto">
|
|
The next-generation browser-based VTuber studio. Generate your persona with AI and animate it with your face.
|
|
</p>
|
|
<button
|
|
onClick={handleStartCreation}
|
|
className="mt-8 px-8 py-4 bg-white text-slate-900 rounded-full font-bold hover:bg-cyan-50 transition-colors shadow-[0_0_20px_rgba(255,255,255,0.3)]"
|
|
>
|
|
Start Creation
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 w-full max-w-5xl">
|
|
<div className="p-6 bg-slate-800/50 rounded-xl border border-slate-700 backdrop-blur-sm">
|
|
<div className="h-12 w-12 bg-cyan-500/10 rounded-lg flex items-center justify-center mb-4 text-2xl">✨</div>
|
|
<h3 className="text-xl font-bold mb-2">AI Generation</h3>
|
|
<p className="text-slate-400">Describe your dream character. Gemini 3 Pro creates high-fidelity sprites in seconds.</p>
|
|
</div>
|
|
<div className="p-6 bg-slate-800/50 rounded-xl border border-slate-700 backdrop-blur-sm">
|
|
<div className="h-12 w-12 bg-purple-500/10 rounded-lg flex items-center justify-center mb-4 text-2xl">📸</div>
|
|
<h3 className="text-xl font-bold mb-2">Face Tracking</h3>
|
|
<p className="text-slate-400">Powered by MediaPipe. No expensive equipment needed—just your webcam.</p>
|
|
</div>
|
|
<div className="p-6 bg-slate-800/50 rounded-xl border border-slate-700 backdrop-blur-sm">
|
|
<div className="h-12 w-12 bg-pink-500/10 rounded-lg flex items-center justify-center mb-4 text-2xl">🎥</div>
|
|
<h3 className="text-xl font-bold mb-2">Live Animation</h3>
|
|
<p className="text-slate-400">Your avatar mimics your head movements and speech in real-time.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{appState === AppState.CREATION && (
|
|
<div className="container mx-auto px-4 py-12 min-h-screen flex flex-col">
|
|
<button
|
|
onClick={() => setAppState(AppState.SETUP)}
|
|
className="self-start mb-8 px-4 py-2 text-slate-400 hover:text-white transition-colors"
|
|
>
|
|
← Back to Home
|
|
</button>
|
|
<div className="flex-1 flex items-center justify-center">
|
|
<AvatarCreator onAvatarGenerated={handleAvatarGenerated} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{appState === AppState.RIGGING && generatedData && (
|
|
<div className="container mx-auto px-4 py-8 min-h-screen flex flex-col">
|
|
<button
|
|
onClick={() => setAppState(AppState.CREATION)}
|
|
className="self-start mb-4 px-4 py-2 text-slate-400 hover:text-white transition-colors"
|
|
>
|
|
← Back to Generator
|
|
</button>
|
|
<RiggingEditor
|
|
imageUrl={generatedData.url}
|
|
initialData={generatedData.initialData}
|
|
onComplete={handleRiggingComplete}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{appState === AppState.STUDIO && avatar && (
|
|
<Studio
|
|
avatar={avatar}
|
|
onBack={() => setAppState(AppState.SETUP)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App;
|