@react-three/drei
Version:
useful add-ons for react-three-fiber
260 lines (257 loc) • 7.91 kB
JavaScript
import _extends from '@babel/runtime/helpers/esm/extends';
import * as React from 'react';
import * as THREE from 'three';
import { context, useThree, createPortal, useFrame } from '@react-three/fiber';
import tunnel from 'tunnel-rat';
const isOrthographicCamera = def => def && def.isOrthographicCamera;
const col = /* @__PURE__ */new THREE.Color();
const tracked = /* @__PURE__ */tunnel();
function computeContainerPosition(canvasSize, trackRect) {
const {
right,
top,
left: trackLeft,
bottom: trackBottom,
width,
height
} = trackRect;
const isOffscreen = trackRect.bottom < 0 || top > canvasSize.height || right < 0 || trackRect.left > canvasSize.width;
const canvasBottom = canvasSize.top + canvasSize.height;
const bottom = canvasBottom - trackBottom;
const left = trackLeft - canvasSize.left;
return {
position: {
width,
height,
left,
top,
bottom,
right
},
isOffscreen
};
}
function prepareSkissor(state, {
left,
bottom,
width,
height
}) {
let autoClear;
const aspect = width / height;
if (isOrthographicCamera(state.camera)) {
if (!state.camera.manual) {
if (state.camera.left !== width / -2 || state.camera.right !== width / 2 || state.camera.top !== height / 2 || state.camera.bottom !== height / -2) {
Object.assign(state.camera, {
left: width / -2,
right: width / 2,
top: height / 2,
bottom: height / -2
});
state.camera.updateProjectionMatrix();
}
} else {
state.camera.updateProjectionMatrix();
}
} else if (state.camera.aspect !== aspect) {
state.camera.aspect = aspect;
state.camera.updateProjectionMatrix();
}
autoClear = state.gl.autoClear;
state.gl.autoClear = false;
state.gl.setViewport(left, bottom, width, height);
state.gl.setScissor(left, bottom, width, height);
state.gl.setScissorTest(true);
return autoClear;
}
function finishSkissor(state, autoClear) {
// Restore the default state
state.gl.setScissorTest(false);
state.gl.autoClear = autoClear;
}
function clear(state) {
state.gl.getClearColor(col);
state.gl.setClearColor(col, state.gl.getClearAlpha());
state.gl.clear(true, true);
}
function Container({
visible = true,
canvasSize,
scene,
index,
children,
frames,
rect,
track
}) {
const rootState = useThree();
const [isOffscreen, setOffscreen] = React.useState(false);
let frameCount = 0;
useFrame(state => {
if (frames === Infinity || frameCount <= frames) {
var _track$current;
if (track) rect.current = (_track$current = track.current) == null ? void 0 : _track$current.getBoundingClientRect();
frameCount++;
}
if (rect.current) {
const {
position,
isOffscreen: _isOffscreen
} = computeContainerPosition(canvasSize, rect.current);
if (isOffscreen !== _isOffscreen) setOffscreen(_isOffscreen);
if (visible && !isOffscreen && rect.current) {
const autoClear = prepareSkissor(state, position);
// When children are present render the portalled scene, otherwise the default scene
state.gl.render(children ? state.scene : scene, state.camera);
finishSkissor(state, autoClear);
}
}
}, index);
React.useLayoutEffect(() => {
const curRect = rect.current;
if (curRect && (!visible || !isOffscreen)) {
// If the view is not visible clear it once, but stop rendering afterwards!
const {
position
} = computeContainerPosition(canvasSize, curRect);
const autoClear = prepareSkissor(rootState, position);
clear(rootState);
finishSkissor(rootState, autoClear);
}
}, [visible, isOffscreen]);
React.useEffect(() => {
if (!track) return;
const curRect = rect.current;
// Connect the event layer to the tracking element
const old = rootState.get().events.connected;
rootState.setEvents({
connected: track.current
});
return () => {
if (curRect) {
const {
position
} = computeContainerPosition(canvasSize, curRect);
const autoClear = prepareSkissor(rootState, position);
clear(rootState);
finishSkissor(rootState, autoClear);
}
rootState.setEvents({
connected: old
});
};
}, [track]);
return /*#__PURE__*/React.createElement(React.Fragment, null, children, /*#__PURE__*/React.createElement("group", {
onPointerOver: () => null
}));
}
const CanvasView = /* @__PURE__ */React.forwardRef(({
track,
visible = true,
index = 1,
id,
style,
className,
frames = Infinity,
children,
...props
}, fref) => {
var _rect$current, _rect$current2, _rect$current3, _rect$current4;
const rect = React.useRef(null);
const {
size,
scene
} = useThree();
const [virtualScene] = React.useState(() => new THREE.Scene());
const [ready, toggle] = React.useReducer(() => true, false);
const compute = React.useCallback((event, state) => {
if (rect.current && track && track.current && event.target === track.current) {
const {
width,
height,
left,
top
} = rect.current;
const x = event.clientX - left;
const y = event.clientY - top;
state.pointer.set(x / width * 2 - 1, -(y / height) * 2 + 1);
state.raycaster.setFromCamera(state.pointer, state.camera);
}
}, [rect, track]);
React.useEffect(() => {
var _track$current2;
// We need the tracking elements bounds beforehand in order to inject it into the portal
if (track) rect.current = (_track$current2 = track.current) == null ? void 0 : _track$current2.getBoundingClientRect();
// And now we can proceed
toggle();
}, [track]);
return /*#__PURE__*/React.createElement("group", _extends({
ref: fref
}, props), ready && createPortal(/*#__PURE__*/React.createElement(Container, {
visible: visible,
canvasSize: size,
frames: frames,
scene: scene,
track: track,
rect: rect,
index: index
}, children), virtualScene, {
events: {
compute,
priority: index
},
size: {
width: (_rect$current = rect.current) == null ? void 0 : _rect$current.width,
height: (_rect$current2 = rect.current) == null ? void 0 : _rect$current2.height,
// @ts-ignore
top: (_rect$current3 = rect.current) == null ? void 0 : _rect$current3.top,
// @ts-ignore
left: (_rect$current4 = rect.current) == null ? void 0 : _rect$current4.left
}
}));
});
const HtmlView = /* @__PURE__ */React.forwardRef(({
as: El = 'div',
id,
visible,
className,
style,
index = 1,
track,
frames = Infinity,
children,
...props
}, fref) => {
const uuid = React.useId();
const ref = React.useRef(null);
React.useImperativeHandle(fref, () => ref.current);
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(El, _extends({
ref: ref,
id: id,
className: className,
style: style
}, props)), /*#__PURE__*/React.createElement(tracked.In, null, /*#__PURE__*/React.createElement(CanvasView, {
visible: visible,
key: uuid,
track: ref,
frames: frames,
index: index
}, children)));
});
const View = /* @__PURE__ */(() => {
const _View = /*#__PURE__*/React.forwardRef((props, fref) => {
// If we're inside a canvas we should be able to access the context store
const store = React.useContext(context);
// If that's not the case we render a tunnel
if (!store) return /*#__PURE__*/React.createElement(HtmlView, _extends({
ref: fref
}, props));
// Otherwise a plain canvas-view
else return /*#__PURE__*/React.createElement(CanvasView, _extends({
ref: fref
}, props));
});
_View.Port = () => /*#__PURE__*/React.createElement(tracked.Out, null);
return _View;
})();
export { View };