From 2b76e7f34ccff695087ff44fcaa809f896765add Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 24 May 2026 05:15:37 -0600 Subject: [PATCH] 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. --- app/globals.css | 6 +++ components/landing/landing-pet-preview.tsx | 44 +++++++++++++++++++++- components/pet/animated-pet-sprite.tsx | 4 +- components/pet/pet-habitat.tsx | 14 +++++-- 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/app/globals.css b/app/globals.css index 9b9551f..7e082c4 100644 --- a/app/globals.css +++ b/app/globals.css @@ -116,6 +116,12 @@ max-width: calc(32rem - 2.5rem - 2rem); } + /* Landing hero preview: cap LCD height on narrow viewports */ + .tamagotchi-lcd-landing { + @apply aspect-[4/3] w-full shrink-0; + max-height: clamp(140px, 42vw, 280px); + } + .pet-viewport-stage { @apply flex min-h-0 w-full flex-1 flex-col items-center justify-center; } diff --git a/components/landing/landing-pet-preview.tsx b/components/landing/landing-pet-preview.tsx index bff1178..78277c0 100644 --- a/components/landing/landing-pet-preview.tsx +++ b/components/landing/landing-pet-preview.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { PetHabitat } from "@/components/pet/pet-habitat"; import { @@ -23,18 +23,49 @@ function LandingPetPreviewSkeleton() {
-
+
); } +function computeLandingPetScale(lcdWidth: number): number { + return Math.min(7, Math.max(4, Math.round(lcdWidth / 36))); +} + export function LandingPetPreview() { + const resizeObserverRef = useRef(null); const [isLoading, setIsLoading] = useState(true); const [isSignedIn, setIsSignedIn] = useState(false); + const [petScale, setPetScale] = useState(7); const [customization, setCustomization] = useState( defaultAvatarCustomization, ); + const lcdRef = useCallback((node: HTMLDivElement | null) => { + resizeObserverRef.current?.disconnect(); + resizeObserverRef.current = null; + + if (!node) { + return; + } + + const updateScale = (width: number) => { + setPetScale(computeLandingPetScale(width)); + }; + + updateScale(node.getBoundingClientRect().width); + + const observer = new ResizeObserver((entries) => { + const entry = entries[0]; + if (entry) { + updateScale(entry.contentRect.width); + } + }); + + observer.observe(node); + resizeObserverRef.current = observer; + }, []); + const loadPreview = useCallback(async () => { setIsLoading(true); @@ -65,6 +96,12 @@ export function LandingPetPreview() { void loadPreview(); }, [loadPreview]); + useEffect(() => { + return () => { + resizeObserverRef.current?.disconnect(); + }; + }, []); + if (isLoading) { return ; } @@ -74,6 +111,9 @@ export function LandingPetPreview() { customization={customization} className="w-full max-w-sm" showStyleLink={false} + petScale={petScale} + lcdClassName="tamagotchi-lcd-landing" + lcdRef={lcdRef} /> ); diff --git a/components/pet/animated-pet-sprite.tsx b/components/pet/animated-pet-sprite.tsx index 620701d..95da511 100644 --- a/components/pet/animated-pet-sprite.tsx +++ b/components/pet/animated-pet-sprite.tsx @@ -13,6 +13,7 @@ import { cn } from "@/lib/utils"; type AnimatedPetSpriteProps = { customization: AvatarCustomization; className?: string; + petScale?: number; }; type WanderPosition = { @@ -52,6 +53,7 @@ function randomPosition(current?: WanderPosition): WanderPosition { export function AnimatedPetSprite({ customization, className, + petScale = 7, }: AnimatedPetSpriteProps) { const [position, setPosition] = useState({ x: PET_WANDER_START.x, @@ -98,7 +100,7 @@ export function AnimatedPetSprite({ colors={customization.colors} variants={customization.variants} equippedItems={customization.equippedItems} - scale={7} + scale={petScale} compact className="!h-auto max-h-none" /> diff --git a/components/pet/pet-habitat.tsx b/components/pet/pet-habitat.tsx index 2867d91..d3a3809 100644 --- a/components/pet/pet-habitat.tsx +++ b/components/pet/pet-habitat.tsx @@ -23,6 +23,9 @@ type PetHabitatProps = { className?: string; fillViewport?: boolean; showStyleLink?: boolean; + petScale?: number; + lcdClassName?: string; + lcdRef?: React.Ref; }; const MOOD_LABELS = { @@ -36,6 +39,9 @@ export function PetHabitat({ className, fillViewport = false, showStyleLink = true, + petScale, + lcdClassName: lcdClassNameOverride, + lcdRef, }: PetHabitatProps) { const [condition, setCondition] = useState( defaultAvatarCondition, @@ -60,7 +66,9 @@ export function PetHabitat({ const lcdClassName = cn( "tamagotchi-lcd relative", - fillViewport ? "tamagotchi-lcd-pet-fill" : "tamagotchi-lcd-pet-match", + fillViewport + ? "tamagotchi-lcd-pet-fill" + : (lcdClassNameOverride ?? "tamagotchi-lcd-pet-match"), ); if (!isReady) { @@ -82,9 +90,9 @@ export function PetHabitat({

-
+
- +