UNPKG

@react-three/drei

Version:

useful add-ons for react-three-fiber

238 lines (234 loc) 8.11 kB
import _extends from '@babel/runtime/helpers/esm/extends'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { useThree, useFrame, context as context$1 } from '@react-three/fiber'; import { easing } from 'maath'; const context = /* @__PURE__ */React.createContext(null); function useScroll() { return React.useContext(context); } function ScrollControls({ eps = 0.00001, enabled = true, infinite, horizontal, pages = 1, distance = 1, damping = 0.25, maxSpeed = Infinity, prepend = false, style = {}, children }) { const { get, setEvents, gl, size, invalidate, events } = useThree(); const [el] = React.useState(() => document.createElement('div')); const [fill] = React.useState(() => document.createElement('div')); const [fixed] = React.useState(() => document.createElement('div')); const target = gl.domElement.parentNode; const scroll = React.useRef(0); const state = React.useMemo(() => { const state = { el, eps, fill, fixed, horizontal, damping, offset: 0, delta: 0, scroll, pages, // 0-1 for a range between from -> from + distance range(from, distance, margin = 0) { const start = from - margin; const end = start + distance + margin * 2; return this.offset < start ? 0 : this.offset > end ? 1 : (this.offset - start) / (end - start); }, // 0-1-0 for a range between from -> from + distance curve(from, distance, margin = 0) { return Math.sin(this.range(from, distance, margin) * Math.PI); }, // true/false for a range between from -> from + distance visible(from, distance, margin = 0) { const start = from - margin; const end = start + distance + margin * 2; return this.offset >= start && this.offset <= end; } }; return state; }, [eps, damping, horizontal, pages]); React.useEffect(() => { el.style.position = 'absolute'; el.style.width = '100%'; el.style.height = '100%'; el.style[horizontal ? 'overflowX' : 'overflowY'] = 'auto'; el.style[horizontal ? 'overflowY' : 'overflowX'] = 'hidden'; el.style.top = '0px'; el.style.left = '0px'; for (const key in style) { el.style[key] = style[key]; } fixed.style.position = 'sticky'; fixed.style.top = '0px'; fixed.style.left = '0px'; fixed.style.width = '100%'; fixed.style.height = '100%'; fixed.style.overflow = 'hidden'; el.appendChild(fixed); fill.style.height = horizontal ? '100%' : `${pages * distance * 100}%`; fill.style.width = horizontal ? `${pages * distance * 100}%` : '100%'; fill.style.pointerEvents = 'none'; el.appendChild(fill); if (prepend) target.prepend(el);else target.appendChild(el); // Init scroll one pixel in to allow upward/leftward scroll el[horizontal ? 'scrollLeft' : 'scrollTop'] = 1; const oldTarget = events.connected || gl.domElement; requestAnimationFrame(() => events.connect == null ? void 0 : events.connect(el)); const oldCompute = get().events.compute; setEvents({ compute(event, state) { // we are using boundingClientRect because we could not rely on target.offsetTop as canvas could be positioned anywhere in dom const { left, top } = target.getBoundingClientRect(); const offsetX = event.clientX - left; const offsetY = event.clientY - top; state.pointer.set(offsetX / state.size.width * 2 - 1, -(offsetY / state.size.height) * 2 + 1); state.raycaster.setFromCamera(state.pointer, state.camera); } }); return () => { target.removeChild(el); setEvents({ compute: oldCompute }); events.connect == null || events.connect(oldTarget); }; }, [pages, distance, horizontal, el, fill, fixed, target]); React.useEffect(() => { if (events.connected === el) { const containerLength = size[horizontal ? 'width' : 'height']; const scrollLength = el[horizontal ? 'scrollWidth' : 'scrollHeight']; const scrollThreshold = scrollLength - containerLength; let current = 0; let disableScroll = true; let firstRun = true; const onScroll = () => { // Prevent first scroll because it is indirectly caused by the one pixel offset if (!enabled || firstRun) return; invalidate(); current = el[horizontal ? 'scrollLeft' : 'scrollTop']; scroll.current = current / scrollThreshold; if (infinite) { if (!disableScroll) { if (current >= scrollThreshold) { const damp = 1 - state.offset; el[horizontal ? 'scrollLeft' : 'scrollTop'] = 1; scroll.current = state.offset = -damp; disableScroll = true; } else if (current <= 0) { const damp = 1 + state.offset; el[horizontal ? 'scrollLeft' : 'scrollTop'] = scrollLength; scroll.current = state.offset = damp; disableScroll = true; } } if (disableScroll) setTimeout(() => disableScroll = false, 40); } }; el.addEventListener('scroll', onScroll, { passive: true }); requestAnimationFrame(() => firstRun = false); const onWheel = e => el.scrollLeft += e.deltaY / 2; if (horizontal) el.addEventListener('wheel', onWheel, { passive: true }); return () => { el.removeEventListener('scroll', onScroll); if (horizontal) el.removeEventListener('wheel', onWheel); }; } }, [el, events, size, infinite, state, invalidate, horizontal, enabled]); let last = 0; useFrame((_, delta) => { last = state.offset; easing.damp(state, 'offset', scroll.current, damping, delta, maxSpeed, undefined, eps); easing.damp(state, 'delta', Math.abs(last - state.offset), damping, delta, maxSpeed, undefined, eps); if (state.delta > eps) invalidate(); }); return /*#__PURE__*/React.createElement(context.Provider, { value: state }, children); } const ScrollCanvas = /* @__PURE__ */React.forwardRef(({ children }, ref) => { const group = React.useRef(null); React.useImperativeHandle(ref, () => group.current, []); const state = useScroll(); const { width, height } = useThree(state => state.viewport); useFrame(() => { group.current.position.x = state.horizontal ? -width * (state.pages - 1) * state.offset : 0; group.current.position.y = state.horizontal ? 0 : height * (state.pages - 1) * state.offset; }); return /*#__PURE__*/React.createElement("group", { ref: group }, children); }); const ScrollHtml = /*#__PURE__*/React.forwardRef(({ children, style, ...props }, ref) => { const state = useScroll(); const group = React.useRef(null); React.useImperativeHandle(ref, () => group.current, []); const { width, height } = useThree(state => state.size); const fiberState = React.useContext(context$1); const root = React.useMemo(() => ReactDOM.createRoot(state.fixed), [state.fixed]); useFrame(() => { if (state.delta > state.eps) { group.current.style.transform = `translate3d(${state.horizontal ? -width * (state.pages - 1) * state.offset : 0}px,${state.horizontal ? 0 : height * (state.pages - 1) * -state.offset}px,0)`; } }); root.render(/*#__PURE__*/React.createElement("div", _extends({ ref: group, style: { ...style, position: 'absolute', top: 0, left: 0, willChange: 'transform' } }, props), /*#__PURE__*/React.createElement(context.Provider, { value: state }, /*#__PURE__*/React.createElement(context$1.Provider, { value: fiberState }, children)))); return null; }); const Scroll = /* @__PURE__ */React.forwardRef(({ html, ...props }, ref) => { const El = html ? ScrollHtml : ScrollCanvas; return /*#__PURE__*/React.createElement(El, _extends({ ref: ref }, props)); }); export { Scroll, ScrollControls, useScroll };