Files
healer-man/src/ui/CharacterPreview3D.tsx
T
2026-08-30 14:16:56 -04:00

82 lines
2.7 KiB
TypeScript

import { Bounds, useBounds } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { useCallback } from "react";
import type { CharacterDraft, CharacterProfile } from "../app/types";
import { CharacterModel } from "../avatar/CharacterModel";
import { ProceduralAvatar } from "../avatar/ProceduralAvatar";
import { classById, raceById } from "../app/characterCatalog";
import {
numericFrameRateLimit,
type PreviewFrameRateLimit,
} from "../game/frameRateSettings";
import { useDocumentVisibility } from "../platform/useDocumentVisibility";
import { CappedFrameLoop } from "../scene/CappedFrameLoop";
function FittedCharacter({
characterClassColor,
source,
turnDegrees,
}: {
characterClassColor: string;
source: CharacterProfile | CharacterDraft;
turnDegrees: number;
}) {
const bounds = useBounds();
const refit = useCallback(() => {
bounds.refresh().clip().fit();
}, [bounds]);
return (
<CharacterModel
identity={source}
onReady={refit}
preview
turnDegrees={turnDegrees}
fallback={<ProceduralAvatar accent={characterClassColor} />}
/>
);
}
export default function CharacterPreview3D({ frameRateLimit, source }: {
frameRateLimit: PreviewFrameRateLimit;
source: CharacterProfile | CharacterDraft;
}) {
const race = raceById(source.raceId);
const characterClass = classById(source.classId);
const turnDegrees = "rotation" in source ? source.rotation : -8;
const documentVisible = useDocumentVisibility();
const targetFps = numericFrameRateLimit(frameRateLimit);
const frameloop = !documentVisible
? "demand"
: targetFps === null ? "always" : "never";
return (
<div
className="character-preview__viewport"
aria-hidden="true"
data-frame-rate-limit={frameRateLimit}
data-frame-loop={frameloop}
>
<Canvas
dpr={[1, 1.25]}
frameloop={frameloop}
camera={{ fov: 32, near: 0.05, far: 40, position: [3.2, 1.8, 4.6] }}
gl={{ alpha: true, antialias: true, powerPreference: "default" }}
>
{targetFps !== null && frameloop === "never" && (
<CappedFrameLoop active={documentVisible} targetFps={targetFps} />
)}
<ambientLight intensity={1.15} color="#d8e2ca" />
<hemisphereLight args={["#d8e9ce", "#142019", 1.8]} />
<directionalLight position={[3, 5, 4]} intensity={2.1} color="#f0e5bf" />
<directionalLight position={[-4, 2, -3]} intensity={1.1} color={race.accent} />
<Bounds fit clip observe margin={1.2}>
<FittedCharacter
characterClassColor={characterClass.color}
source={source}
turnDegrees={turnDegrees}
/>
</Bounds>
</Canvas>
</div>
);
}