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 <canvas ref={canvasRef}>
element directly in JSX that React manages. Pixi app uses .
Scale set to 82% of container.
This commit is contained in:
@@ -23,7 +23,7 @@ const OUTFIT_TEXTURES: Record<string, string> = {
|
||||
};
|
||||
|
||||
export default function KiraAvatar(props: Props) {
|
||||
const wrapRef = useRef<HTMLDivElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const modelRef = useRef<any>(null);
|
||||
const textureRef = useRef<any>(null);
|
||||
const lipSyncRef = useRef<number>(0);
|
||||
@@ -32,11 +32,10 @@ export default function KiraAvatar(props: Props) {
|
||||
const [loadError, setLoadError] = useState(false);
|
||||
const [currentExpression, setCurrentExpression] = useState<ExpressionName>('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 (
|
||||
<div className="flex flex-col items-center w-full h-full overflow-hidden">
|
||||
<div className="relative w-full flex-1" style={{ minHeight: 250 }}>
|
||||
{/* Pixi canvas mounts here */}
|
||||
<div ref={wrapRef} className="w-full h-full" />
|
||||
{/* Live2D canvas */}
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-full block"
|
||||
style={{ opacity: live2dReady ? 1 : 0 }}
|
||||
/>
|
||||
|
||||
{/* SVG fallback when Live2D fails */}
|
||||
{/* SVG fallback */}
|
||||
{(!live2dReady || loadError) && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<AnimatedAvatar
|
||||
@@ -207,7 +208,7 @@ export default function KiraAvatar(props: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading spinner */}
|
||||
{/* Loading */}
|
||||
{!live2dReady && !loadError && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2">
|
||||
<div className="w-8 h-8 border-3 border-kira-pink border-t-transparent rounded-full animate-spin" />
|
||||
@@ -215,7 +216,7 @@ export default function KiraAvatar(props: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Expression buttons overlay top */}
|
||||
{/* Expression buttons */}
|
||||
{live2dReady && (
|
||||
<>
|
||||
<div className="absolute top-1 left-0 right-0 flex gap-1 justify-center flex-wrap pointer-events-none">
|
||||
@@ -238,7 +239,7 @@ export default function KiraAvatar(props: Props) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Talk button overlay bottom-center */}
|
||||
{/* Talk button */}
|
||||
<div className="absolute bottom-2 left-0 right-0 flex justify-center pointer-events-none">
|
||||
<button
|
||||
onClick={props.onTalkToggle}
|
||||
@@ -253,7 +254,7 @@ export default function KiraAvatar(props: Props) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Status overlay bottom-right */}
|
||||
{/* Status */}
|
||||
<div className="absolute bottom-2 right-3 flex items-center gap-2 text-xs text-kira-plum/50 pointer-events-none">
|
||||
<span className={`w-2 h-2 rounded-full ${
|
||||
props.isSpeaking ? 'bg-kira-pink animate-pulse'
|
||||
|
||||
Reference in New Issue
Block a user