# Plugin: pinchScroll

  • 👌 Pinch your thumb and index to grab the page
  • ↕ While pinched, move hand up and down to scroll page
  • Adjustable speed

Models: MediaPipe Hands

Activate: handsfree.plugin.pinchScroll.enable()

Tags: ['browser']

About: This plugin helps you scroll pages by pinching together your thumb and pointer finger.

# Try scrolling these different areas at the same time

# Config

# During instantiation

const handsfree = new Handsfree({
  hands: true,

  plugin: {
    pinchScroll: {
      enabled: true,
      
      // Number of frames over the same element before activating that element
      framesToFocus: 10,

      // Number of pixels the middle and thumb tips must be near each other to drag
      threshold: 50,

      // Number of frames where a hold is not registered before releasing a drag
      numThresholdErrorFrames: 5,

      // Speed multiplier
      speed: .5
    }
  }
})

# After instantiation

const handsfree = new Handsfree({hands: true})
handsfree.start()

// Scroll a little slower
handsfree.plugin.pinchScroll.enable()
handsfree.plugin.pinchScroll.config.speed = 2

# See Also

  • palmPointers - Move a pointer on the screen with your hands
  • pinchers - A collection of events, properties, and helper styles for finger pinching

# Full plugin code

/**
 * Scrolls the page vertically by closing hand
 */
export default {
  models: 'hands',
  tags: ['browser'],
  enabled: false,

  // Number of frames the current element is the same as the last
  numFramesFocused: [0, 0, 0, 0],
  // The current scrollable target
  $target: [null, null, null, null],

  // The original grab point
  origScrollLeft: [0, 0, 0, 0],
  origScrollTop: [0, 0, 0, 0],

  // The tweened scrollTop, used to smoothen out scroll
  tweenScroll: [{x: 0, y: 0}, {x: 0, y: 0}, {x: 0, y: 0}, {x: 0, y: 0}],

  config: {
    // Number of frames over the same element before activating that element
    framesToFocus: 10,

    // Number of pixels the middle and thumb tips must be near each other to drag
    threshold: 50,

    // Number of frames where a hold is not registered before releasing a drag
    numThresholdErrorFrames: 5,

    // Speed multiplier
    speed: 1
  },

  /**
   * Scroll the page when the cursor goes above/below the threshold
   */
  onFrame ({hands}) {
    // Wait for other plugins to update
    setTimeout(() => {
      if (!hands.pointer) return
      const height = this.handsfree.debug.$canvas.hands.height
      const width = this.handsfree.debug.$canvas.hands.width

      hands.pointer.forEach((pointer, n) => {
        // @fixme Get rid of n > origPinch.length
        if (!pointer.isVisible || n > hands.origPinch.length) return

        // Start scroll
        if (hands.pinchState[n]?.[0] === 'start') {
          let $potTarget = document.elementFromPoint(pointer.x, pointer.y)

          this.$target[n] = this.getTarget($potTarget)
          this.tweenScroll[n].x = this.origScrollLeft[n] = this.getTargetScrollLeft(this.$target[n])
          this.tweenScroll[n].y = this.origScrollTop[n] = this.getTargetScrollTop(this.$target[n])
          this.handsfree.TweenMax.killTweensOf(this.tweenScroll[n])
        }

        if (hands.pinchState[n]?.[0] === 'held' && this.$target[n]) {
          // With this one you have to pinch, drag, and release in sections each time
          // this.handsfree.TweenMax.to(this.tweenScroll[n], 1, {
          //   x: this.origScrollLeft[n] - (hands.origPinch[n][0].x - hands.curPinch[n][0].x) * width,
          //   y: this.origScrollTop[n] + (hands.origPinch[n][0].y - hands.curPinch[n][0].y) * height,
          //   overwrite: true,
          //   ease: 'linear.easeNone',
          //   immediateRender: true  
          // })

          // With this one it continuously moves based on the pinch drag distance
          this.handsfree.TweenMax.to(this.tweenScroll[n], 1, {
            x: this.tweenScroll[n].x - (hands.origPinch[n][0].x - hands.curPinch[n][0].x) * width * this.config.speed,
            y: this.tweenScroll[n].y + (hands.origPinch[n][0].y - hands.curPinch[n][0].y) * height * this.config.speed,
            overwrite: true,
            ease: 'linear.easeNone',
            immediateRender: true  
          })

          this.$target[n].scrollTo(this.tweenScroll[n].x, this.tweenScroll[n].y)
        }
      })
    })
  },

  /**
   * Finds the closest scroll area
   */
  getTarget ($potTarget) {
    const styles = $potTarget && $potTarget.getBoundingClientRect ? getComputedStyle($potTarget) : {}

    if ($potTarget && $potTarget.scrollHeight > $potTarget.clientHeight &&
      (styles.overflow === 'auto' ||
        styles.overflow === 'auto scroll' ||
        styles.overflowY === 'auto' ||
        styles.overflowY === 'auto scroll')
    ) {
      return $potTarget
    } else {
      if ($potTarget && $potTarget.parentElement) {
        return this.getTarget($potTarget.parentElement)
      } else {
        return window
      }
    }
  },

  /**
   * Gets the scrolltop, taking account the window object
   */
  getTargetScrollLeft ($target) {
    return $target.scrollX || $target.scrollLeft || 0
  },

  /**
   * Gets the scrolltop, taking account the window object
   */
  getTargetScrollTop ($target) {
    return $target.scrollY || $target.scrollTop || 0
  }
}
Debugger