Files
Genarrative/src/components/game-canvas/GameCanvasSceneLayer.tsx
高物 c49c64896a
Some checks failed
CI / verify (push) Has been cancelled
初始仓库迁移
2026-04-04 23:57:06 +08:00

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>
)}
</>
);
}