90 lines
No EOL
2.9 KiB
TypeScript
90 lines
No EOL
2.9 KiB
TypeScript
import { useThree } from "@react-three/fiber";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import ReactDOM from "react-dom";
|
|
|
|
interface Props {
|
|
showResetButton?: boolean;
|
|
onReset?: () => void;
|
|
}
|
|
|
|
/**
|
|
* Renders camera control UI buttons as a DOM overlay on top of the R3F canvas.
|
|
* Must be placed inside a <Canvas> so it has access to useThree().
|
|
*
|
|
* Uses a portal into a div that is absolutely positioned over the canvas element,
|
|
* so the buttons sit in 2D screen space rather than 3D world space.
|
|
*/
|
|
export default function CameraOverlay({ showResetButton, onReset }: Props) {
|
|
const { gl } = useThree();
|
|
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
const canvas = gl.domElement;
|
|
const parent = canvas.parentElement;
|
|
if (!parent) return;
|
|
|
|
// Ensure the parent is positioned so absolute children are relative to it
|
|
const parentPosition = getComputedStyle(parent).position;
|
|
if (parentPosition === "static") {
|
|
parent.style.position = "relative";
|
|
}
|
|
|
|
const div = document.createElement("div");
|
|
div.style.cssText = `
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
pointer-events: none;
|
|
z-index: 10;
|
|
`;
|
|
parent.appendChild(div);
|
|
setContainer(div);
|
|
|
|
return () => {
|
|
parent.removeChild(div);
|
|
};
|
|
}, [gl]);
|
|
|
|
if (!container) return null;
|
|
|
|
return ReactDOM.createPortal(
|
|
<div style={{
|
|
position: "absolute",
|
|
bottom: 12,
|
|
right: 12,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 8,
|
|
pointerEvents: "auto",
|
|
}}>
|
|
{showResetButton && (
|
|
<button
|
|
onClick={onReset}
|
|
title="Reset camera"
|
|
style={{
|
|
width: 36,
|
|
height: 36,
|
|
borderRadius: 6,
|
|
border: "1px solid rgba(255,255,255,0.2)",
|
|
background: "rgba(0,0,0,0.45)",
|
|
color: "white",
|
|
cursor: "pointer",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
fontSize: 16,
|
|
backdropFilter: "blur(4px)",
|
|
transition: "background 0.15s",
|
|
}}
|
|
onMouseEnter={e => (e.currentTarget.style.background = "rgba(0,0,0,0.7)")}
|
|
onMouseLeave={e => (e.currentTarget.style.background = "rgba(0,0,0,0.45)")}
|
|
>
|
|
⟳
|
|
</button>
|
|
)}
|
|
</div>,
|
|
container
|
|
);
|
|
} |