Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div class="instructions">
	<p>
		Move your <b>right hand</b> in front of your webcam to move the cursor.
		<br>Pinch your thumb and index finger to pick up a photo.
	</p>
	<label><input type="checkbox" id="hand_choice"> <span>Click here to use <b>left hand</b></span></label>
</div>

<div class="debug_window">
  <video class="input_video"></video>
  <canvas class="output_canvas" width="1280px" height="720px"></canvas>
</div>

<div class="movable photo photo_a">
  <div class="photo_frame">
    <img src="https://assets.codepen.io/246719/pexels-photo-145685.jpeg" alt="garden pond" />
  </div>
</div>

<div class="movable photo photo_b">
  <div class="photo_frame">
    <img src="https://assets.codepen.io/246719/pexels-photo-206904.jpeg" alt="sun over mountains" />
  </div>
</div>

<div class="cursor wand"></div>
              
            
!

CSS

              
                .instructions {
	color: #eee;
	font-size: 20px;
	padding: 20px;
	line-height: 1.4;
	font-family: verdana;
}
.instructions b {
	color: gold;
}
.instructions label {
	cursor: pointer;
}
.instructions label span:hover {
	text-decoration: underline;
}

.movable {
  position: absolute;
  left: 0;
  top: 0;
  transition: transform 0.2s;
}

body {
  margin: 0px;
  font-family: sans-serif;
  background-color: #333;
}

.input_video {
  display: none;
}
.output_canvas {
  display: none;
  width: 400px;
}

.wand {
  height: 0px;
  width: 0px;
  position: absolute;
  left: 0px;
  top: 0px;
  z-index: 10;
  transition: transform 0.1s;
}

.wand::after {
  content: '';
  display: block;
  height: 50px;
  width: 50px;
  border-radius: 50%;
  position: absolute;
  left: 0;
  top: 0;
  transform: translate(-50%, -50%);
  transition: transform 0.2s;
  box-shadow:
    inset 0 0 4px 2px white,
    inset 0 0 4px #0098db,
    0 0 8px #fff,
    -4px 0 10px #f0fd,
    4px 0 10px #0ffd
  ;
  animation: rotate-cursor 5s infinite linear;
}

@keyframes rotate-cursor {
  0% {
    transform: translate(-50%, -50%) rotate(0deg);
  }
  100% {
    transform: translate(-50%, -50%) rotate(360deg);
  }
}

.photo_frame {
  width: 200px;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 3px;
  background: #fff;
  transition: transform 0.2s;
  box-shadow: 0px 2px 10px 0px #000e;
}
.element_dragging .photo_frame {
  box-shadow: 0px 10px 20px 5px #000c;
  transform: scale(1.2);
}
.photo img {
  width: 100%;
  display: block;
}
.photo_a {
  transform: translate(5vw, 45vh);
}
.photo_b {
  transform: translate(55vw, 50vh);
}
.photo_a .photo_frame {
  width: 250px;
}
              
            
!

JS

              
                const leftyCheckbox = document.querySelector('#hand_choice');
leftyCheckbox.addEventListener('change', () => {
	OPTIONS.PREFERRED_HAND = leftyCheckbox.checked ? 'left' : 'right';
});

const OPTIONS = {
  IS_DEBUG_MODE: false,
  PREFERRED_HAND: leftyCheckbox.checked ? 'left' : 'right',
  PINCH_DELAY_MS: 60,
};

const state = {
  isPinched: false,
  pinchChangeTimeout: null,
  grabbedElement: null,
  lastGrabbedElement: null,
};

const PINCH_EVENTS = {
  START: 'pinch_start',
  MOVE: 'pinch_move',
  STOP: 'pinch_stop',
  PICK_UP: 'pinch_pick_up',
  DROP: 'pinch_drop',
};

function triggerEvent({ eventName, eventData }) {
  const event = new CustomEvent(eventName, { detail: eventData });
  document.dispatchEvent(event);
}

const videoElement = document.querySelector('.input_video');
const debugCanvas = document.querySelector('.output_canvas');
const debugCanvasCtx = debugCanvas.getContext('2d');

const cursor = document.querySelector('.cursor');
const movableElements = [
  ...document.querySelectorAll('.movable'),
];

const handParts = {
  wrist: 0,
  thumb: { base: 1, middle: 2, topKnuckle: 3, tip: 4 },
  indexFinger: { base: 5, middle: 6, topKnuckle: 7, tip: 8 },
  middleFinger: { base: 9, middle: 10, topKnuckle: 11, tip: 12 },
  ringFinger: { base: 13, middle: 14, topKnuckle: 15, tip: 16 },
  pinky: { base: 17, middle: 18, topKnuckle: 19, tip: 20 },
};

function getElementCoords(element) {
  const rect = element.getBoundingClientRect();
  const elementTop = rect.top / window.innerHeight;
  const elementBottom = rect.bottom / window.innerHeight;
  const elementLeft = rect.left / window.innerWidth;
  const elementRight = rect.right / window.innerWidth;
  return { elementTop, elementBottom, elementLeft, elementRight };
};

function isElementPinched({ pinchX, pinchY, elementTop, elementBottom, elementLeft, elementRight }) {
  const isPinchInXRange = elementLeft <= pinchX && pinchX <= elementRight;
  const isPinchInYRange = elementTop <= pinchY && pinchY <= elementBottom;
  return isPinchInXRange && isPinchInYRange;
};

function getPinchedElement({ pinchX, pinchY, elements }) {
  let grabbedElement;
  for (const element of elements) {
    const elementCoords = getElementCoords(element);
    if (isElementPinched({ pinchX, pinchY, ...elementCoords })) {
      grabbedElement = {
        domNode: element,
        coords: {
          x: elementCoords.elementLeft,
          y: elementCoords.elementTop,
        },
        offsetFromCorner: {
          x: pinchX - elementCoords.elementLeft,
          y: pinchY - elementCoords.elementTop,
        },
      };
      const isTopElement = element === state.lastGrabbedElement;
      if (isTopElement) {
        return grabbedElement;
      }
    }
  }
  return grabbedElement;
};

function log(...args) {
  if (OPTIONS.IS_DEBUG_MODE) {
    console.log(...args);
  }
}

function getCurrentHand(handData) {
  const isHandAvailable = !!handData.multiHandLandmarks?.[0];
  if (!isHandAvailable) { return null; }
  const mirroredHand = handData.multiHandedness[0].label === 'Left' ? 'right' : 'left';
  return mirroredHand;
}

function isPinched(handData) {
  if (isPrimaryHandAvailable(handData)) {
    const fingerTip = handData.multiHandLandmarks[0][handParts.indexFinger.tip];
    const thumbTip = handData.multiHandLandmarks[0][handParts.thumb.tip];
    const distance = {
      x: Math.abs(fingerTip.x - thumbTip.x),
      y: Math.abs(fingerTip.y - thumbTip.y),
      z: Math.abs(fingerTip.z - thumbTip.z),
    };
    const areFingersCloseEnough = distance.x < 0.08 && distance.y < 0.08 && distance.z < 0.11;
    log(distance);
    log({isPinched: areFingersCloseEnough});
    return areFingersCloseEnough;
  }
  return false;
}

function convertCoordsToDomPosition({ x, y }) {
  return {
    x: `${x * 100}vw`,
    y: `${y * 100}vh`,
  };
}

function updateDebugCanvas(handData) {
  debugCanvasCtx.save();
  debugCanvasCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height);
  debugCanvasCtx.drawImage(handData.image, 0, 0, debugCanvas.width, debugCanvas.height);
  if (handData.multiHandLandmarks) {
    for (const landmarks of handData.multiHandLandmarks) {
      drawConnectors(debugCanvasCtx, landmarks, HAND_CONNECTIONS, {color: '#0ff', lineWidth: 5});
      drawLandmarks(debugCanvasCtx, landmarks, {color: '#f0f', lineWidth: 2});
    }
  }
  debugCanvasCtx.restore();
}

function getCursorCoords(handData) {
  const { x, y, z } = handData.multiHandLandmarks[0][handParts.indexFinger.middle];
  const mirroredXCoord = -x + 1; /* due to camera mirroring */
  return { x: mirroredXCoord, y, z };
}

function isPrimaryHandAvailable(handData) {
  return getCurrentHand(handData) === OPTIONS.PREFERRED_HAND;
}

function onResults(handData) {
  if (!handData) { return; }
  if (OPTIONS.IS_DEBUG_MODE) { updateDebugCanvas(handData); }

  updateCursor(handData);
  updatePinchState(handData);
}

function updateCursor(handData) {
  if (isPrimaryHandAvailable(handData)) {
    const cursorCoords = getCursorCoords(handData);
    if (!cursorCoords) { return; }
    const { x, y } = convertCoordsToDomPosition(cursorCoords);
    cursor.style.transform = `translate(${x}, ${y})`;
  }
}

function updatePinchState(handData) {
  const wasPinchedBefore = state.isPinched;
  const isPinchedNow = isPinched(handData);
  const hasPassedPinchThreshold = isPinchedNow !== wasPinchedBefore;
  const hasWaitStarted = !!state.pinchChangeTimeout;

  if (hasPassedPinchThreshold && !hasWaitStarted) {
    registerChangeAfterWait(handData, isPinchedNow);
  }

  if (!hasPassedPinchThreshold) {
    cancelWaitForChange();
    if (isPinchedNow) {
      triggerEvent({
        eventName: PINCH_EVENTS.MOVE,
        eventData: getCursorCoords(handData),
      });
    }
  }
}

function registerChangeAfterWait(handData, isPinchedNow) {
  state.pinchChangeTimeout = setTimeout(() => {
    state.isPinched = isPinchedNow;
    triggerEvent({
      eventName: isPinchedNow ? PINCH_EVENTS.START : PINCH_EVENTS.STOP,
      eventData: getCursorCoords(handData),
    });
  }, OPTIONS.PINCH_DELAY_MS);
}

function cancelWaitForChange() {
  clearTimeout(state.pinchChangeTimeout);
  state.pinchChangeTimeout = null;
}

document.addEventListener(PINCH_EVENTS.START, onPinchStart);
document.addEventListener(PINCH_EVENTS.MOVE, onPinchMove);
document.addEventListener(PINCH_EVENTS.STOP, onPinchStop);
document.addEventListener(PINCH_EVENTS.PICK_UP, onPickUp);
document.addEventListener(PINCH_EVENTS.DROP, onDrop);

function onPinchStart(eventInfo) {
  const cursorCoords = eventInfo.detail;

  state.grabbedElement = getPinchedElement({
    pinchX: cursorCoords.x,
    pinchY: cursorCoords.y,
    elements: movableElements,
  });
  if (state.grabbedElement) {
    triggerEvent({
      eventName: PINCH_EVENTS.PICK_UP,
      eventData: state.grabbedElement.domNode,
    });
  }

  document.body.classList.add('is-pinched');
}

function onPinchMove(eventInfo) {
  const cursorCoords = eventInfo.detail;

  if (state.grabbedElement) {
    state.grabbedElement.coords = {
      x: cursorCoords.x - state.grabbedElement.offsetFromCorner.x,
      y: cursorCoords.y - state.grabbedElement.offsetFromCorner.y,
    };

    const { x, y } = convertCoordsToDomPosition(state.grabbedElement.coords);
    state.grabbedElement.domNode.style.transform = `translate(${x}, ${y})`;
  }
}

function onPinchStop() {
  document.body.classList.remove('is-pinched');
  if (state.grabbedElement) {
    triggerEvent({
      eventName: PINCH_EVENTS.DROP,
      eventData: state.grabbedElement.domNode,
    });
  }
}

function onPickUp(eventInfo) {
  const element = eventInfo.detail;
  state.lastGrabbedElement?.style.removeProperty('z-index');
  state.lastGrabbedElement = element;
  element.style.zIndex = 1;
  element.classList.add('element_dragging');
};

function onDrop(eventInfo) {
  const element = eventInfo.detail;
  state.isDragging = false;
  state.grabbedElement = undefined;
  element.classList.remove('element_dragging');
};

const hands = new Hands({locateFile: (file) => {
  return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`;
}});
hands.setOptions({
  maxNumHands: 1,
  modelComplexity: 1,
  minDetectionConfidence: 0.5,
  minTrackingConfidence: 0.5
});
hands.onResults(onResults);

const camera = new Camera(videoElement, {
  onFrame: async () => {
    await hands.send({image: videoElement});
  },
  width: 1280,
  height: 720
});
camera.start();

function startDebug() {
  debugCanvas.style.display = 'block';
  document.head.innerHTML += `
    <style>
      body.is-pinched {
        background: gold;
      }
    </style>
  `;
}
if (OPTIONS.IS_DEBUG_MODE) {
  startDebug();
}

              
            
!
999px

Console