fix(pets): dynamically position cat at PetZone DOM element

Cat position is now measured from the [data-petzone] DOM element's
getBoundingClientRect(), so it always aligns with the PetZone section
regardless of window size or sidebar content height.
Removed Live2DCat import from PetZone (cat renders on shared stage).
This commit is contained in:
2026-06-05 13:51:50 -04:00
parent 04ad706de6
commit 73fe77f9aa
2 changed files with 27 additions and 38 deletions
+14 -22
View File
@@ -12,49 +12,38 @@ interface Props {
* - Mochi the cat (PetZone, bottom of right sidebar) * - Mochi the cat (PetZone, bottom of right sidebar)
* *
* Canvas sits behind UI panels (z-0, pointer-events: none). * Canvas sits behind UI panels (z-0, pointer-events: none).
* Cat position is dynamically measured from the [data-petzone] DOM element.
*/ */
// --- Layout constants (must match Tailwind in App.tsx) ---
// Left sidebar: w-72 = 288px, gap-4 = 16px, px-4 = 16px
// Right sidebar: w-64 = 256px, bottom bar: ~40px
const PAD = 16; const PAD = 16;
const LEFT_W = 288; const LEFT_W = 288;
const GAP = 16; const GAP = 16;
const RIGHT_W = 256; const RIGHT_W = 256;
const BOTTOM_BAR_H = 40;
function positionModels( function positionKira(kira: any, appW: number, appH: number) {
kira: any, cat: any,
appW: number, appH: number,
) {
// Center panel center
const centerLeft = PAD + LEFT_W + GAP; const centerLeft = PAD + LEFT_W + GAP;
const centerRight = appW - GAP - RIGHT_W - PAD; const centerRight = appW - GAP - RIGHT_W - PAD;
const centerMidX = (centerLeft + centerRight) / 2; const centerMidX = (centerLeft + centerRight) / 2;
const centerMidY = appH * 0.46; const centerMidY = appH * 0.46;
if (kira) {
const s = Math.min(((centerRight - centerLeft) * 0.78) / kira.width, (appH * 0.78) / kira.height); const s = Math.min(((centerRight - centerLeft) * 0.78) / kira.width, (appH * 0.78) / kira.height);
kira.scale.set(s); kira.scale.set(s);
kira.anchor.set(0.5, 0.5); kira.anchor.set(0.5, 0.5);
kira.position.set(centerMidX, centerMidY); kira.position.set(centerMidX, centerMidY);
} }
// Right sidebar center function positionCat(cat: any) {
const rightLeft = appW - RIGHT_W - PAD; const el = document.querySelector('[data-petzone]');
const rightMidX = rightLeft + RIGHT_W / 2; if (!el || !cat) return;
const catY = appH - BOTTOM_BAR_H - 50; const r = el.getBoundingClientRect();
if (cat) {
// Set scale to 1 first to get natural bounds, then compute target
cat.scale.set(1); cat.scale.set(1);
const naturalH = cat.getLocalBounds().height; const naturalH = cat.getLocalBounds().height;
const targetPx = 100; // ~100px tall rendered const targetPx = 100;
const cs = naturalH > 0 ? targetPx / naturalH : 0.05; const cs = naturalH > 0 ? targetPx / naturalH : 0.05;
cat.scale.set(cs); cat.scale.set(cs);
cat.anchor.set(0.5, 0.5); cat.anchor.set(0.5, 0.5);
cat.position.set(rightMidX, catY); 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 }: Props) {
@@ -92,7 +81,8 @@ export default function Live2DStage({ onKiraReady, onReady }: Props) {
const onResize = () => { const onResize = () => {
app.renderer.resize(window.innerWidth, window.innerHeight); app.renderer.resize(window.innerWidth, window.innerHeight);
positionModels(kiraModel, catModel, app.screen.width, app.screen.height); if (kiraModel) positionKira(kiraModel, app.screen.width, app.screen.height);
if (catModel) positionCat(catModel);
}; };
window.addEventListener('resize', onResize); window.addEventListener('resize', onResize);
@@ -102,7 +92,7 @@ export default function Live2DStage({ onKiraReady, onReady }: Props) {
}); });
if (!mounted) { app.destroy(true); return; } if (!mounted) { app.destroy(true); return; }
positionModels(kiraModel, null, app.screen.width, app.screen.height); positionKira(kiraModel, app.screen.width, app.screen.height);
(kiraModel as any).isInteractive = () => false; (kiraModel as any).isInteractive = () => false;
app.stage.addChild(kiraModel as any); app.stage.addChild(kiraModel as any);
@@ -118,7 +108,9 @@ export default function Live2DStage({ onKiraReady, onReady }: Props) {
}); });
if (!mounted) return; if (!mounted) return;
positionModels(kiraModel, catModel, app.screen.width, app.screen.height); // Small delay to ensure PetZone DOM is rendered
await new Promise(r => setTimeout(r, 100));
positionCat(catModel);
(catModel as any).isInteractive = () => false; (catModel as any).isInteractive = () => false;
app.stage.addChild(catModel as any); app.stage.addChild(catModel as any);
+3 -6
View File
@@ -1,14 +1,11 @@
import Live2DCat from './Live2DCat';
export default function PetZone() { export default function PetZone() {
return ( return (
<div className="p-4 relative overflow-hidden" style={{ minHeight: 160 }}> <div data-petzone 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 flex-col items-center"> <div className="flex flex-col items-center pt-2">
<Live2DCat className="w-28 h-28" /> <span className="text-[10px] text-kira-plum/60 font-medium">Mochi</span>
<span className="text-[10px] text-kira-plum/60 mt-1 font-medium">Mochi</span>
</div> </div>
</div> </div>
); );