UNPKG

@theforce/core

Version:

Core library for TheForce hand tracking

287 lines (283 loc) 10 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; // src/index.ts var src_exports = {}; __export(src_exports, { HandTracker: () => HandTracker }); module.exports = __toCommonJS(src_exports); // src/utils.ts var getCursorScreenCoordinates = (landmark, config) => { const centerX = window.innerWidth / 2; const centerY = window.innerHeight / 2; const normalizedXFromCenter = 0.5 - landmark.x; const normalizedYFromCenter = landmark.y - 0.5; const scaledXFromCenter = normalizedXFromCenter * (config.sensitivityX || 1); const scaledYFromCenter = normalizedYFromCenter * (config.sensitivityY || 1); const clampedScaledX = Math.max(-0.5, Math.min(0.5, scaledXFromCenter)); const clampedScaledY = Math.max(-0.5, Math.min(0.5, scaledYFromCenter)); const x = centerX + clampedScaledX * window.innerWidth; const y = centerY + clampedScaledY * window.innerHeight; return { x, y }; }; // src/hand-tracker.ts var HandTracker = class { constructor(config = {}) { __publicField(this, "hands"); // Changed to any __publicField(this, "camera", null); // Changed to any __publicField(this, "videoElement"); __publicField(this, "canvasElement"); __publicField(this, "cursorElement", null); __publicField(this, "config"); __publicField(this, "onResultsCallback", null); __publicField(this, "hoverTimeout", null); __publicField(this, "hoveredElement", null); __publicField(this, "isInitialized", false); this.config = { hoverDelay: 2e3, sensitivityX: 1, sensitivityY: 1, cursorLandmarkIndex: 9, // Default to middle finger base ...config }; this.hands = new window.Hands({ locateFile: (file) => { return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`; } }); this.videoElement = document.createElement("video"); this.videoElement.autoplay = true; this.videoElement.muted = true; this.videoElement.playsInline = true; this.videoElement.style.display = "none"; this.canvasElement = document.createElement("canvas"); this.canvasElement.style.display = "none"; if (this.config.debug) { this.videoElement.style.display = "block"; this.videoElement.style.position = "fixed"; this.videoElement.style.bottom = "10px"; this.videoElement.style.right = "10px"; this.videoElement.style.width = "200px"; this.videoElement.style.height = "150px"; this.videoElement.style.zIndex = "9999"; document.body.appendChild(this.videoElement); this.canvasElement.style.display = "block"; this.canvasElement.style.position = "fixed"; this.canvasElement.style.bottom = "10px"; this.canvasElement.style.right = "10px"; this.canvasElement.style.width = "200px"; this.canvasElement.style.height = "150px"; this.canvasElement.style.zIndex = "10000"; document.body.appendChild(this.canvasElement); } this.setupHands(); } setupHands() { this.hands.setOptions({ maxNumHands: 1, modelComplexity: 1, minDetectionConfidence: 0.9, minTrackingConfidence: 0.9 }); this.hands.onResults((results) => { this.handleResults(results); }); } handleResults(results) { if (this.onResultsCallback) { this.onResultsCallback(results); } if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) { const landmarks = results.multiHandLandmarks[0]; this.updateCursor(landmarks); this.checkHoverableElements(landmarks); } } _getCursorScreenCoordinates(landmark) { return getCursorScreenCoordinates(landmark, { sensitivityX: this.config.sensitivityX, sensitivityY: this.config.sensitivityY }); } updateCursor(landmarks) { if (!this.cursorElement || !landmarks || landmarks.length === 0) return; const cursorLandmark = landmarks[this.config.cursorLandmarkIndex || 9]; if (cursorLandmark) { const { x, y } = this._getCursorScreenCoordinates(cursorLandmark); this.cursorElement.style.left = `${x}px`; this.cursorElement.style.top = `${y}px`; this.cursorElement.style.display = "block"; } } checkHoverableElements(landmarks) { if (!landmarks || landmarks.length === 0) return; const cursorLandmark = landmarks[this.config.cursorLandmarkIndex || 9]; if (!cursorLandmark) return; const { x, y } = this._getCursorScreenCoordinates(cursorLandmark); const element = document.elementFromPoint(x, y); const hoverableElement = element?.closest( "[data-hoverable]" ); if (hoverableElement && hoverableElement !== this.hoveredElement) { this.handleHoverStart(hoverableElement); } else if (!hoverableElement && this.hoveredElement) { this.handleHoverEnd(); } } handleHoverStart(element) { this.hoveredElement = element; element.classList.add("force-hover"); if (this.cursorElement) { this.cursorElement.classList.add("force-loading"); this.cursorElement.style.setProperty( "--hover-delay", `${this.config.hoverDelay || 2e3}ms` ); } if (this.hoverTimeout) { clearTimeout(this.hoverTimeout); } this.hoverTimeout = setTimeout(() => { this.triggerClick(element); }, this.config.hoverDelay || 2e3); } handleHoverEnd() { if (this.hoveredElement) { this.hoveredElement.classList.remove("force-hover"); this.hoveredElement = null; } if (this.cursorElement) { this.cursorElement.classList.remove("force-loading"); } if (this.hoverTimeout) { clearTimeout(this.hoverTimeout); this.hoverTimeout = null; } } triggerClick(element) { const event = new MouseEvent("click", { bubbles: true, cancelable: true, view: window }); element.dispatchEvent(event); } async initialize() { if (this.isInitialized) return; this.cursorElement = document.createElement("div"); this.cursorElement.style.position = "fixed"; this.cursorElement.style.pointerEvents = "none"; this.cursorElement.style.zIndex = "9999"; this.cursorElement.style.display = "none"; this.cursorElement.style.width = "20px"; this.cursorElement.style.height = "20px"; this.cursorElement.style.borderRadius = "50%"; this.cursorElement.style.backgroundColor = "red"; this.cursorElement.style.opacity = "0.7"; this.cursorElement.style.transform = "translate(-50%, -50%)"; this.cursorElement.style.transition = "all 0.2s"; const style = document.createElement("style"); style.innerHTML = ` .force-cursor { position: fixed; width: 20px; height: 20px; border-radius: 50%; background-color: red; opacity: 0.7; pointer-events: none; z-index: 9999; transform: translate(-50%, -50%); transition: transform 0.2s ease-out; } @keyframes force-spin { to { transform: rotate(360deg); } } `; document.head.appendChild(style); if (this.config.cursorImageUrl) { this.cursorElement.style.backgroundImage = `url(${this.config.cursorImageUrl})`; this.cursorElement.style.backgroundSize = "contain"; this.cursorElement.style.backgroundColor = "transparent"; } document.body.appendChild(this.cursorElement); this.isInitialized = true; } async start() { if (!this.isInitialized) { await this.initialize(); } if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { const stream = await navigator.mediaDevices.getUserMedia({ video: true }); this.videoElement.srcObject = stream; await this.videoElement.play(); this.canvasElement.width = this.videoElement.videoWidth; this.canvasElement.height = this.videoElement.videoHeight; } this.camera = new window.Camera(this.videoElement, { onFrame: async () => { if (this.videoElement) { await this.hands.send({ image: this.videoElement }); } }, width: 640, height: 480 }); await this.camera.start(); } async stop() { if (this.camera) { await this.camera.stop(); this.camera = null; } if (this.videoElement && this.videoElement.parentElement) { this.videoElement.parentElement.removeChild(this.videoElement); } if (this.canvasElement && this.canvasElement.parentElement) { this.canvasElement.parentElement.removeChild(this.canvasElement); } if (this.cursorElement && this.cursorElement.parentElement) { this.cursorElement.parentElement.removeChild(this.cursorElement); this.cursorElement = null; } this.handleHoverEnd(); this.isInitialized = false; } onResults(callback) { this.onResultsCallback = callback; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { HandTracker });