# Plugin: pinchers

  • 👌 Pinch your thumb with any finger to set that fingers "click" state
  • Unpinched fingers are black, pinched fingers are red

Models: MediaPipe Hands

About: This plugin emits events, adds new properties to the hand data, and sets classes on the body to help you style elements based on which fingers were pinched

Activate: This plugin is automatically activated when the Hands Model is enabled

Tags: ['core']

Hand Index [0] Middle [1] Ring [2] Pinky [3]
Left
Right

# Properties

# Pinch States with .pinchState

This plugin adds handsfree.data.hands.pinchState to the Hands Model. It is a 2D array with the following:

handsfree.data.hands.pinchState = [
  // Left hand
  // index, middle, ring, pinky
  ['', '', '', ''],
  // Right hand
  // index, middle, ring, pinky
  ['', '', '', '']
]

Each index can be of one of the following states:

State Note
start When the pinch first starts
held Every frame the pinch is held
released When the pinch is released
const handsfree = new Handsfree({hands: true})
handsfree.use('logger', ({hands}) => {
  console.log(hands.pinchState)
})

# Original Pinch Locations with .origPos

In addition the the .pinchState, you also have access to the original pixel {x, y} that the pinch occurred within the webcam through .origPinch. This is very useful for determining how far a pinch was "dragged". Like with .pinchState, handsfree.data.hands.origPinch contains one value per finger per hand:

// Log the original point of pinch
handsfree.on('finger-pinched-0-1', () => {
  // Display the x and y of the left pointer finger
  console.log(
    handsfree.data.hands.origPinch[0][0].x,
    handsfree.data.hands.origPinch[0][0].y
  )
})

# Current Pinch Locations with .curPinch

Like .origPinch, .curPinch lists the current pixel {x, y} that the pinch is happening at. This is useful for calculating the distance since the .origPinch:

// Log the original point of pinch
handsfree.on('finger-pinched-1-3', () => {
  // Display the x and y of the right pinky
  console.log(
    handsfree.data.hands.curPinch[1][3].x,
    handsfree.data.hands.curPinch[1][3].y
  )
})

# Events

Currently this plugin emits an event for every individual finger, which you can listen to. There are a total of 8 possible events, where h represents the hand (0 = left, 1 = right) and f represents the index:

/* SPECIFIC HAND */
/* Any event */
handsfree-finger-pinched-h-f
/* start event */
handsfree-finger-pinched-start-h-f
/* held event */
handsfree-finger-pinched-held-h-f
/* released event */
handsfree-finger-pinched-released-h-f

/* ANY HAND */
/* Any event */
handsfree-finger-pinched-f
/* start event */
handsfree-finger-pinched-start-f
/* held event */
handsfree-finger-pinched-held-f
/* released event */
handsfree-finger-pinched-released-f

Here are a few examples for listening to these events:

// ## SPECIFIC HAND
// Listen to any event from left hand (0), index finger (0)
document.addEventListener('handsfree-finger-pinched-0-0')
// Listen to any event right hand (1), pinky finger (3)
handsfree.on('finger-pinched-1-3')

// Listen to a specific event from left hand (0), middle finger (1)
handsfree.on('finger-pinched-start-0-1')
// Listen to a specific event from right hand (1), ring finger (2)
document.addEventListener('handsfree-finger-pinched-1-2')

// ## ANY HAND
// Listen to any event from with any index finger (0)
document.addEventListener('handsfree-finger-pinched-0')
// Listen to any event with any pinky finger (3)
handsfree.on('finger-pinched-3')

// Listen to a specific event with any middle finger (1)
handsfree.on('finger-pinched-start-1')
// Listen to a specific event with any ring finger (2)
document.addEventListener('handsfree-finger-pinched-2')

# Classes

This plugin comes with many helper classes to help you style your app based on the pinching fingers, they look like this:

/* ## SPECIFIC HAND */
/* Left hand (0), index finger (0) */
.handsfree-show-when-finger-pinched-0-0
.handsfree-hide-when-finger-pinched-0-0

/* Right hand (1), pinky finger (3) */
.handsfree-show-when-finger-pinched-1-3
.handsfree-hide-when-finger-pinched-1-3

/* ## ANY HAND */
/* Any index finger (0) */
.handsfree-show-when-finger-pinched-0
.handsfree-hide-when-finger-pinched-0

/* Any pinky finger (3) */
.handsfree-show-when-finger-pinched-3
.handsfree-hide-when-finger-pinched-3

Simply apply these classes to the elements you'd like to show/hide. If you'd like some more styling then you can take advantage of the body classes that get added:

/* Left (0) middle finger (1) */
body.handsfree-finger-pinched-0-1

/* Right (1) ring finger (2) */
body.handsfree-finger-pinched-1-2

/* ANY middle finger (1) */
body.handsfree-finger-pinched-1

/* ANY ring finger (2) */
body.handsfree-finger-pinched-2

# Full plugin code

export default {
  models: 'hands',
  enabled: true,
  tags: ['core'],

  // Index of fingertips
  fingertipIndex: [8, 12, 16, 20],

  // Number of frames the current element is the same as the last
  // [left, right]
  // [index, middle, ring, pinky]
  numFramesFocused: [[0, 0, 0, 0,], [0, 0, 0, 0]],

  // Whether the fingers are touching
  thresholdMet: [[0, 0, 0, 0,], [0, 0, 0, 0]],
  framesSinceLastGrab: [[0, 0, 0, 0,], [0, 0, 0, 0]],

  // The original grab point for each finger
  origPinch: [
    [{x: 0, y: 0}, {x: 0, y: 0}, {x: 0, y: 0}, {x: 0, y: 0}],
    [{x: 0, y: 0}, {x: 0, y: 0}, {x: 0, y: 0}, {x: 0, y: 0}]
  ],
  curPinch: [
    [{x: 0, y: 0}, {x: 0, y: 0}, {x: 0, y: 0}, {x: 0, y: 0}],
    [{x: 0, y: 0}, {x: 0, y: 0}, {x: 0, y: 0}, {x: 0, y: 0}]
  ],

  // Just downel
  pinchDowned: [
    [0, 0, 0, 0],
    [0, 0, 0, 0]
  ],
  pinchDown: [
    [false, false, false, false],
    [false, false, false, false]
  ],
  pinchUp: [
    [false, false, false, false],
    [false, false, false, false]
  ],

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

  // Number of frames that has passed since the last grab
  numFramesFocused: [[0, 0, 0, 0,], [0, 0, 0, 0]],

  // Number of frames mouse has been downed
  mouseDowned: 0,
  // Is the mouse up?
  mouseUp: false,
  // Whether one of the morph confidences have been met
  mouseThresholdMet: false,

  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,

    maxMouseDownedFrames: 1
  },

  onUse () {
    this.$target = window
  },

  /**
   * Scroll the page when the cursor goes above/below the threshold
   */
  onFrame ({hands}) {
    if (!hands.multiHandLandmarks) return

    const height = this.handsfree.debug.$canvas.hands.height
    const leftVisible = hands.multiHandedness.some(hand => hand.label === 'Right')
    const rightVisible = hands.multiHandedness.some(hand => hand.label === 'Left')
    
    // Detect if the threshold for clicking is met with specific morphs
    for (let n = 0; n < hands.multiHandLandmarks.length; n++) {
      // Set the hand index
      let hand = hands.multiHandedness[n].label === 'Right' ? 0 : 1
      
      for (let finger = 0; finger < 4; finger++) {
        // Check if fingers are touching
        const a = hands.multiHandLandmarks[n][4].x - hands.multiHandLandmarks[n][this.fingertipIndex[finger]].x
        const b = hands.multiHandLandmarks[n][4].y - hands.multiHandLandmarks[n][this.fingertipIndex[finger]].y
        const c = Math.sqrt(a*a + b*b) * height
        const thresholdMet = this.thresholdMet[hand][finger] = c < this.config.threshold

        if (thresholdMet) {
          // Set the current pinch
          this.curPinch[hand][finger] = hands.multiHandLandmarks[n][4]
          
          // Store the original pinch
          if (this.framesSinceLastGrab[hand][finger] > this.config.numThresholdErrorFrames) {
            this.origPinch[hand][finger] = hands.multiHandLandmarks[n][4]
            this.handsfree.TweenMax.killTweensOf(this.tween[hand][finger])
          }
          this.framesSinceLastGrab[hand][finger] = 0
        }
        ++this.framesSinceLastGrab[hand][finger]
      }
    }

    // Update the hands object
    hands.origPinch = this.origPinch
    hands.curPinch = this.curPinch
    this.handsfree.data.hands = this.getPinchStates(hands, leftVisible, rightVisible)
  },

  /**
   * Check if we are "mouse clicking"
   */
  getPinchStates (hands, leftVisible, rightVisible) {
    const visible = [leftVisible, rightVisible]

    // Make sure states are available
    hands.pinchState = [
      ['', '', '', ''],
      ['', '', '', '']
    ]
    
    // Loop through every hand and finger
    for (let hand = 0; hand < 2; hand++) {
      for (let finger = 0; finger < 4; finger++) {
        // Click
        if (visible[hand] && this.thresholdMet[hand][finger]) {
          this.pinchDowned[hand][finger]++
          document.body.classList.add(`handsfree-finger-pinched-${hand}-${finger}`, `handsfree-finger-pinched-${finger}`)
        } else {
          this.pinchUp[hand][finger] = this.pinchDowned[hand][finger]
          this.pinchDowned[hand][finger] = 0
          document.body.classList.remove(`handsfree-finger-pinched-${hand}-${finger}`, `handsfree-finger-pinched-${finger}`)
        }
        
        // Set the state
        if (this.pinchDowned[hand][finger] > 0 && this.pinchDowned[hand][finger] <= this.config.maxMouseDownedFrames) {
          hands.pinchState[hand][finger] = 'start'
        } else if (this.pinchDowned[hand][finger] > this.config.maxMouseDownedFrames) {
          hands.pinchState[hand][finger] = 'held'
        } else if (this.pinchUp[hand][finger]) {
          hands.pinchState[hand][finger] = 'released'
        } else {
          hands.pinchState[hand][finger] = ''
        }

        // Emit an event
        if (hands.pinchState[hand][finger]) {
          // Specific hand
          this.handsfree.emit(`finger-pinched-${hand}-${finger}`, {
            event: hands.pinchState[hand][finger],
            origPinch: hands.origPinch[hand][finger],
            curPinch: hands.curPinch[hand][finger]
          })
          this.handsfree.emit(`finger-pinched-${hands.pinchState[hand][finger]}-${hand}-${finger}`, {
            event: hands.pinchState[hand][finger],
            origPinch: hands.origPinch[hand][finger],
            curPinch: hands.curPinch[hand][finger]
          })
          // Any hand
          this.handsfree.emit(`finger-pinched-${finger}`, {
            event: hands.pinchState[hand][finger],
            origPinch: hands.origPinch[hand][finger],
            curPinch: hands.curPinch[hand][finger]
          })
          this.handsfree.emit(`finger-pinched-${hands.pinchState[hand][finger]}-${finger}`, {
            event: hands.pinchState[hand][finger],
            origPinch: hands.origPinch[hand][finger],
            curPinch: hands.curPinch[hand][finger]
          })
        }
      }
    }

    return hands
  }
}
Debugger