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:
@@ -33,7 +33,6 @@ export default function App() {
|
|||||||
const [currentOutfit, setCurrentOutfit] = useState('cozy-hoodie');
|
const [currentOutfit, setCurrentOutfit] = useState('cozy-hoodie');
|
||||||
const [currentAcc, setCurrentAcc] = useState<string | null>(null);
|
const [currentAcc, setCurrentAcc] = useState<string | null>(null);
|
||||||
const [textInput, setTextInput] = useState('');
|
const [textInput, setTextInput] = useState('');
|
||||||
const [pixiApp, setPixiApp] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (preferences.scene) setCurrentSceneId(preferences.scene);
|
if (preferences.scene) setCurrentSceneId(preferences.scene);
|
||||||
@@ -148,7 +147,6 @@ export default function App() {
|
|||||||
outfit={currentOutfit}
|
outfit={currentOutfit}
|
||||||
accessory={currentAcc}
|
accessory={currentAcc}
|
||||||
onTalkToggle={handleTalkToggle}
|
onTalkToggle={handleTalkToggle}
|
||||||
onAppReady={setPixiApp}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -157,7 +155,7 @@ export default function App() {
|
|||||||
<MusicPlayer />
|
<MusicPlayer />
|
||||||
<WhiteNoise />
|
<WhiteNoise />
|
||||||
<Wardrobe onOutfitChange={handleOutfitChange} onAccessoryChange={handleAccessoryChange} />
|
<Wardrobe onOutfitChange={handleOutfitChange} onAccessoryChange={handleAccessoryChange} />
|
||||||
<PetZone app={pixiApp} />
|
<PetZone />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ interface Props {
|
|||||||
outfit: string;
|
outfit: string;
|
||||||
accessory: string | null;
|
accessory: string | null;
|
||||||
onTalkToggle: () => void;
|
onTalkToggle: () => void;
|
||||||
onAppReady?: (app: any) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpressionName = 'Normal' | 'Smile' | 'Sad' | 'Angry' | 'Surprised' | 'Blushing';
|
type ExpressionName = 'Normal' | 'Smile' | 'Sad' | 'Angry' | 'Surprised' | 'Blushing';
|
||||||
@@ -83,7 +82,6 @@ export default function KiraAvatar(props: Props) {
|
|||||||
app.stage.addChild(model as any);
|
app.stage.addChild(model as any);
|
||||||
(model as any).isInteractive = () => false;
|
(model as any).isInteractive = () => false;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (props.onAppReady) props.onAppReady(app);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
textureRef.current = {
|
textureRef.current = {
|
||||||
|
|||||||
@@ -1,42 +1,56 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
app: any; // shared Pixi Application from KiraAvatar
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Live2DCat({ app }: Props) {
|
export default function Live2DCat({ className }: Props) {
|
||||||
const modelRef = useRef<any>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!app) return;
|
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
let app: any = null;
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
try {
|
try {
|
||||||
|
const pixi = await import('pixi.js');
|
||||||
const { Live2DModel } = await import('pixi-live2d-display/cubism4');
|
const { Live2DModel } = await import('pixi-live2d-display/cubism4');
|
||||||
const { Ticker } = await import('pixi.js');
|
(Live2DModel as any).registerTicker(pixi.Ticker as any);
|
||||||
(Live2DModel as any).registerTicker(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', {
|
const model = await Live2DModel.from('/live2d/models/little-cat/LittleCat.model3.json', {
|
||||||
autoInteract: false,
|
autoInteract: false,
|
||||||
});
|
});
|
||||||
if (!mounted) { model.destroy(); return; }
|
if (!mounted) { app.destroy(true); return; }
|
||||||
|
|
||||||
// Scale cat to ~15% of avatar canvas height
|
const sw = app.screen.width;
|
||||||
const targetH = app.screen.height * 0.18;
|
const sh = app.screen.height;
|
||||||
const s = targetH / model.height;
|
const s = Math.min((sw * 0.85) / model.width, (sh * 0.85) / model.height);
|
||||||
model.scale.set(s);
|
model.scale.set(s);
|
||||||
model.anchor.set(0.5, 0.5);
|
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);
|
app.stage.addChild(model as any);
|
||||||
(model as any).isInteractive = () => false;
|
(model as any).isInteractive = () => false;
|
||||||
modelRef.current = model;
|
|
||||||
|
|
||||||
try { model.motion('Idle'); } catch { /* */ }
|
try { model.motion('Idle'); } catch { /* */ }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -48,13 +62,15 @@ export default function Live2DCat({ app }: Props) {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
mounted = false;
|
mounted = false;
|
||||||
if (modelRef.current && app) {
|
if (app) { app.destroy(true, { children: true }); }
|
||||||
app.stage.removeChild(modelRef.current);
|
|
||||||
modelRef.current.destroy();
|
|
||||||
modelRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [app]);
|
}, []);
|
||||||
|
|
||||||
return null; // renders on KiraAvatar's shared canvas
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className={className}
|
||||||
|
style={{ display: 'block' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
import Live2DCat from './Live2DCat';
|
import Live2DCat from './Live2DCat';
|
||||||
|
|
||||||
interface Props {
|
export default function PetZone() {
|
||||||
app?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PetZone({ app }: Props) {
|
|
||||||
return (
|
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">
|
<h3 className="text-sm font-bold text-kira-plum mb-2 flex items-center gap-2">
|
||||||
<span>🐱</span> Pet Zone
|
<span>🐱</span> Pet Zone
|
||||||
</h3>
|
</h3>
|
||||||
{app ? (
|
<div className="flex flex-col items-center">
|
||||||
<div className="flex flex-col items-center">
|
<Live2DCat className="w-28 h-28" />
|
||||||
<Live2DCat app={app} />
|
<span className="text-[10px] text-kira-plum/60 mt-1 font-medium">Mochi</span>
|
||||||
<span className="text-[10px] text-kira-plum/60 mt-1 font-medium">Mochi</span>
|
</div>
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-xs text-kira-plum/30">loading...</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user