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

Auto Save

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 beam sword.
		<br>Pinch your thumb and index finger to turn the beam on or off.
	</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="cursor lightsaber"></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);
  }
}


.lightsaber {
  --blade-color: #0ff;
  --handle-length: 350px;
  --blade-length: 600px;

  height: 0px;
  width: 0px;
  position: absolute;
  left: 0px;
  top: 0px;
  z-index: 10;
  transition: transform 0.15s;
  transform: translate(-300px, 200px);

  --color-shift:
    inset 0 0 4px 2px white,
    inset 0 0 4px #0098db,
    0 0 8px #fff,
    -4px 0 10px #f0fd,
    4px 0 10px #0ffd
  ;
  --color-shift--animated:
    inset 0 0 4px 2px white,
    inset 0 0 4px #0098db,
    0 0 8px #fff,
    -4px 0 20px #0ffd,
    4px 0 20px #f0fd
  ;

  --solid-color:
    inset 0 0 4px var(--blade-color),
    -4px 0 10px var(--blade-color);
  --solid-color--animated:
    inset 0 0 4px var(--blade-color),
    -4px 0 20px var(--blade-color);
}

/* blade */
.lightsaber::before {
  content: '';
  display: block;
  transform-origin: center right;
  transform: translate(0%, -50%) scaleX(0);
  transition: transform 0.3s;
  height: 20px;
  width: var(--blade-length);
  position: absolute;
  right: calc(var(--handle-length) / 2 - 40px);
  top: 0;
  background: #fff;
  border-radius: 10px;
  /* box-shadow: var(--color-shift); */
  box-shadow: var(--solid-color);
}
.lightsaber.expanded::before {
  transform: translate(0%, -50%) scaleX(1);
  animation: animate-blade 5s infinite linear;
}

@keyframes animate-blade {
  50% {
    /* box-shadow: var(--color-shift--animated); */
    box-shadow: var(--solid-color--animated);
  }
}

/* hilt */
.lightsaber::after {
  content: '';
  display: block;
  height: 80px;
  width: var(--handle-length);
  position: absolute;
  left: 0;
  top: 0;
  transform: translate(-50%, -50%);
  transition: transform 0.2s;
  /* border: 1px dashed #ddd; */
  background-image: url("https://assets.codepen.io/246719/lightsaber_hilt.png"); /* image credit: pngkey.com (free to download) */
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}

              
            
!

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: handData,
      });
    }
  }
}

function registerChangeAfterWait(handData, isPinchedNow) {
  state.pinchChangeTimeout = setTimeout(() => {
    state.isPinched = isPinchedNow;
    triggerEvent({
      eventName: isPinchedNow ? PINCH_EVENTS.START : PINCH_EVENTS.STOP,
      eventData: 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 handData = eventInfo.detail;
  const cursorCoords = getCursorCoords(handData);

  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 handData = eventInfo.detail;
  const cursorCoords = getCursorCoords(handData);

  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();
}






const lightsaber = document.querySelector('.lightsaber');
document.addEventListener('pinch_start', () => {
  lightsaber.classList.toggle('expanded');
});

              
            
!
999px

Console