diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index a073cdb..4518cec 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -110,6 +110,7 @@ export default function App() {
onKiraReady={(model) => { kiraModelRef.current = model; }}
onReady={() => setLive2dReady(true)}
isSpeaking={isKiraSpeaking}
+ outfit={currentOutfit}
/>
diff --git a/frontend/src/components/Live2DStage.tsx b/frontend/src/components/Live2DStage.tsx
index a985969..1546738 100644
--- a/frontend/src/components/Live2DStage.tsx
+++ b/frontend/src/components/Live2DStage.tsx
@@ -4,6 +4,7 @@ interface Props {
onKiraReady?: (model: any) => void;
onReady?: () => void;
isSpeaking?: boolean;
+ outfit?: string;
}
/**
@@ -46,8 +47,10 @@ function positionCat(cat: any) {
cat.position.set(r.left + r.width / 2, r.top + r.height / 2 + 10);
}
-export default function Live2DStage({ onKiraReady, onReady }: Props) {
+export default function Live2DStage({ onKiraReady, onReady, outfit }: Props) {
const canvasRef = useRef(null);
+ const kiraModelRef = useRef(null);
+ const clothTexIdxRef = useRef(-1);
useEffect(() => {
let mounted = true;
@@ -96,6 +99,22 @@ export default function Live2DStage({ onKiraReady, onReady }: Props) {
(kiraModel as any).isInteractive = () => false;
app.stage.addChild(kiraModel as any);
+ kiraModelRef.current = kiraModel;
+
+ // Find the clothing texture index (texture_02 = the outfit sheet)
+ try {
+ const core = (kiraModel as any).internalModel?.coreModel;
+ if (core) {
+ const drawCount = core.getDrawableCount();
+ // texture_02 is the last texture, find which drawable uses it
+ // We'll swap the PIXI texture directly on the model's textures array
+ const textures = (kiraModel as any).internalModel?.textures;
+ if (textures && textures.length >= 3) {
+ clothTexIdxRef.current = 2; // texture_02 is the outfit
+ }
+ }
+ } catch {}
+
try { kiraModel.motion('Idle'); } catch {}
try { kiraModel.expression('Normal'); } catch {}
@@ -135,6 +154,42 @@ export default function Live2DStage({ onKiraReady, onReady }: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+ // Outfit swap effect
+ useEffect(() => {
+ const model = kiraModelRef.current;
+ const texIdx = clothTexIdxRef.current;
+ if (!model || texIdx < 0 || !outfit) return;
+
+ const outfitSrc = `/live2d/models/kira/outfits/${outfit}.png`;
+
+ const swapTexture = async () => {
+ try {
+ const textures = (model as any).internalModel?.textures;
+ if (!textures || !textures[texIdx]) return;
+
+ const PIXI = await import('pixi.js');
+ const newBase = PIXI.BaseTexture.from(outfitSrc);
+
+ const doSwap = () => {
+ if (newBase.valid) {
+ textures[texIdx].baseTexture = newBase;
+ textures[texIdx].update();
+ }
+ };
+
+ if (newBase.valid) {
+ doSwap();
+ } else {
+ newBase.once('loaded', doSwap);
+ }
+ } catch (e) {
+ console.warn('[Live2DStage] outfit swap failed:', e);
+ }
+ };
+
+ swapTexture();
+ }, [outfit]);
+
return (