feat(web): clamp tabletop camera to the horizon

Add a maxPolarAngle prop to Scene and use it in the tabletop view so the
camera can't dip below the table and peek under face-down cards.
This commit is contained in:
2026-08-10 12:24:43 +08:00
parent 9d776771b1
commit 812b4640e2
2 changed files with 10 additions and 1 deletions
@@ -56,6 +56,9 @@ export default function TabletopScene({
enablePan enablePan
fullscreen fullscreen
shadowScale={shadowScale} shadowScale={shadowScale}
// Keep the camera above the table so face-down cards can't be peeked
// from below. π/2 clamps the polar angle at the horizon.
maxPolarAngle={Math.PI / 2}
overlay={ overlay={
<button <button
onClick={() => setShowSurface((v) => !v)} onClick={() => setShowSurface((v) => !v)}
+7 -1
View File
@@ -16,7 +16,9 @@ import { EffectComposer, Vignette } from '@react-three/postprocessing';
* *
* `autoRotate` and `enablePan` tune the orbit controls (a tabletop view, for * `autoRotate` and `enablePan` tune the orbit controls (a tabletop view, for
* example, pans instead of rotating). `fullscreen` adds a toggle button that * example, pans instead of rotating). `fullscreen` adds a toggle button that
* expands the scene to the full screen. * expands the scene to the full screen. `maxPolarAngle` (radians) clamps how
* far the camera can tilt below the horizon, e.g. to stop a tabletop view from
* peeking under face-down cards.
*/ */
export default function Scene({ export default function Scene({
children, children,
@@ -25,6 +27,7 @@ export default function Scene({
fullscreen = false, fullscreen = false,
overlay, overlay,
shadowScale = 22, shadowScale = 22,
maxPolarAngle = Math.PI,
}: { }: {
children: ReactNode; children: ReactNode;
autoRotate?: boolean; autoRotate?: boolean;
@@ -34,6 +37,8 @@ export default function Scene({
overlay?: ReactNode; overlay?: ReactNode;
/** Contact shadow plane size in world units; defaults to a generous 22. */ /** Contact shadow plane size in world units; defaults to a generous 22. */
shadowScale?: number; shadowScale?: number;
/** Max camera polar angle in radians; defaults to unrestricted (π). */
maxPolarAngle?: number;
}) { }) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
@@ -93,6 +98,7 @@ export default function Scene({
enablePan={enablePan} enablePan={enablePan}
minDistance={0.01} minDistance={0.01}
maxDistance={8} maxDistance={8}
maxPolarAngle={maxPolarAngle}
autoRotate={autoRotate} autoRotate={autoRotate}
makeDefault makeDefault
/> />