|
|
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 = 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, |
|
|
|
|
|
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); |
|
|
}, |
|
|
|
|
|
curve(from, distance, margin = 0) { |
|
|
return Math.sin(this.range(from, distance, margin) * Math.PI); |
|
|
}, |
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
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 = () => { |
|
|
|
|
|
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 React.createElement(context.Provider, { |
|
|
value: state |
|
|
}, children); |
|
|
} |
|
|
const ScrollCanvas = 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 React.createElement("group", { |
|
|
ref: group |
|
|
}, children); |
|
|
}); |
|
|
const ScrollHtml = 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(React.createElement("div", _extends({ |
|
|
ref: group, |
|
|
style: { |
|
|
...style, |
|
|
position: 'absolute', |
|
|
top: 0, |
|
|
left: 0, |
|
|
willChange: 'transform' |
|
|
} |
|
|
}, props), React.createElement(context.Provider, { |
|
|
value: state |
|
|
}, React.createElement(context$1.Provider, { |
|
|
value: fiberState |
|
|
}, children)))); |
|
|
return null; |
|
|
}); |
|
|
const Scroll = React.forwardRef(({ |
|
|
html, |
|
|
...props |
|
|
}, ref) => { |
|
|
const El = html ? ScrollHtml : ScrollCanvas; |
|
|
return React.createElement(El, _extends({ |
|
|
ref: ref |
|
|
}, props)); |
|
|
}); |
|
|
|
|
|
export { Scroll, ScrollControls, useScroll }; |
|
|
|