|
|
import _extends from '@babel/runtime/helpers/esm/extends'; |
|
|
import * as React from 'react'; |
|
|
import * as ReactDOM from 'react-dom/client'; |
|
|
import { Vector3, DoubleSide, OrthographicCamera, PerspectiveCamera, Vector2 } from 'three'; |
|
|
import { useThree, useFrame } from '@react-three/fiber'; |
|
|
|
|
|
const v1 = new Vector3(); |
|
|
const v2 = new Vector3(); |
|
|
const v3 = new Vector3(); |
|
|
const v4 = new Vector2(); |
|
|
function defaultCalculatePosition(el, camera, size) { |
|
|
const objectPos = v1.setFromMatrixPosition(el.matrixWorld); |
|
|
objectPos.project(camera); |
|
|
const widthHalf = size.width / 2; |
|
|
const heightHalf = size.height / 2; |
|
|
return [objectPos.x * widthHalf + widthHalf, -(objectPos.y * heightHalf) + heightHalf]; |
|
|
} |
|
|
function isObjectBehindCamera(el, camera) { |
|
|
const objectPos = v1.setFromMatrixPosition(el.matrixWorld); |
|
|
const cameraPos = v2.setFromMatrixPosition(camera.matrixWorld); |
|
|
const deltaCamObj = objectPos.sub(cameraPos); |
|
|
const camDir = camera.getWorldDirection(v3); |
|
|
return deltaCamObj.angleTo(camDir) > Math.PI / 2; |
|
|
} |
|
|
function isObjectVisible(el, camera, raycaster, occlude) { |
|
|
const elPos = v1.setFromMatrixPosition(el.matrixWorld); |
|
|
const screenPos = elPos.clone(); |
|
|
screenPos.project(camera); |
|
|
v4.set(screenPos.x, screenPos.y); |
|
|
raycaster.setFromCamera(v4, camera); |
|
|
const intersects = raycaster.intersectObjects(occlude, true); |
|
|
if (intersects.length) { |
|
|
const intersectionDistance = intersects[0].distance; |
|
|
const pointDistance = elPos.distanceTo(raycaster.ray.origin); |
|
|
return pointDistance < intersectionDistance; |
|
|
} |
|
|
return true; |
|
|
} |
|
|
function objectScale(el, camera) { |
|
|
if (camera instanceof OrthographicCamera) { |
|
|
return camera.zoom; |
|
|
} else if (camera instanceof PerspectiveCamera) { |
|
|
const objectPos = v1.setFromMatrixPosition(el.matrixWorld); |
|
|
const cameraPos = v2.setFromMatrixPosition(camera.matrixWorld); |
|
|
const vFOV = camera.fov * Math.PI / 180; |
|
|
const dist = objectPos.distanceTo(cameraPos); |
|
|
const scaleFOV = 2 * Math.tan(vFOV / 2) * dist; |
|
|
return 1 / scaleFOV; |
|
|
} else { |
|
|
return 1; |
|
|
} |
|
|
} |
|
|
function objectZIndex(el, camera, zIndexRange) { |
|
|
if (camera instanceof PerspectiveCamera || camera instanceof OrthographicCamera) { |
|
|
const objectPos = v1.setFromMatrixPosition(el.matrixWorld); |
|
|
const cameraPos = v2.setFromMatrixPosition(camera.matrixWorld); |
|
|
const dist = objectPos.distanceTo(cameraPos); |
|
|
const A = (zIndexRange[1] - zIndexRange[0]) / (camera.far - camera.near); |
|
|
const B = zIndexRange[1] - A * camera.far; |
|
|
return Math.round(A * dist + B); |
|
|
} |
|
|
return undefined; |
|
|
} |
|
|
const epsilon = value => Math.abs(value) < 1e-10 ? 0 : value; |
|
|
function getCSSMatrix(matrix, multipliers, prepend = '') { |
|
|
let matrix3d = 'matrix3d('; |
|
|
for (let i = 0; i !== 16; i++) { |
|
|
matrix3d += epsilon(multipliers[i] * matrix.elements[i]) + (i !== 15 ? ',' : ')'); |
|
|
} |
|
|
return prepend + matrix3d; |
|
|
} |
|
|
const getCameraCSSMatrix = (multipliers => { |
|
|
return matrix => getCSSMatrix(matrix, multipliers); |
|
|
})([1, -1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1]); |
|
|
const getObjectCSSMatrix = (scaleMultipliers => { |
|
|
return (matrix, factor) => getCSSMatrix(matrix, scaleMultipliers(factor), 'translate(-50%,-50%)'); |
|
|
})(f => [1 / f, 1 / f, 1 / f, 1, -1 / f, -1 / f, -1 / f, -1, 1 / f, 1 / f, 1 / f, 1, 1, 1, 1, 1]); |
|
|
function isRefObject(ref) { |
|
|
return ref && typeof ref === 'object' && 'current' in ref; |
|
|
} |
|
|
const Html = React.forwardRef(({ |
|
|
children, |
|
|
eps = 0.001, |
|
|
style, |
|
|
className, |
|
|
prepend, |
|
|
center, |
|
|
fullscreen, |
|
|
portal, |
|
|
distanceFactor, |
|
|
sprite = false, |
|
|
transform = false, |
|
|
occlude, |
|
|
onOcclude, |
|
|
castShadow, |
|
|
receiveShadow, |
|
|
material, |
|
|
geometry, |
|
|
zIndexRange = [16777271, 0], |
|
|
calculatePosition = defaultCalculatePosition, |
|
|
as = 'div', |
|
|
wrapperClass, |
|
|
pointerEvents = 'auto', |
|
|
...props |
|
|
}, ref) => { |
|
|
const { |
|
|
gl, |
|
|
camera, |
|
|
scene, |
|
|
size, |
|
|
raycaster, |
|
|
events, |
|
|
viewport |
|
|
} = useThree(); |
|
|
const [el] = React.useState(() => document.createElement(as)); |
|
|
const root = React.useRef(null); |
|
|
const group = React.useRef(null); |
|
|
const oldZoom = React.useRef(0); |
|
|
const oldPosition = React.useRef([0, 0]); |
|
|
const transformOuterRef = React.useRef(null); |
|
|
const transformInnerRef = React.useRef(null); |
|
|
|
|
|
const target = (portal == null ? void 0 : portal.current) || events.connected || gl.domElement.parentNode; |
|
|
const occlusionMeshRef = React.useRef(null); |
|
|
const isMeshSizeSet = React.useRef(false); |
|
|
const isRayCastOcclusion = React.useMemo(() => { |
|
|
return occlude && occlude !== 'blending' || Array.isArray(occlude) && occlude.length && isRefObject(occlude[0]); |
|
|
}, [occlude]); |
|
|
React.useLayoutEffect(() => { |
|
|
const el = gl.domElement; |
|
|
if (occlude && occlude === 'blending') { |
|
|
el.style.zIndex = `${Math.floor(zIndexRange[0] / 2)}`; |
|
|
el.style.position = 'absolute'; |
|
|
el.style.pointerEvents = 'none'; |
|
|
} else { |
|
|
el.style.zIndex = null; |
|
|
el.style.position = null; |
|
|
el.style.pointerEvents = null; |
|
|
} |
|
|
}, [occlude]); |
|
|
React.useLayoutEffect(() => { |
|
|
if (group.current) { |
|
|
const currentRoot = root.current = ReactDOM.createRoot(el); |
|
|
scene.updateMatrixWorld(); |
|
|
if (transform) { |
|
|
el.style.cssText = `position:absolute;top:0;left:0;pointer-events:none;overflow:hidden;`; |
|
|
} else { |
|
|
const vec = calculatePosition(group.current, camera, size); |
|
|
el.style.cssText = `position:absolute;top:0;left:0;transform:translate3d(${vec[0]}px,${vec[1]}px,0);transform-origin:0 0;`; |
|
|
} |
|
|
if (target) { |
|
|
if (prepend) target.prepend(el);else target.appendChild(el); |
|
|
} |
|
|
return () => { |
|
|
if (target) target.removeChild(el); |
|
|
currentRoot.unmount(); |
|
|
}; |
|
|
} |
|
|
}, [target, transform]); |
|
|
React.useLayoutEffect(() => { |
|
|
if (wrapperClass) el.className = wrapperClass; |
|
|
}, [wrapperClass]); |
|
|
const styles = React.useMemo(() => { |
|
|
if (transform) { |
|
|
return { |
|
|
position: 'absolute', |
|
|
top: 0, |
|
|
left: 0, |
|
|
width: size.width, |
|
|
height: size.height, |
|
|
transformStyle: 'preserve-3d', |
|
|
pointerEvents: 'none' |
|
|
}; |
|
|
} else { |
|
|
return { |
|
|
position: 'absolute', |
|
|
transform: center ? 'translate3d(-50%,-50%,0)' : 'none', |
|
|
...(fullscreen && { |
|
|
top: -size.height / 2, |
|
|
left: -size.width / 2, |
|
|
width: size.width, |
|
|
height: size.height |
|
|
}), |
|
|
...style |
|
|
}; |
|
|
} |
|
|
}, [style, center, fullscreen, size, transform]); |
|
|
const transformInnerStyles = React.useMemo(() => ({ |
|
|
position: 'absolute', |
|
|
pointerEvents |
|
|
}), [pointerEvents]); |
|
|
React.useLayoutEffect(() => { |
|
|
isMeshSizeSet.current = false; |
|
|
if (transform) { |
|
|
var _root$current; |
|
|
(_root$current = root.current) == null || _root$current.render(React.createElement("div", { |
|
|
ref: transformOuterRef, |
|
|
style: styles |
|
|
}, React.createElement("div", { |
|
|
ref: transformInnerRef, |
|
|
style: transformInnerStyles |
|
|
}, React.createElement("div", { |
|
|
ref: ref, |
|
|
className: className, |
|
|
style: style, |
|
|
children: children |
|
|
})))); |
|
|
} else { |
|
|
var _root$current2; |
|
|
(_root$current2 = root.current) == null || _root$current2.render(React.createElement("div", { |
|
|
ref: ref, |
|
|
style: styles, |
|
|
className: className, |
|
|
children: children |
|
|
})); |
|
|
} |
|
|
}); |
|
|
const visible = React.useRef(true); |
|
|
useFrame(gl => { |
|
|
if (group.current) { |
|
|
camera.updateMatrixWorld(); |
|
|
group.current.updateWorldMatrix(true, false); |
|
|
const vec = transform ? oldPosition.current : calculatePosition(group.current, camera, size); |
|
|
if (transform || Math.abs(oldZoom.current - camera.zoom) > eps || Math.abs(oldPosition.current[0] - vec[0]) > eps || Math.abs(oldPosition.current[1] - vec[1]) > eps) { |
|
|
const isBehindCamera = isObjectBehindCamera(group.current, camera); |
|
|
let raytraceTarget = false; |
|
|
if (isRayCastOcclusion) { |
|
|
if (Array.isArray(occlude)) { |
|
|
raytraceTarget = occlude.map(item => item.current); |
|
|
} else if (occlude !== 'blending') { |
|
|
raytraceTarget = [scene]; |
|
|
} |
|
|
} |
|
|
const previouslyVisible = visible.current; |
|
|
if (raytraceTarget) { |
|
|
const isvisible = isObjectVisible(group.current, camera, raycaster, raytraceTarget); |
|
|
visible.current = isvisible && !isBehindCamera; |
|
|
} else { |
|
|
visible.current = !isBehindCamera; |
|
|
} |
|
|
if (previouslyVisible !== visible.current) { |
|
|
if (onOcclude) onOcclude(!visible.current);else el.style.display = visible.current ? 'block' : 'none'; |
|
|
} |
|
|
const halfRange = Math.floor(zIndexRange[0] / 2); |
|
|
const zRange = occlude ? isRayCastOcclusion |
|
|
? [zIndexRange[0], halfRange] : [halfRange - 1, 0] : zIndexRange; |
|
|
el.style.zIndex = `${objectZIndex(group.current, camera, zRange)}`; |
|
|
if (transform) { |
|
|
const [widthHalf, heightHalf] = [size.width / 2, size.height / 2]; |
|
|
const fov = camera.projectionMatrix.elements[5] * heightHalf; |
|
|
const { |
|
|
isOrthographicCamera, |
|
|
top, |
|
|
left, |
|
|
bottom, |
|
|
right |
|
|
} = camera; |
|
|
const cameraMatrix = getCameraCSSMatrix(camera.matrixWorldInverse); |
|
|
const cameraTransform = isOrthographicCamera ? `scale(${fov})translate(${epsilon(-(right + left) / 2)}px,${epsilon((top + bottom) / 2)}px)` : `translateZ(${fov}px)`; |
|
|
let matrix = group.current.matrixWorld; |
|
|
if (sprite) { |
|
|
matrix = camera.matrixWorldInverse.clone().transpose().copyPosition(matrix).scale(group.current.scale); |
|
|
matrix.elements[3] = matrix.elements[7] = matrix.elements[11] = 0; |
|
|
matrix.elements[15] = 1; |
|
|
} |
|
|
el.style.width = size.width + 'px'; |
|
|
el.style.height = size.height + 'px'; |
|
|
el.style.perspective = isOrthographicCamera ? '' : `${fov}px`; |
|
|
if (transformOuterRef.current && transformInnerRef.current) { |
|
|
transformOuterRef.current.style.transform = `${cameraTransform}${cameraMatrix}translate(${widthHalf}px,${heightHalf}px)`; |
|
|
transformInnerRef.current.style.transform = getObjectCSSMatrix(matrix, 1 / ((distanceFactor || 10) / 400)); |
|
|
} |
|
|
} else { |
|
|
const scale = distanceFactor === undefined ? 1 : objectScale(group.current, camera) * distanceFactor; |
|
|
el.style.transform = `translate3d(${vec[0]}px,${vec[1]}px,0) scale(${scale})`; |
|
|
} |
|
|
oldPosition.current = vec; |
|
|
oldZoom.current = camera.zoom; |
|
|
} |
|
|
} |
|
|
if (!isRayCastOcclusion && occlusionMeshRef.current && !isMeshSizeSet.current) { |
|
|
if (transform) { |
|
|
if (transformOuterRef.current) { |
|
|
const el = transformOuterRef.current.children[0]; |
|
|
if (el != null && el.clientWidth && el != null && el.clientHeight) { |
|
|
const { |
|
|
isOrthographicCamera |
|
|
} = camera; |
|
|
if (isOrthographicCamera || geometry) { |
|
|
if (props.scale) { |
|
|
if (!Array.isArray(props.scale)) { |
|
|
occlusionMeshRef.current.scale.setScalar(1 / props.scale); |
|
|
} else if (props.scale instanceof Vector3) { |
|
|
occlusionMeshRef.current.scale.copy(props.scale.clone().divideScalar(1)); |
|
|
} else { |
|
|
occlusionMeshRef.current.scale.set(1 / props.scale[0], 1 / props.scale[1], 1 / props.scale[2]); |
|
|
} |
|
|
} |
|
|
} else { |
|
|
const ratio = (distanceFactor || 10) / 400; |
|
|
const w = el.clientWidth * ratio; |
|
|
const h = el.clientHeight * ratio; |
|
|
occlusionMeshRef.current.scale.set(w, h, 1); |
|
|
} |
|
|
isMeshSizeSet.current = true; |
|
|
} |
|
|
} |
|
|
} else { |
|
|
const ele = el.children[0]; |
|
|
if (ele != null && ele.clientWidth && ele != null && ele.clientHeight) { |
|
|
const ratio = 1 / viewport.factor; |
|
|
const w = ele.clientWidth * ratio; |
|
|
const h = ele.clientHeight * ratio; |
|
|
occlusionMeshRef.current.scale.set(w, h, 1); |
|
|
isMeshSizeSet.current = true; |
|
|
} |
|
|
occlusionMeshRef.current.lookAt(gl.camera.position); |
|
|
} |
|
|
} |
|
|
}); |
|
|
const shaders = React.useMemo(() => ({ |
|
|
vertexShader: !transform ? ` |
|
|
/* |
|
|
This shader is from the THREE's SpriteMaterial. |
|
|
We need to turn the backing plane into a Sprite |
|
|
(make it always face the camera) if "transfrom" |
|
|
is false. |
|
|
*/ |
|
|
#include <common> |
|
|
|
|
|
void main() { |
|
|
vec2 center = vec2(0., 1.); |
|
|
float rotation = 0.0; |
|
|
|
|
|
// This is somewhat arbitrary, but it seems to work well |
|
|
// Need to figure out how to derive this dynamically if it even matters |
|
|
float size = 0.03; |
|
|
|
|
|
vec4 mvPosition = modelViewMatrix * vec4( 0.0, 0.0, 0.0, 1.0 ); |
|
|
vec2 scale; |
|
|
scale.x = length( vec3( modelMatrix[ 0 ].x, modelMatrix[ 0 ].y, modelMatrix[ 0 ].z ) ); |
|
|
scale.y = length( vec3( modelMatrix[ 1 ].x, modelMatrix[ 1 ].y, modelMatrix[ 1 ].z ) ); |
|
|
|
|
|
bool isPerspective = isPerspectiveMatrix( projectionMatrix ); |
|
|
if ( isPerspective ) scale *= - mvPosition.z; |
|
|
|
|
|
vec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale * size; |
|
|
vec2 rotatedPosition; |
|
|
rotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y; |
|
|
rotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y; |
|
|
mvPosition.xy += rotatedPosition; |
|
|
|
|
|
gl_Position = projectionMatrix * mvPosition; |
|
|
} |
|
|
` : undefined, |
|
|
fragmentShader: ` |
|
|
void main() { |
|
|
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); |
|
|
} |
|
|
` |
|
|
}), [transform]); |
|
|
return React.createElement("group", _extends({}, props, { |
|
|
ref: group |
|
|
}), occlude && !isRayCastOcclusion && React.createElement("mesh", { |
|
|
castShadow: castShadow, |
|
|
receiveShadow: receiveShadow, |
|
|
ref: occlusionMeshRef |
|
|
}, geometry || React.createElement("planeGeometry", null), material || React.createElement("shaderMaterial", { |
|
|
side: DoubleSide, |
|
|
vertexShader: shaders.vertexShader, |
|
|
fragmentShader: shaders.fragmentShader |
|
|
}))); |
|
|
}); |
|
|
|
|
|
export { Html }; |
|
|
|