fix(avatar): use ResizeObserver for accurate container sizing; force canvas CSS 100%; reduce margin to 68%
Problem: flex layout wasn't ready on first paint, so clientWidth fell back to 400px. Canvas was 400px wide but parent was only 288px, causing the avatar to be clipped on the right. Fix: ResizeObserver measures real laid-out size before init. Canvas forced to width/height 100% via CSS so it never overflows. Model scaled to 68% with centered anchor. Resize handled dynamically.
This commit is contained in:
@@ -39,49 +39,55 @@ export default function KiraAvatar(props: Props) {
|
||||
const container = canvasRef.current;
|
||||
if (!container) return;
|
||||
|
||||
(async () => {
|
||||
let app: any = null;
|
||||
let model: any = null;
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
const resp = await fetch('/live2d/models/kira/kira.model3.json', { method: 'HEAD' });
|
||||
if (!resp.ok) {
|
||||
if (mounted) setLoadError(true);
|
||||
return;
|
||||
}
|
||||
if (!resp.ok) { if (mounted) setLoadError(true); return; }
|
||||
|
||||
await loadScript('/live2d/cubism/live2dcubismcore.min.js');
|
||||
const { Application, Ticker } = await import('pixi.js');
|
||||
const { Live2DModel } = await import('pixi-live2d-display/cubism4');
|
||||
|
||||
(Live2DModel as any).registerTicker(Ticker as any);
|
||||
|
||||
const containerW = container.clientWidth || 400;
|
||||
const containerH = container.clientHeight || 400;
|
||||
const app = new Application({
|
||||
width: containerW,
|
||||
height: containerH,
|
||||
// Measure real container size (flex layout may not be ready on first paint)
|
||||
const rect = container.getBoundingClientRect();
|
||||
const w = Math.max(Math.round(rect.width), 260);
|
||||
const h = Math.max(Math.round(rect.height), 260);
|
||||
|
||||
app = new Application({
|
||||
width: w,
|
||||
height: h,
|
||||
antialias: true,
|
||||
resolution: Math.min(window.devicePixelRatio || 1, 2),
|
||||
backgroundAlpha: 0,
|
||||
autoDensity: true,
|
||||
});
|
||||
appRef.current = app;
|
||||
if (!mounted) { app.destroy(true); return; }
|
||||
|
||||
container.appendChild(app.view as HTMLCanvasElement);
|
||||
// Force canvas to fill container via CSS so it never overflows
|
||||
const canvas = app.view as HTMLCanvasElement;
|
||||
canvas.style.width = '100%';
|
||||
canvas.style.height = '100%';
|
||||
canvas.style.display = 'block';
|
||||
container.appendChild(canvas);
|
||||
|
||||
const model = await Live2DModel.from('/live2d/models/kira/kira.model3.json', {
|
||||
model = await Live2DModel.from('/live2d/models/kira/kira.model3.json', {
|
||||
autoInteract: false,
|
||||
});
|
||||
modelRef.current = model;
|
||||
|
||||
// Fit model to fill the canvas — leave generous margin to avoid clipping
|
||||
const maxW = containerW * 0.72;
|
||||
const maxH = containerH * 0.72;
|
||||
// Fit model with generous margin to avoid clipping
|
||||
const maxW = w * 0.68;
|
||||
const maxH = h * 0.68;
|
||||
const scale = Math.min(maxW / model.width, maxH / model.height);
|
||||
model.scale.set(scale);
|
||||
model.anchor.set(0.5, 0.52);
|
||||
model.position.set(app.screen.width / 2, app.screen.height / 2 + 6);
|
||||
model.anchor.set(0.5, 0.5);
|
||||
model.position.set(app.screen.width / 2, app.screen.height / 2);
|
||||
|
||||
app.stage.addChild(model as any);
|
||||
|
||||
(model as any).isInteractive = () => false;
|
||||
|
||||
try {
|
||||
@@ -107,16 +113,34 @@ export default function KiraAvatar(props: Props) {
|
||||
console.warn('[Live2D]', e);
|
||||
if (mounted) setLoadError(true);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
// Use ResizeObserver so we init with the real laid-out size
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
if (app) {
|
||||
// Already init'd — handle resize
|
||||
const cr = entries[0].contentRect;
|
||||
app.renderer.resize(Math.round(cr.width), Math.round(cr.height));
|
||||
if (model) {
|
||||
const maxW = cr.width * 0.68;
|
||||
const maxH = cr.height * 0.68;
|
||||
const s = Math.min(maxW / model.width, maxH / model.height);
|
||||
model.scale.set(s);
|
||||
model.position.set(app.screen.width / 2, app.screen.height / 2);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// First measurement — run init
|
||||
init();
|
||||
});
|
||||
ro.observe(container);
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
ro.disconnect();
|
||||
cancelAnimationFrame(lipSyncRef.current);
|
||||
clearInterval(idleExprRef.current ?? undefined);
|
||||
if (appRef.current) {
|
||||
appRef.current.destroy(true, { children: true });
|
||||
appRef.current = null;
|
||||
}
|
||||
if (app) { app.destroy(true, { children: true }); }
|
||||
modelRef.current = null;
|
||||
textureRef.current = null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user