Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Omni-Hand Diagnostic Mode</title> | |
| <style> | |
| body { | |
| margin: 0; overflow: hidden; background-color: #050505; | |
| font-family: 'Segoe UI', monospace; user-select: none; | |
| } | |
| /* HUD - Top Left */ | |
| #hud { | |
| position: absolute; top: 20px; left: 20px; | |
| width: 280px; padding: 20px; | |
| background: rgba(20, 20, 20, 0.9); | |
| border-left: 4px solid #00ff88; | |
| border-radius: 4px; | |
| color: #fff; z-index: 10; | |
| } | |
| .row { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 8px; color: #ccc; } | |
| .val { font-weight: bold; color: #fff; } | |
| .highlight { color: #00ff88; text-shadow: 0 0 10px rgba(0,255,136,0.5); } | |
| #gesture-display { | |
| font-size: 20px; text-align: center; margin-top: 15px; | |
| padding-top: 15px; border-top: 1px solid #333; | |
| color: #666; font-weight: 300; | |
| } | |
| /* DEBUG VIEW - Bottom Right */ | |
| #debug-container { | |
| position: absolute; bottom: 20px; right: 20px; | |
| width: 320px; height: 240px; | |
| background: #000; | |
| border: 2px solid #333; | |
| z-index: 100; | |
| } | |
| #debug-video { width: 100%; height: 100%; object-fit: cover; transform: scaleX(-1); opacity: 0.6; } | |
| #debug-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; transform: scaleX(-1); } | |
| #debug-label { | |
| position: absolute; top: 0; left: 0; background: red; color: white; | |
| font-size: 10px; padding: 2px 5px; | |
| } | |
| #loading { | |
| position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); | |
| color: #00ff88; font-size: 24px; text-transform: uppercase; letter-spacing: 4px; | |
| text-shadow: 0 0 20px #00ff88; | |
| } | |
| </style> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js"></script> | |
| </head> | |
| <body> | |
| <div id="loading">Loading AI Engine...</div> | |
| <div id="hud"> | |
| <div style="font-size: 10px; color: #888; margin-bottom: 10px;">DIAGNOSTIC MODE</div> | |
| <div class="row"><span>HAND TRACKING</span><span id="ui-track" class="val" style="color:red">OFFLINE</span></div> | |
| <div class="row"><span>SHAPE</span><span id="ui-shape" class="val">GALAXY</span></div> | |
| <div class="row"><span>PARTICLES</span><span class="val">12,000</span></div> | |
| <div id="gesture-display">WAITING...</div> | |
| </div> | |
| <div id="debug-container"> | |
| <div id="debug-label">CAMERA FEED</div> | |
| <video id="debug-video" playsinline></video> | |
| <canvas id="debug-canvas" width="320" height="240"></canvas> | |
| </div> | |
| <script> | |
| // --- CONFIG --- | |
| const CONFIG = { | |
| count: 12000, | |
| camWidth: 640, | |
| camHeight: 480 | |
| }; | |
| const State = { | |
| active: false, | |
| hand: { x: 0, y: 0, z: 0 }, | |
| gesture: 'NONE', | |
| shapeIdx: 0, | |
| lastGestureTime: 0 | |
| }; | |
| // --- THREE.JS SETUP --- | |
| const scene = new THREE.Scene(); | |
| scene.fog = new THREE.FogExp2(0x050505, 0.04); | |
| const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 100); | |
| camera.position.z = 10; | |
| const renderer = new THREE.WebGLRenderer({ antialias: false }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| document.body.appendChild(renderer.domElement); | |
| // Particles | |
| const geo = new THREE.BufferGeometry(); | |
| const pos = new Float32Array(CONFIG.count * 3); | |
| const tar = new Float32Array(CONFIG.count * 3); | |
| const vel = new Float32Array(CONFIG.count * 3); // Velocity | |
| for(let i=0; i<CONFIG.count*3; i++) { | |
| pos[i] = (Math.random()-0.5)*30; | |
| tar[i] = pos[i]; | |
| vel[i] = 0; | |
| } | |
| geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); | |
| // Texture | |
| const cvs = document.createElement('canvas'); cvs.width=32; cvs.height=32; | |
| const ctx = cvs.getContext('2d'); | |
| const grd = ctx.createRadialGradient(16,16,0,16,16,16); | |
| grd.addColorStop(0,'white'); grd.addColorStop(1,'transparent'); | |
| ctx.fillStyle = grd; ctx.fillRect(0,0,32,32); | |
| const tex = new THREE.CanvasTexture(cvs); | |
| const mat = new THREE.PointsMaterial({ | |
| size: 0.15, map: tex, color: 0x00ff88, | |
| transparent: true, opacity: 0.8, blending: THREE.AdditiveBlending, depthWrite: false | |
| }); | |
| const particles = new THREE.Points(geo, mat); | |
| scene.add(particles); | |
| // --- SHAPES --- | |
| const Shapes = [ | |
| (i) => { // Galaxy | |
| const r = (i/CONFIG.count)*8; const a = (i/CONFIG.count)*20; | |
| return {x: Math.cos(a)*r, y: (Math.random()-0.5), z: Math.sin(a)*r}; | |
| }, | |
| (i) => { // Sphere | |
| const p = Math.acos(-1+(2*i)/CONFIG.count); const t = Math.sqrt(CONFIG.count*Math.PI)*p; | |
| return {x: 4*Math.sin(p)*Math.cos(t), y: 4*Math.sin(p)*Math.sin(t), z: 4*Math.cos(p)}; | |
| } | |
| ]; | |
| function updateTargets() { | |
| const func = Shapes[State.shapeIdx]; | |
| for(let i=0; i<CONFIG.count; i++) { | |
| const p = func(i); | |
| tar[i*3] = p.x; tar[i*3+1] = p.y; tar[i*3+2] = p.z; | |
| } | |
| } | |
| updateTargets(); | |
| // --- GESTURE LOGIC (RELAXED) --- | |
| function detectGesture(lm) { | |
| // Tips: 8(Idx), 12(Mid), 16(Rng), 20(Pnk) | PIPs: 6, 10, 14, 18 | |
| // 0 = Wrist | |
| // Helper: Is finger extended? (Tip higher than PIP) | |
| const idxUp = lm[8].y < lm[6].y; | |
| const midUp = lm[12].y < lm[10].y; | |
| const rngUp = lm[16].y < lm[14].y; | |
| const pnkUp = lm[20].y < lm[18].y; | |
| // Helper: Distance | |
| const pinchDist = Math.hypot(lm[4].x - lm[8].x, lm[4].y - lm[8].y); | |
| const fistDist = Math.hypot(lm[0].x - lm[12].x, lm[0].y - lm[12].y); // Wrist to mid-tip | |
| let g = "UNKNOWN"; | |
| // Logic Tree | |
| if (fistDist < 0.25 && !idxUp) g = "FIST (GRAVITY)"; // Relaxed threshold | |
| else if (pinchDist < 0.08) g = "PINCH (ZOOM)"; | |
| else if (idxUp && pnkUp && !midUp) g = "ROCK (TRAILS)"; | |
| else if (idxUp && midUp && !rngUp) g = "PEACE (CHAOS)"; | |
| else if (idxUp && !midUp && !pnkUp) g = "INDEX (CURSOR)"; | |
| else if (!idxUp && !midUp && pnkUp) g = "PINKY (VORTEX)"; | |
| else if (idxUp && midUp && rngUp && pnkUp) g = "OPEN (SHIELD)"; | |
| return g; | |
| } | |
| // --- MEDIAPIPE SETUP --- | |
| const videoElem = document.getElementById('debug-video'); | |
| const debugCanvas = document.getElementById('debug-canvas'); | |
| const debugCtx = debugCanvas.getContext('2d'); | |
| const uiTrack = document.getElementById('ui-track'); | |
| const uiGesture = document.getElementById('gesture-display'); | |
| function onResults(results) { | |
| document.getElementById('loading').style.display = 'none'; | |
| // Debug Draw | |
| debugCtx.save(); | |
| debugCtx.clearRect(0, 0, debugCanvas.width, debugCanvas.height); | |
| if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) { | |
| State.active = true; | |
| uiTrack.innerText = "ONLINE"; | |
| uiTrack.style.color = "#00ff88"; | |
| const lm = results.multiHandLandmarks[0]; | |
| // DRAW SKELETON (Crucial for Debug) | |
| drawConnectors(debugCtx, lm, HAND_CONNECTIONS, {color: '#00ff00', lineWidth: 2}); | |
| drawLandmarks(debugCtx, lm, {color: '#ff0000', lineWidth: 1, radius: 2}); | |
| // Map Position | |
| const x = (1 - lm[9].x) * 16 - 8; | |
| const y = -(lm[9].y - 0.5) * 12; | |
| State.hand.x += (x - State.hand.x) * 0.2; | |
| State.hand.y += (y - State.hand.y) * 0.2; | |
| // Detect | |
| const g = detectGesture(lm); | |
| if(g !== State.gesture) { | |
| State.gesture = g; | |
| uiGesture.innerHTML = `<span class="highlight">${g}</span>`; | |
| // Swipe Logic | |
| if(g === "INDEX (CURSOR)" && Math.abs(x - State.hand.x) > 0.5) { | |
| // Simple logic for swipe | |
| } | |
| } | |
| } else { | |
| State.active = false; | |
| uiTrack.innerText = "NO HAND"; | |
| uiTrack.style.color = "red"; | |
| uiGesture.innerText = "SHOW HAND"; | |
| } | |
| debugCtx.restore(); | |
| } | |
| const hands = new Hands({locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`}); | |
| hands.setOptions({maxNumHands: 1, minDetectionConfidence: 0.5, minTrackingConfidence: 0.5}); | |
| hands.onResults(onResults); | |
| const cam = new Camera(videoElem, { | |
| onFrame: async () => { await hands.send({image: videoElem}); }, | |
| width: 320, height: 240 | |
| }); | |
| cam.start(); | |
| // --- ANIMATION LOOP --- | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| // Physics | |
| for(let i=0; i<CONFIG.count; i++) { | |
| const i3 = i*3; | |
| let tx = tar[i3]; let ty = tar[i3+1]; let tz = tar[i3+2]; | |
| if(State.active) { | |
| const dx = pos[i3] - State.hand.x; | |
| const dy = pos[i3+1] - State.hand.y; | |
| const dist = Math.sqrt(dx*dx + dy*dy); | |
| if(State.gesture.includes("FIST") && dist < 10) { | |
| tx -= dx*0.5; ty -= dy*0.5; // Suck | |
| } else if(State.gesture.includes("SHIELD") && dist < 6) { | |
| tx += dx*0.5; ty += dy*0.5; // Push | |
| } | |
| } | |
| vel[i3] += (tx - pos[i3])*0.05; | |
| vel[i3+1] += (ty - pos[i3+1])*0.05; | |
| vel[i3+2] += (tz - pos[i3+2])*0.05; | |
| vel[i3] *= 0.9; vel[i3+1] *= 0.9; vel[i3+2] *= 0.9; | |
| pos[i3] += vel[i3]; pos[i3+1] += vel[i3+1]; pos[i3+2] += vel[i3+2]; | |
| } | |
| geo.attributes.position.needsUpdate = true; | |
| renderer.render(scene, camera); | |
| } | |
| animate(); | |
| window.onresize = () => { | |
| camera.aspect = window.innerWidth/window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }; | |
| </script> | |
| </body> | |
| </html> | |