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.
This commit is contained in:
2026-06-05 13:25:10 -04:00
parent 1f8bcf6b4f
commit 43a392e5f5
4 changed files with 47 additions and 43 deletions
+1 -3
View File
@@ -33,7 +33,6 @@ export default function App() {
const [currentOutfit, setCurrentOutfit] = useState('cozy-hoodie');
const [currentAcc, setCurrentAcc] = useState<string | null>(null);
const [textInput, setTextInput] = useState('');
const [pixiApp, setPixiApp] = useState<any>(null);
useEffect(() => {
if (preferences.scene) setCurrentSceneId(preferences.scene);
@@ -148,7 +147,6 @@ export default function App() {
outfit={currentOutfit}
accessory={currentAcc}
onTalkToggle={handleTalkToggle}
onAppReady={setPixiApp}
/>
</div>
@@ -157,7 +155,7 @@ export default function App() {
<MusicPlayer />
<WhiteNoise />
<Wardrobe onOutfitChange={handleOutfitChange} onAccessoryChange={handleAccessoryChange} />
<PetZone app={pixiApp} />
<PetZone />
</div>
</div>
-2
View File
@@ -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 = {
+40 -24
View File
@@ -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<any>(null);
export default function Live2DCat({ className }: Props) {
const canvasRef = useRef<HTMLCanvasElement>(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 (
<canvas
ref={canvasRef}
className={className}
style={{ display: 'block' }}
/>
);
}
+3 -11
View File
@@ -1,23 +1,15 @@
import Live2DCat from './Live2DCat';
interface Props {
app?: any;
}
export default function PetZone({ app }: Props) {
export default function PetZone() {
return (
<div className="p-4 relative overflow-hidden" style={{ minHeight: 140 }}>
<div className="p-4 relative overflow-hidden" style={{ minHeight: 160 }}>
<h3 className="text-sm font-bold text-kira-plum mb-2 flex items-center gap-2">
<span>🐱</span> Pet Zone
</h3>
{app ? (
<div className="flex flex-col items-center">
<Live2DCat app={app} />
<Live2DCat className="w-28 h-28" />
<span className="text-[10px] text-kira-plum/60 mt-1 font-medium">Mochi</span>
</div>
) : (
<p className="text-xs text-kira-plum/30">loading...</p>
)}
</div>
);
}