pmndrs / drei

🥉 useful helpers for react-three-fiber
https://docs.pmnd.rs/drei
MIT License
8.35k stars 697 forks source link

Add initial scroll state to ScrollControls component #1246

Open milotoor opened 1 year ago

milotoor commented 1 year ago

Describe the feature you'd like:

It would be nice if the <ScrollControls> component accepted some initial scrolling offset value (perhaps a pixel value or a percentage) such that when the scene initializes the user can scroll in either direction towards opposite extremes of the control flow.

For example, suppose we are constructing a scene in which a character may walk forward or backward. We would like the character to begin in the middle of the walkable space, such that they may walk forward or backward from the beginning. This could be achieved by setting passing a parameter, say initialScroll, to 50%.

Suggested implementation:

I was able to cobble together a (possible?) solution to this without editing the ScrollControls component, though it has its downsides. In a child component of <ScrollControls> I have the following hook:

const scrollState = useScroll();
const { size } = useThree();
const initialScrollPercent = 0.5;

useEffect(() => {
  scrollState.offset = scrollState.scroll.current = initialScrollPercent;

  const containerLength = size.height;
  const scrollLength = scrollState.el.scrollHeight;
  const scrollThreshold = scrollLength - containerLength;
  scrollState.el.scrollTop = initialScrollPercent * scrollThreshold;
}, [scrollState.el.style.position]);

The first line of the hook updates the offset state variable and the scroll ref (incidentally, the latter operation is not TypeScript-friendly, as the scroll ref is not present in the ScrollControlsState type--perhaps an oversight?). The last four lines are to update the div's scrollTop property; this is the opposite of line 170 which updates the ref on scroll. Finally, the hook runs when the div's position style changes so that the effect is run after the div receives its absolute-positioning (it has no height at all before then).

This is functional for my use case (i.e. vertical scrolling with my project's particular version of R3D--9.54.0) but it's very kludgy, relies on internal implementation details and accesses an object unavailable to TypeScript users (the scrollState.scroll ref). For all these reasons I think it would be preferable if this were supported by the library. I'll try to implement the feature and will share how it goes.

milotoor commented 1 year ago

See my basic implementation. These changes were sufficient for my use case (where the initial scroll value is a percentage); some additional work would need to be done to handle non-percentage values.

I have reservations about the change made to lines 131-132. It's not clear to me why the initial scroll was being set to 1, and the in situ comment "Init scroll one pixel in to allow upward/leftward scroll" doesn't really clarify the why for me. It was introduced in this commit, titled "fix: scrollcontrols infinite trap and firefox support". I'm guessing it's needed to support infinite scroll--if the initial scroll is 0px then no up-scroll event could be triggered (i.e. the first scroll event would necessarily be a down-scroll) and so the jump-to-end would fail? If that's the case, then perhaps the following changes would be sufficient:

-    // Init scroll one pixel in to allow upward/leftward scroll
-    el[horizontal ? 'scrollLeft' : 'scrollTop'] = 1
+    // Initialize scroll
+    const { scrollThreshold } = getScrollDimensions()
+    el[horizontal ? 'scrollLeft' : 'scrollTop'] = Math.max(initialScroll * scrollThreshold, 1)