From 95f97fa8976bae580b9eb010b33cdb5267942296 Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Fri, 5 Jun 2026 10:10:03 -0400 Subject: [PATCH] fix(avatar): declarative canvas element in JSX; remove manual DOM append React was potentially clearing the canvas on re-render because we appended it manually to a div. Now using a element directly in JSX that React manages. Pixi app uses . Scale set to 82% of container. --- frontend/src/components/KiraAvatar.tsx | 41 +++++++++++++------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/KiraAvatar.tsx b/frontend/src/components/KiraAvatar.tsx index 1e078fd..f90a16f 100644 --- a/frontend/src/components/KiraAvatar.tsx +++ b/frontend/src/components/KiraAvatar.tsx @@ -23,7 +23,7 @@ const OUTFIT_TEXTURES: Record = { }; export default function KiraAvatar(props: Props) { - const wrapRef = useRef(null); + const canvasRef = useRef(null); const modelRef = useRef(null); const textureRef = useRef(null); const lipSyncRef = useRef(0); @@ -32,11 +32,10 @@ export default function KiraAvatar(props: Props) { const [loadError, setLoadError] = useState(false); const [currentExpression, setCurrentExpression] = useState('Normal'); - // Initialize Live2D useEffect(() => { let mounted = true; - const container = wrapRef.current; - if (!container) return; + const canvas = canvasRef.current; + if (!canvas) return; let app: any = null; let model: any = null; @@ -51,8 +50,13 @@ export default function KiraAvatar(props: Props) { const { Live2DModel } = await import('pixi-live2d-display/cubism4'); (Live2DModel as any).registerTicker(Ticker as any); + const w = canvas.clientWidth || 260; + const h = canvas.clientHeight || 320; + app = new Application({ - resizeTo: container, + view: canvas, + width: w, + height: h, antialias: true, resolution: Math.min(window.devicePixelRatio || 1, 2), backgroundAlpha: 0, @@ -60,8 +64,6 @@ export default function KiraAvatar(props: Props) { }); if (!mounted) { app.destroy(true); return; } - container.appendChild(app.view as HTMLCanvasElement); - model = await Live2DModel.from('/live2d/models/kira/kira.model3.json', { autoInteract: false, }); @@ -70,9 +72,7 @@ export default function KiraAvatar(props: Props) { const fit = () => { const sw = app.screen.width; const sh = app.screen.height; - // Scale to fit within the container, leaving margin so nothing clips - const margin = 0.9; // 90% of container - const s = Math.min((sw * margin) / model.width, (sh * margin) / model.height); + const s = Math.min((sw * 0.82) / model.width, (sh * 0.82) / model.height); model.scale.set(s); model.anchor.set(0.5, 0.5); model.position.set(sw / 2, sh / 2); @@ -82,9 +82,6 @@ export default function KiraAvatar(props: Props) { app.stage.addChild(model as any); (model as any).isInteractive = () => false; - // Re-fit on resize - app.renderer.on('resize', fit); - try { textureRef.current = { index: 2, @@ -191,10 +188,14 @@ export default function KiraAvatar(props: Props) { return (
- {/* Pixi canvas mounts here */} -
+ {/* Live2D canvas */} + - {/* SVG fallback when Live2D fails */} + {/* SVG fallback */} {(!live2dReady || loadError) && (
)} - {/* Loading spinner */} + {/* Loading */} {!live2dReady && !loadError && (
@@ -215,7 +216,7 @@ export default function KiraAvatar(props: Props) {
)} - {/* Expression buttons overlay top */} + {/* Expression buttons */} {live2dReady && ( <>
@@ -238,7 +239,7 @@ export default function KiraAvatar(props: Props) {
- {/* Talk button overlay bottom-center */} + {/* Talk button */}
- {/* Status overlay bottom-right */} + {/* Status */}