import { isDefined } from '@h5web/shared' ; import type { GroupProps } from '@react-three/fiber' ; import { useFrame , useThree } from '@react-three/fiber' ; import { useState , useLayoutEffect , useRef , useCallback , useEffect , } from 'react' ; import type { HTMLAttributes } from 'react' ; import ReactDOM from 'react-dom' ; import type { Group } from 'three' ; import { Vector3 } from 'three' ; import { projectCameraToHtml } from '../utils' ; interface Props extends HTMLAttributes < HTMLDivElement > { groupProps ? : GroupProps ; followCamera ? : boolean ; scaleOnZoom ? : boolean ; } function Html ( props ) { const { className , style , children , followCamera , scaleOnZoom , groupProps = { } , ... divProps } = props ; const { width , height } = useThree ( ( state ) => state . size ) ; const camera = useThree ( ( state ) => state . camera ) ; const gl = useThree ( ( state ) => state . gl ) ; const { parentElement } = gl . domElement ; const [ el ] = useState ( ( ) => { const elem = document . createElement ( 'div' ) ; elem . style . cssText = `position: absolute; top: 0; left: 0; pointer-events: none;` ; return elem ; } ) ; const innerRef = useRef < HTMLDivElement > ( null ) ; const group = useRef < Group > ( null ! ) ; const getGroupPosition = useCallback ( ( ) => { const worldPos = new Vector3 ( ) . setFromMatrixPosition ( group . current . matrixWorld ) ; const cameraPos = worldPos . clone ( ) . project ( camera ) ; const htmlPos = projectCameraToHtml ( cameraPos , width , height ) ; return [ htmlPos . x , htmlPos . y ] ; } , [ camera , height , width ] ) ; const getInnerDivTransform = useCallback ( ( ) => { const position = followCamera ? getGroupPosition ( ) : undefined ; const scale = new Vector3 ( ) . setFromMatrixScale ( camera . projectionMatrix ) . multiply ( new Vector3 ( ) . set ( width / 2 , height / 2 , 0 ) ) ; return [ position && `translate3d(${ position [ 0 ] }px, ${ position [ 1 ] }px, 0)` , scaleOnZoom ? `scale(${ scale . x }, ${ scale . y })` : undefined , ] . filter ( isDefined ) . join ( ' ' ) ; } , [ camera , followCamera , getGroupPosition , scaleOnZoom , width , height ] ) ; useLayoutEffect ( ( ) => { if ( parentElement ) { parentElement . append ( el ) ; } return ( ) => { if ( parentElement ) { el . remove ( ) ; } ReactDOM . unmountComponentAtNode ( el ) ; } ; } , [ el , parentElement ] ) ; useLayoutEffect ( ( ) => { el . style . width = `${ width }px` ; el . style . height = `${ height }px` ; el . style . overflow = followCamera ? 'hidden' : 'visible' ; } , [ el , width , height , followCamera ] ) ; useEffect ( ( ) => { group . current . updateMatrixWorld ( ) ; ReactDOM . render ( < div ref = { innerRef } className = { className } style = { { position : 'absolute' , top : 0 , left : 0 , transformOrigin : 'left top' , transform : getInnerDivTransform ( ) , ... style , } } { ... divProps } > { children } < /div>, el); } , [ children , className , divProps , el , getInnerDivTransform , style ] ) ; useFrame ( ( ) => { if ( innerRef . current ) { innerRef . current . style . transform = getInnerDivTransform ( ) ; } } ) ; return < group ref = { group } { ... groupProps } />; } export default Html ;