fix(pets): cat renders on shared KiraAvatar canvas via onAppReady callback
Single WebGL context, no bindBuffer spam. Cat model loads onto KiraAvatar's stage and positions itself at bottom-right corner. PetZone passes app prop through to Live2DCat.
This commit is contained in:
@@ -33,6 +33,7 @@ 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);
|
||||||
@@ -147,6 +148,7 @@ export default function App() {
|
|||||||
outfit={currentOutfit}
|
outfit={currentOutfit}
|
||||||
accessory={currentAcc}
|
accessory={currentAcc}
|
||||||
onTalkToggle={handleTalkToggle}
|
onTalkToggle={handleTalkToggle}
|
||||||
|
onAppReady={setPixiApp}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -155,7 +157,7 @@ export default function App() {
|
|||||||
<MusicPlayer />
|
<MusicPlayer />
|
||||||
<WhiteNoise />
|
<WhiteNoise />
|
||||||
<Wardrobe onOutfitChange={handleOutfitChange} onAccessoryChange={handleAccessoryChange} />
|
<Wardrobe onOutfitChange={handleOutfitChange} onAccessoryChange={handleAccessoryChange} />
|
||||||
<PetZone />
|
<PetZone app={pixiApp} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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';
|
||||||
@@ -82,6 +83,7 @@ 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,58 +1,44 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
app: any; // shared Pixi Application from KiraAvatar
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Live2DCat({ className }: Props) {
|
export default function Live2DCat({ app }: Props) {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const modelRef = useRef<any>(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 { Application, Ticker } = 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(Ticker as any);
|
(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', {
|
const model = await Live2DModel.from('/live2d/models/little-cat/LittleCat.model3.json', {
|
||||||
autoInteract: false,
|
autoInteract: false,
|
||||||
});
|
});
|
||||||
if (!mounted) { app.destroy(true); return; }
|
if (!mounted) { model.destroy(); return; }
|
||||||
|
|
||||||
const sw = app.screen.width;
|
// Scale cat to ~15% of avatar canvas height
|
||||||
const sh = app.screen.height;
|
const targetH = app.screen.height * 0.18;
|
||||||
const s = Math.min((sw * 0.85) / model.width, (sh * 0.85) / model.height);
|
const s = targetH / 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) {
|
||||||
console.warn('[Live2DCat]', e);
|
console.warn('[Live2DCat]', e);
|
||||||
}
|
}
|
||||||
@@ -62,15 +48,13 @@ export default function Live2DCat({ className }: Props) {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
mounted = false;
|
mounted = false;
|
||||||
if (app) { app.destroy(true, { children: true }); }
|
if (modelRef.current && app) {
|
||||||
};
|
app.stage.removeChild(modelRef.current);
|
||||||
}, []);
|
modelRef.current.destroy();
|
||||||
|
modelRef.current = null;
|
||||||
return (
|
}
|
||||||
<canvas
|
};
|
||||||
ref={canvasRef}
|
}, [app]);
|
||||||
className={className}
|
|
||||||
style={{ display: 'block' }}
|
return null; // renders on KiraAvatar's shared canvas
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
import Live2DCat from './Live2DCat';
|
import Live2DCat from './Live2DCat';
|
||||||
|
|
||||||
export default function PetZone() {
|
interface Props {
|
||||||
|
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: 140 }}>
|
||||||
<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>
|
||||||
<div className="flex items-center justify-center">
|
{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>
|
||||||
</div>
|
) : (
|
||||||
|
<p className="text-xs text-kira-plum/30">loading...</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user