diff --git a/src/components/student-space/EngineHost.tsx b/src/components/student-space/EngineHost.tsx index 90da055..3cae451 100644 --- a/src/components/student-space/EngineHost.tsx +++ b/src/components/student-space/EngineHost.tsx @@ -51,11 +51,17 @@ export function EngineHost({ children, showOnboardingFlow = true, hideCompanion = false, + landingShowcase = false, }: { className?: string children?: ReactNode showOnboardingFlow?: boolean hideCompanion?: boolean + // When true, populate the island (all flowers, the tree, butterflies) + // so signed-out visitors see a mature island instead of the sparse + // one. Reverts on cleanup so a sign-in transition lands cleanly on + // the empty onboarding stage. + landingShowcase?: boolean }) { const containerRef = useRef(null) const [error, setError] = useState(null) @@ -156,6 +162,38 @@ export function EngineHost({ view?.fruits?.hideAll?.() }, [game]) + // Landing-page showcase: when a signed-out visitor is on /onboarding the + // engine renders behind the login surface. Surface every flower, the + // tree, and the full butterfly count so the preview reads as "what a + // mature island looks like" rather than the sparse onboarding start + // state. Cleanup re-hides them so a sign-in transition drops cleanly + // onto the empty onboarding stage. + useEffect(() => { + if (!game || !landingShowcase) return + const view = ( + game as unknown as { + view?: { + flowers?: { showAll?: () => void; hideAll?: () => void } + tree?: { showAll?: () => void; hideAll?: () => void } + butterflies?: { + showAll?: () => void + hideAll?: () => void + showCount?: (n: number) => void + } + } + } + ).view + if (!view) return + view.flowers?.showAll?.() + view.tree?.showAll?.() + view.butterflies?.showAll?.() + return () => { + view.flowers?.hideAll?.() + view.tree?.hideAll?.() + view.butterflies?.hideAll?.() + } + }, [game, landingShowcase]) + useEffect(() => { const container = containerRef.current if (!container) return diff --git a/src/lib/student-space/camera-tuner.ts b/src/lib/student-space/camera-tuner.ts index c401c31..7f2c7b3 100644 --- a/src/lib/student-space/camera-tuner.ts +++ b/src/lib/student-space/camera-tuner.ts @@ -131,9 +131,12 @@ export const DEFAULT_PRESETS: Readonly = Object.freeze({ durationMs: 800, }, 'login-orbit': { - azimuthDegPerSec: 1, - distance: 33.9, - pitchDeg: 35, + // Slow, cinematic arc — ~3°/s reads as ambient sweep without the + // dizziness a faster rotation would introduce; the populated island + // gets to show off over the dwell time on the login surface. + azimuthDegPerSec: 3, + distance: 32, + pitchDeg: 28, }, }) diff --git a/src/routes/_app.tsx b/src/routes/_app.tsx index ed7632e..61b20bd 100644 --- a/src/routes/_app.tsx +++ b/src/routes/_app.tsx @@ -1,6 +1,8 @@ import { createFileRoute, Outlet, redirect, useLocation } from '@tanstack/react-router' +import { useEffect, useState } from 'react' import { EngineHost } from '~/components/student-space/EngineHost' import { EdupassLogin } from '~/components/student-space/onboarding/EdupassLogin' +import { useEngine } from '~/lib/student-space/use-engine' import { loadAuthMenu } from '~/server/auth-menu.functions' import type { AuthMenuState } from '~/server/auth-menu.handler.server' @@ -51,7 +53,7 @@ function AppLayout() { if (isOnboardingPath(location.pathname) && authMenu?.status !== 'signed-in') { return ( - + ) @@ -73,9 +75,26 @@ export function isOnboardingPath(pathname: string) { } function SignedOutOnboarding() { + const game = useEngine() + // biome-ignore lint/suspicious/noExplicitAny: engine view bag is untyped. + const camera = (game as any)?.view?.camera ?? null + const reducedMotion = useReducedMotion() return (
- +
) } + +function useReducedMotion() { + const [reduced, setReduced] = useState(false) + useEffect(() => { + if (typeof window === 'undefined' || !window.matchMedia) return + const mq = window.matchMedia('(prefers-reduced-motion: reduce)') + setReduced(mq.matches) + const handler = (e: MediaQueryListEvent) => setReduced(e.matches) + mq.addEventListener('change', handler) + return () => mq.removeEventListener('change', handler) + }, []) + return reduced +}