diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1960f1c..8a4b51f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -33,6 +33,7 @@ export default function App() { const [currentOutfit, setCurrentOutfit] = useState('cozy-hoodie'); const [currentAcc, setCurrentAcc] = useState(null); const [textInput, setTextInput] = useState(''); + const [pixiApp, setPixiApp] = useState(null); useEffect(() => { if (preferences.scene) setCurrentSceneId(preferences.scene); @@ -147,6 +148,7 @@ export default function App() { outfit={currentOutfit} accessory={currentAcc} onTalkToggle={handleTalkToggle} + onAppReady={setPixiApp} /> @@ -155,7 +157,7 @@ export default function App() { - + diff --git a/frontend/src/components/KiraAvatar.tsx b/frontend/src/components/KiraAvatar.tsx index e91d522..840d335 100644 --- a/frontend/src/components/KiraAvatar.tsx +++ b/frontend/src/components/KiraAvatar.tsx @@ -7,6 +7,7 @@ interface Props { outfit: string; accessory: string | null; onTalkToggle: () => void; + onAppReady?: (app: any) => void; } type ExpressionName = 'Normal' | 'Smile' | 'Sad' | 'Angry' | 'Surprised' | 'Blushing'; @@ -82,6 +83,7 @@ export default function KiraAvatar(props: Props) { app.stage.addChild(model as any); (model as any).isInteractive = () => false; if (!mounted) return; + if (props.onAppReady) props.onAppReady(app); try { textureRef.current = { diff --git a/frontend/src/components/Live2DCat.tsx b/frontend/src/components/Live2DCat.tsx index 9746558..65fd736 100644 --- a/frontend/src/components/Live2DCat.tsx +++ b/frontend/src/components/Live2DCat.tsx @@ -1,58 +1,44 @@ import { useEffect, useRef } from 'react'; interface Props { - className?: string; + app: any; // shared Pixi Application from KiraAvatar } -export default function Live2DCat({ className }: Props) { - const canvasRef = useRef(null); +export default function Live2DCat({ app }: Props) { + const modelRef = useRef(null); useEffect(() => { + if (!app) return; let mounted = true; - const canvas = canvasRef.current; - if (!canvas) return; - - let app: any = null; const init = async () => { try { - const { Application, Ticker } = await import('pixi.js'); const { Live2DModel } = await import('pixi-live2d-display/cubism4'); + const { Ticker } = await import('pixi.js'); (Live2DModel as any).registerTicker(Ticker as any); - const w = canvas.clientWidth || 120; - const h = canvas.clientHeight || 120; - - // forceCanvas avoids WebGL context conflicts with KiraAvatar's context - app = new Application({ - view: canvas, - width: w, - height: h, - antialias: true, - resolution: Math.min(window.devicePixelRatio || 1, 2), - backgroundAlpha: 0, - autoDensity: true, - forceCanvas: true, - }); - if (!mounted) { app.destroy(true); return; } - const model = await Live2DModel.from('/live2d/models/little-cat/LittleCat.model3.json', { autoInteract: false, }); - if (!mounted) { app.destroy(true); return; } + if (!mounted) { model.destroy(); return; } - const sw = app.screen.width; - const sh = app.screen.height; - const s = Math.min((sw * 0.85) / model.width, (sh * 0.85) / model.height); + // Scale cat to ~15% of avatar canvas height + const targetH = app.screen.height * 0.18; + const s = targetH / model.height; model.scale.set(s); model.anchor.set(0.5, 0.5); - model.position.set(sw / 2, sh / 2); + + // Bottom-right corner of Kira's canvas + model.position.set( + app.screen.width - (model.width * s) / 2 - 10, + app.screen.height - (model.height * s) / 2 - 10 + ); app.stage.addChild(model as any); (model as any).isInteractive = () => false; + modelRef.current = model; try { model.motion('Idle'); } catch { /* */ } - } catch (e) { console.warn('[Live2DCat]', e); } @@ -62,15 +48,13 @@ export default function Live2DCat({ className }: Props) { return () => { mounted = false; - if (app) { app.destroy(true, { children: true }); } + if (modelRef.current && app) { + app.stage.removeChild(modelRef.current); + modelRef.current.destroy(); + modelRef.current = null; + } }; - }, []); + }, [app]); - return ( - - ); + return null; // renders on KiraAvatar's shared canvas } diff --git a/frontend/src/components/PetZone.tsx b/frontend/src/components/PetZone.tsx index 826a4f7..debfe14 100644 --- a/frontend/src/components/PetZone.tsx +++ b/frontend/src/components/PetZone.tsx @@ -1,17 +1,23 @@ import Live2DCat from './Live2DCat'; -export default function PetZone() { +interface Props { + app?: any; +} + +export default function PetZone({ app }: Props) { return (

🐱 Pet Zone

-
+ {app ? (
- + Mochi
-
+ ) : ( +

loading...

+ )}
); }