Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zoom pinch gestures and the from parameter #666

Open
msdcssd opened this issue May 27, 2024 · 0 comments
Open

Zoom pinch gestures and the from parameter #666

msdcssd opened this issue May 27, 2024 · 0 comments
Assignees

Comments

@msdcssd
Copy link

msdcssd commented May 27, 2024

Thanks a lot for this absolutely fantastic package!

After testing multiple non-working pinch-zoom-pan solutions for React, i've decided to build my own and use-gesture came up immediately as the de-facto standard for gesture handling in React.
I've followed the great YT series "Building an Animated Image Cropper" by Sam Selikoff and came up with a pretty flexible viewport system similar to 'react-zoom-pan-pinch', also supporting a minimap, spring motion, origin correction and more.

Question 1:

This viewport system needs to work on basically any system and I think I've finally managed to have all gestures required to work nicely together.
I'm still not 100% sure if I'm detecting different gesture states in the right way ...
In particular the touchpad pinch gesture is a bit confusing because it's fired with a 'wheel' event and looks exactly the same as a mousewheel event except for the ctrlKey set to true (and a different scaling in terms of delta).
The touchpad pinch also could be returning a 'scale', 'x' or 'y' axis value with the 'delta' saved in [0] or [1] and with different scaling.
I couldn't find this in the docs so I wonder if there's a better way to handle this scenario.
The zoom handler (below) is also used for example by some UI buttons with a fake gesture state.

const handleZoom = useCallback((gestureState, viewportOrMinimap, memo) => {
  if (!viewerSettings.zoom.enabled) return
  if (gestureState.last) return

  memo ??= { crop: cropRef.current }

  const viewportBounds = viewportRef.current.getBoundingClientRect()

  let deltaZoom = 0
  let origin = [0, 0]

  let gestureSignal = 'IGNORED'

  if (gestureState.type === 'click') {
    // Button click
    deltaZoom = Number(gestureState.currentTarget.dataset.zoom) * viewerSettings.zoom.zoomButtonStep
    gestureSignal = 'CLICK'
  } else if (gestureState.type === 'pointermove' && gestureState.pinching) {
    // Touchscreen (mobile) pinch-zoom
    deltaZoom = gestureState.delta[0]
    origin[0] = gestureState.origin[0] - viewportBounds.width / 2
    origin[1] = gestureState.origin[1] - viewportBounds.height / 2
    gestureSignal = 'MOBILE PINCH'
  } else if (gestureState.type === 'wheel') {
    if (gestureState.ctrlKey && gestureState.pinching === true) {
      // Touchpad (laptop) pinch-zoom
      switch (gestureState.axis) {
        case 'scale':
          deltaZoom = gestureState.delta[0]
          gestureSignal = 'TOUCHPAD PINCH'
          break
        case 'x':
          // ignore x
          break
        case 'y':
          // ignore y
          break
      }
      origin[0] = gestureState.event.clientX - viewportBounds.width / 2
      origin[1] = gestureState.event.clientY - viewportBounds.height / 2
    } else if (gestureState.pinching !== true && gestureState.axis === 'y') {
      // Mouse wheel
      deltaZoom = -gestureState.delta[1] / mouseWheelUnits * viewerSettings.zoom.mouseWheelStep
      origin[0] = gestureState.event.clientX - viewportBounds.width / 2
      origin[1] = gestureState.event.clientY - viewportBounds.height / 2
      gestureSignal = 'WHEEL'
    }
  }

  // REST OF THE CODE HAS BEEN REMOVED FOR SIMPLICITY
}), [viewerSettings, setCrop, enforceCrop]

Question 2:

This is not really a question but something that might help other users trying to build something similar: the 'from' parameter could use a simpler and clear explanation, which is extremely important when implementing a custom zoom pinch gesture that, in most cases, requires clamping the zoom scaling with a min / max range.

This took me ages to fully understand and the solution was actually very simple (passing the custom crop to the 'from' parameter).

Apparently without using 'from' parameter, use-gesture internally 'integrates' / 'accumulates' the intensity of the pinch based on how much the user pinch-zoomed so far (I'm not sure how to better explain this, sorry).
This gets complicated when mixing multiple gestures (wheel, touchpad, touch, etc.) and also button clicks ... a common requirement for viewport-based components that work like, for example, Google Maps.

For example, here's another issue which I suspect ran into the exact same problem: #435
I wonder if, without using 'from' parameter, this behavior should also affect the delta at all ... or perhaps if an additional delta property unaffected by the internal zoom level tracked by use-gesture could or should be provided?
Something like 'rawDelta' ? That would be yet another parameter requiring documentation and I don't know if it would be a good design / solution though.

const useGestureConfiguration = {
  drag: {
    enabled: viewerSettings.pan.enabled,
    from: () => cropRef.current.pan,
    preventScroll: false,
  },
  pinch: {
    enabled: viewerSettings.zoom.enabled,
    preventDefault: true,
    pinchOnWheel: true,
    angleBounds: { min: 0, max: 0, },
    from: () => [cropRef.current.zoom * pinchSensitivity, 0],
  },
  wheel: {
    enabled: viewerSettings.zoom.enabled,
    preventDefault: true,
    from: () => [0, -cropRef.current.zoom * mouseWheelUnits],
  },
  eventOptions: {
    passive: false,
  },
}

// Viewport gestures
useGesture({
  onDrag: (state) => handleDrag(state, 'viewport', state.memo),
  onPinch: (state) => handleZoom(state, 'viewport', state.memo),
  onWheel: (state) => handleZoom(state, 'viewport', state.memo),
},
  {
    ...useGestureConfiguration,
    target: viewportRef,
  })

// Minimap gestures
useGesture({
  onDrag: (state) => handleDrag(state, 'minimap', state.memo),
  onPinch: (state) => handleZoom(state, 'minimap', state.memo),
  onWheel: (state) => handleZoom(state, 'minimap', state.memo),
}, {
  ...useGestureConfiguration,
  target: minimapRef,
})

Information:

  • use-gesture/react: 10.3.1
  • Device: HP ZBook 15, Lenovo Yoga 7i 2024, Google Pixel 6
  • Browser: Google Chrome

Checklist:

  • [YES] I've read the documentation.
  • [YES] If this is an issue with drag, I've tried setting touch-action: none to the draggable element.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants