Skip to content

Commit 2b76e7f

Browse files
committed
Scale landing pet preview for small viewports.
ResizeObserver on the landing LCD drives pet sprite scale while capping LCD height so the hero preview stays proportional on mobile.
1 parent 0b309fb commit 2b76e7f

4 files changed

Lines changed: 62 additions & 6 deletions

File tree

app/globals.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@
116116
max-width: calc(32rem - 2.5rem - 2rem);
117117
}
118118

119+
/* Landing hero preview: cap LCD height on narrow viewports */
120+
.tamagotchi-lcd-landing {
121+
@apply aspect-[4/3] w-full shrink-0;
122+
max-height: clamp(140px, 42vw, 280px);
123+
}
124+
119125
.pet-viewport-stage {
120126
@apply flex min-h-0 w-full flex-1 flex-col items-center justify-center;
121127
}

components/landing/landing-pet-preview.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import Link from "next/link";
4-
import { useCallback, useEffect, useState } from "react";
4+
import { useCallback, useEffect, useRef, useState } from "react";
55

66
import { PetHabitat } from "@/components/pet/pet-habitat";
77
import {
@@ -23,18 +23,49 @@ function LandingPetPreviewSkeleton() {
2323
<div className="h-4 w-24 animate-pulse rounded bg-muted" />
2424
<div className="h-2 w-16 animate-pulse rounded bg-muted" />
2525
</div>
26-
<div className="tamagotchi-lcd aspect-[4/3] w-full animate-pulse bg-muted/40" />
26+
<div className="tamagotchi-lcd tamagotchi-lcd-landing animate-pulse bg-muted/40" />
2727
</div>
2828
);
2929
}
3030

31+
function computeLandingPetScale(lcdWidth: number): number {
32+
return Math.min(7, Math.max(4, Math.round(lcdWidth / 36)));
33+
}
34+
3135
export function LandingPetPreview() {
36+
const resizeObserverRef = useRef<ResizeObserver | null>(null);
3237
const [isLoading, setIsLoading] = useState(true);
3338
const [isSignedIn, setIsSignedIn] = useState(false);
39+
const [petScale, setPetScale] = useState(7);
3440
const [customization, setCustomization] = useState<AvatarCustomization>(
3541
defaultAvatarCustomization,
3642
);
3743

44+
const lcdRef = useCallback((node: HTMLDivElement | null) => {
45+
resizeObserverRef.current?.disconnect();
46+
resizeObserverRef.current = null;
47+
48+
if (!node) {
49+
return;
50+
}
51+
52+
const updateScale = (width: number) => {
53+
setPetScale(computeLandingPetScale(width));
54+
};
55+
56+
updateScale(node.getBoundingClientRect().width);
57+
58+
const observer = new ResizeObserver((entries) => {
59+
const entry = entries[0];
60+
if (entry) {
61+
updateScale(entry.contentRect.width);
62+
}
63+
});
64+
65+
observer.observe(node);
66+
resizeObserverRef.current = observer;
67+
}, []);
68+
3869
const loadPreview = useCallback(async () => {
3970
setIsLoading(true);
4071

@@ -65,6 +96,12 @@ export function LandingPetPreview() {
6596
void loadPreview();
6697
}, [loadPreview]);
6798

99+
useEffect(() => {
100+
return () => {
101+
resizeObserverRef.current?.disconnect();
102+
};
103+
}, []);
104+
68105
if (isLoading) {
69106
return <LandingPetPreviewSkeleton />;
70107
}
@@ -74,6 +111,9 @@ export function LandingPetPreview() {
74111
customization={customization}
75112
className="w-full max-w-sm"
76113
showStyleLink={false}
114+
petScale={petScale}
115+
lcdClassName="tamagotchi-lcd-landing"
116+
lcdRef={lcdRef}
77117
/>
78118
);
79119

components/pet/animated-pet-sprite.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { cn } from "@/lib/utils";
1313
type AnimatedPetSpriteProps = {
1414
customization: AvatarCustomization;
1515
className?: string;
16+
petScale?: number;
1617
};
1718

1819
type WanderPosition = {
@@ -52,6 +53,7 @@ function randomPosition(current?: WanderPosition): WanderPosition {
5253
export function AnimatedPetSprite({
5354
customization,
5455
className,
56+
petScale = 7,
5557
}: AnimatedPetSpriteProps) {
5658
const [position, setPosition] = useState<WanderPosition>({
5759
x: PET_WANDER_START.x,
@@ -98,7 +100,7 @@ export function AnimatedPetSprite({
98100
colors={customization.colors}
99101
variants={customization.variants}
100102
equippedItems={customization.equippedItems}
101-
scale={7}
103+
scale={petScale}
102104
compact
103105
className="!h-auto max-h-none"
104106
/>

components/pet/pet-habitat.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ type PetHabitatProps = {
2323
className?: string;
2424
fillViewport?: boolean;
2525
showStyleLink?: boolean;
26+
petScale?: number;
27+
lcdClassName?: string;
28+
lcdRef?: React.Ref<HTMLDivElement>;
2629
};
2730

2831
const MOOD_LABELS = {
@@ -36,6 +39,9 @@ export function PetHabitat({
3639
className,
3740
fillViewport = false,
3841
showStyleLink = true,
42+
petScale,
43+
lcdClassName: lcdClassNameOverride,
44+
lcdRef,
3945
}: PetHabitatProps) {
4046
const [condition, setCondition] = useState<AvatarCondition>(
4147
defaultAvatarCondition,
@@ -60,7 +66,9 @@ export function PetHabitat({
6066

6167
const lcdClassName = cn(
6268
"tamagotchi-lcd relative",
63-
fillViewport ? "tamagotchi-lcd-pet-fill" : "tamagotchi-lcd-pet-match",
69+
fillViewport
70+
? "tamagotchi-lcd-pet-fill"
71+
: (lcdClassNameOverride ?? "tamagotchi-lcd-pet-match"),
6472
);
6573

6674
if (!isReady) {
@@ -82,9 +90,9 @@ export function PetHabitat({
8290
</p>
8391
</div>
8492

85-
<div className={lcdClassName}>
93+
<div ref={lcdRef} className={lcdClassName}>
8694
<ParallaxRoomBackground roomId={roomId as RoomBackgroundId} />
87-
<AnimatedPetSprite customization={customization} />
95+
<AnimatedPetSprite customization={customization} petScale={petScale} />
8896
<div className="pointer-events-none absolute left-2 top-2 z-20 grid gap-1.5">
8997
<PixelGauge type="heart" value={condition.health} variant="overlay" />
9098
<PixelGauge type="energy" value={condition.energy} variant="overlay" />

0 commit comments

Comments
 (0)