127 lines
5.0 KiB
TypeScript
127 lines
5.0 KiB
TypeScript
import {AnimatePresence, motion} from 'motion/react';
|
|
|
|
import {type ScenePresetInfo, WorldType} from '../../types';
|
|
import {CHROME_ICONS, getNineSliceStyle, UI_CHROME} from '../../uiAssets';
|
|
import {PixelIcon} from '../PixelIcon';
|
|
import {
|
|
OPENING_CAMP_OVERLAY_SRC,
|
|
SCENE_TITLE_GEAR_FILTER,
|
|
} from './GameCanvasShared';
|
|
|
|
interface GameCanvasSceneLayerProps {
|
|
backgroundLoadFailed: boolean;
|
|
backgroundSrc: string;
|
|
currentScenePreset: ScenePresetInfo | null;
|
|
resolvedWorldType: WorldType | null;
|
|
showOpeningCampOverlay: boolean;
|
|
sceneTitleSpinToken: number;
|
|
onSceneNameClick?: (() => void) | null;
|
|
onBackgroundLoadError: () => void;
|
|
}
|
|
|
|
export function GameCanvasSceneLayer({
|
|
backgroundLoadFailed,
|
|
backgroundSrc,
|
|
currentScenePreset,
|
|
resolvedWorldType,
|
|
showOpeningCampOverlay,
|
|
sceneTitleSpinToken,
|
|
onSceneNameClick = null,
|
|
onBackgroundLoadError,
|
|
}: GameCanvasSceneLayerProps) {
|
|
return (
|
|
<>
|
|
{!backgroundLoadFailed ? (
|
|
<img
|
|
src={backgroundSrc}
|
|
alt={currentScenePreset?.name || 'Scene background'}
|
|
className="absolute inset-0 h-full w-full object-cover"
|
|
style={{imageRendering: 'pixelated'}}
|
|
onError={onBackgroundLoadError}
|
|
/>
|
|
) : (
|
|
<div
|
|
className="absolute inset-0"
|
|
style={{
|
|
background:
|
|
resolvedWorldType === WorldType.WUXIA
|
|
? 'linear-gradient(180deg, #d97706 0%, #451a03 100%)'
|
|
: resolvedWorldType === WorldType.XIANXIA
|
|
? 'linear-gradient(180deg, #1d4ed8 0%, #0f172a 100%)'
|
|
: 'linear-gradient(180deg, #0f766e 0%, #0b1120 100%)',
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
<div className="pointer-events-none absolute inset-0 opacity-10 mix-blend-overlay [background-image:radial-gradient(circle_at_20%_20%,rgba(255,255,255,0.14),transparent_20%),radial-gradient(circle_at_80%_30%,rgba(255,255,255,0.08),transparent_18%),radial-gradient(circle_at_50%_80%,rgba(255,255,255,0.06),transparent_22%)]" />
|
|
|
|
{showOpeningCampOverlay && (
|
|
<img
|
|
src={OPENING_CAMP_OVERLAY_SRC}
|
|
alt=""
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute bottom-[9%] left-1/2 z-[1] w-[min(92%,980px)] -translate-x-1/2 object-contain opacity-95"
|
|
style={{
|
|
imageRendering: 'pixelated',
|
|
filter: 'drop-shadow(0 12px 30px rgba(0, 0, 0, 0.42))',
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{currentScenePreset && (
|
|
<div className="absolute left-1/2 top-3 z-20 -translate-x-1/2">
|
|
<motion.div
|
|
key={`scene-title-gear-left-${sceneTitleSpinToken}`}
|
|
initial={{rotate: 0}}
|
|
animate={{rotate: sceneTitleSpinToken === 0 ? 0 : -180}}
|
|
transition={{duration: 0.92, ease: [0.22, 1, 0.36, 1]}}
|
|
className="pointer-events-none absolute left-0 top-1/2 -translate-x-[46%] -translate-y-1/2"
|
|
>
|
|
<PixelIcon
|
|
src={CHROME_ICONS.settings}
|
|
className="h-[2.35rem] w-[2.35rem] opacity-95"
|
|
style={{filter: SCENE_TITLE_GEAR_FILTER}}
|
|
/>
|
|
</motion.div>
|
|
<motion.div
|
|
key={`scene-title-gear-right-${sceneTitleSpinToken}`}
|
|
initial={{rotate: 0}}
|
|
animate={{rotate: sceneTitleSpinToken === 0 ? 0 : 180}}
|
|
transition={{duration: 0.92, ease: [0.22, 1, 0.36, 1]}}
|
|
className="pointer-events-none absolute right-0 top-1/2 translate-x-[46%] -translate-y-1/2"
|
|
>
|
|
<PixelIcon
|
|
src={CHROME_ICONS.settings}
|
|
className="h-[2.35rem] w-[2.35rem] opacity-95"
|
|
style={{filter: SCENE_TITLE_GEAR_FILTER}}
|
|
/>
|
|
</motion.div>
|
|
<button
|
|
type="button"
|
|
onClick={onSceneNameClick ?? undefined}
|
|
className="pixel-nine-slice pixel-pressable relative z-10 min-w-[168px] max-w-[min(68vw,320px)] text-center text-[11px] font-bold tracking-[0.18em] text-white"
|
|
style={getNineSliceStyle(UI_CHROME.sceneTitle, {paddingX: 16, paddingY: 4})}
|
|
>
|
|
<span className="block overflow-hidden" style={{perspective: '480px'}}>
|
|
<span className="relative block h-[1.1rem] overflow-hidden leading-[1.1rem]">
|
|
<AnimatePresence initial={false}>
|
|
<motion.span
|
|
key={currentScenePreset.name}
|
|
initial={{y: '115%', rotateX: -55, opacity: 0.15, filter: 'blur(1.4px)'}}
|
|
animate={{y: '0%', rotateX: 0, opacity: 1, filter: 'blur(0px)'}}
|
|
exit={{y: '-115%', rotateX: 55, opacity: 0.15, filter: 'blur(1.4px)'}}
|
|
transition={{duration: 0.82, ease: [0.22, 1, 0.36, 1]}}
|
|
className="absolute inset-0 flex items-center justify-center whitespace-nowrap"
|
|
>
|
|
{currentScenePreset.name}
|
|
</motion.span>
|
|
</AnimatePresence>
|
|
</span>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|