From 43a392e5f5572c636689b4a55977b6511261a4d4 Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Fri, 5 Jun 2026 13:25:10 -0400 Subject: [PATCH] fix(pets): use preferWebGLVersion:1 for cat canvas to avoid context conflicts Cat gets its own canvas with WebGL1 context (Kira uses WebGL2 by default). Different GL versions don't share buffers, so no bindBuffer spam. Cat now renders in the PetZone section of the right sidebar where it belongs. Removed all shared-context onAppReady plumbing. --- frontend/src/App.tsx | 4 +- frontend/src/components/KiraAvatar.tsx | 2 - frontend/src/components/Live2DCat.tsx | 64 ++++++++++++++++---------- frontend/src/components/PetZone.tsx | 20 +++----- 4 files changed, 47 insertions(+), 43 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8a4b51f..1960f1c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -33,7 +33,6 @@ 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); @@ -148,7 +147,6 @@ export default function App() { outfit={currentOutfit} accessory={currentAcc} onTalkToggle={handleTalkToggle} - onAppReady={setPixiApp} /> @@ -157,7 +155,7 @@ export default function App() { - + diff --git a/frontend/src/components/KiraAvatar.tsx b/frontend/src/components/KiraAvatar.tsx index 840d335..e91d522 100644 --- a/frontend/src/components/KiraAvatar.tsx +++ b/frontend/src/components/KiraAvatar.tsx @@ -7,7 +7,6 @@ interface Props { outfit: string; accessory: string | null; onTalkToggle: () => void; - onAppReady?: (app: any) => void; } type ExpressionName = 'Normal' | 'Smile' | 'Sad' | 'Angry' | 'Surprised' | 'Blushing'; @@ -83,7 +82,6 @@ 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 65fd736..ab4c50d 100644 --- a/frontend/src/components/Live2DCat.tsx +++ b/frontend/src/components/Live2DCat.tsx @@ -1,42 +1,56 @@ import { useEffect, useRef } from 'react'; interface Props { - app: any; // shared Pixi Application from KiraAvatar + className?: string; } -export default function Live2DCat({ app }: Props) { - const modelRef = useRef(null); +export default function Live2DCat({ className }: Props) { + const canvasRef = 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 pixi = 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); + (Live2DModel as any).registerTicker(pixi.Ticker as any); + + const w = canvas.clientWidth || 120; + const h = canvas.clientHeight || 120; + + // Use explicit CanvasRenderer to avoid WebGL context conflict with KiraAvatar + app = new pixi.Application({ + view: canvas, + width: w, + height: h, + antialias: true, + resolution: Math.min(window.devicePixelRatio || 1, 2), + backgroundAlpha: 0, + autoDensity: true, + // Use WebGL1 context (more compatible with separate contexts) + preferWebGLVersion: 1, + } as any); + if (!mounted) { app.destroy(true); return; } const model = await Live2DModel.from('/live2d/models/little-cat/LittleCat.model3.json', { autoInteract: false, }); - if (!mounted) { model.destroy(); return; } + if (!mounted) { app.destroy(true); return; } - // Scale cat to ~15% of avatar canvas height - const targetH = app.screen.height * 0.18; - const s = targetH / model.height; + const sw = app.screen.width; + const sh = app.screen.height; + const s = Math.min((sw * 0.85) / model.width, (sh * 0.85) / model.height); model.scale.set(s); model.anchor.set(0.5, 0.5); - - // 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 - ); + model.position.set(sw / 2, sh / 2); app.stage.addChild(model as any); (model as any).isInteractive = () => false; - modelRef.current = model; try { model.motion('Idle'); } catch { /* */ } } catch (e) { @@ -48,13 +62,15 @@ export default function Live2DCat({ app }: Props) { return () => { mounted = false; - if (modelRef.current && app) { - app.stage.removeChild(modelRef.current); - modelRef.current.destroy(); - modelRef.current = null; - } + if (app) { app.destroy(true, { children: true }); } }; - }, [app]); + }, []); - return null; // renders on KiraAvatar's shared canvas + return ( + + ); } diff --git a/frontend/src/components/PetZone.tsx b/frontend/src/components/PetZone.tsx index debfe14..d530a8f 100644 --- a/frontend/src/components/PetZone.tsx +++ b/frontend/src/components/PetZone.tsx @@ -1,23 +1,15 @@ import Live2DCat from './Live2DCat'; -interface Props { - app?: any; -} - -export default function PetZone({ app }: Props) { +export default function PetZone() { return ( -
+

🐱 Pet Zone

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

loading...

- )} +
+ + Mochi +
); }