// Dev panel — draggable, collapsible floating panel for tuning hand tracking parameters // Gated on ?devmode=true URL parameter import { HAND_TRACKER_DEFAULTS } from './hand-tracker.js'; const PARAMS = [ { key: 'minHandDetectionConfidence', label: 'Detection confidence', min: 0.1, max: 1.0, step: 0.05, help: 'Palm detection threshold. Lower = more aggressive detection, more false positives.', }, { key: 'minHandPresenceConfidence', label: 'Presence confidence', min: 0.1, max: 1.0, step: 0.05, help: 'Hand presence threshold. Lower = holds onto tracked hands longer.', }, { key: 'minTrackingConfidence', label: 'Tracking confidence', min: 0.1, max: 1.0, step: 0.05, help: 'Frame-to-frame tracking IoU. Lower = tracks through fast movement.', }, { key: 'smoothingFactor', label: 'Smoothing', min: 0.05, max: 1.0, step: 0.05, help: 'Feature smoothing (EMA factor). Lower = smoother/laggier, higher = more responsive/jittery.', }, { key: 'gestureHoldMs', label: 'Gesture hold (ms)', min: 100, max: 1500, step: 50, help: 'How long a gesture must be held before firing.', }, { key: 'useWorldLandmarks', label: 'World landmarks', type: 'toggle', help: 'Use real-world meter coords (distance-independent) vs image-normalized coords.', }, ]; export function createDevPanel(handTrackerGetter) { if (new URLSearchParams(window.location.search).get('devmode') !== 'true') return null; const panel = document.createElement('div'); panel.className = 'dev-panel'; panel.innerHTML = `
Hand Tracking Dev
`; document.body.appendChild(panel); const $body = panel.querySelector('#dev-panel-body'); const $header = panel.querySelector('#dev-panel-header'); const $collapse = panel.querySelector('#dev-panel-collapse'); const sliders = {}; // Build controls for (const p of PARAMS) { const row = document.createElement('div'); row.className = 'dev-row'; if (p.type === 'toggle') { row.innerHTML = ` `; const input = row.querySelector('input'); input.addEventListener('change', () => { applyOption(p.key, input.checked); }); sliders[p.key] = input; } else { const defaultVal = HAND_TRACKER_DEFAULTS[p.key]; row.innerHTML = `
${formatVal(p.key, defaultVal)}
`; const input = row.querySelector('input[type=range]'); const display = row.querySelector('.dev-value'); input.addEventListener('input', () => { const val = parseFloat(input.value); display.textContent = formatVal(p.key, val); applyOption(p.key, val); }); sliders[p.key] = input; } $body.appendChild(row); } // Reset button const resetRow = document.createElement('div'); resetRow.className = 'dev-row dev-row-actions'; resetRow.innerHTML = ``; resetRow.querySelector('button').addEventListener('click', () => { for (const p of PARAMS) { const def = HAND_TRACKER_DEFAULTS[p.key]; if (p.type === 'toggle') { sliders[p.key].checked = def; } else { sliders[p.key].value = def; sliders[p.key].parentElement.querySelector('.dev-value').textContent = formatVal(p.key, def); } applyOption(p.key, def); } }); $body.appendChild(resetRow); // Feature monitor (live feature values) const monitorRow = document.createElement('div'); monitorRow.className = 'dev-row'; monitorRow.innerHTML = ` `; $body.appendChild(monitorRow); const $featureBars = monitorRow.querySelector('canvas'); const featureCtx = $featureBars.getContext('2d'); // --- Collapse --- let collapsed = false; $collapse.addEventListener('click', (e) => { e.stopPropagation(); collapsed = !collapsed; $body.style.display = collapsed ? 'none' : ''; $collapse.textContent = collapsed ? '+' : '_'; panel.classList.toggle('collapsed', collapsed); }); // --- Drag --- let dragging = false, dragOffX = 0, dragOffY = 0; $header.addEventListener('pointerdown', (e) => { if (e.target === $collapse) return; dragging = true; dragOffX = e.clientX - panel.offsetLeft; dragOffY = e.clientY - panel.offsetTop; $header.setPointerCapture(e.pointerId); e.preventDefault(); }); $header.addEventListener('pointermove', (e) => { if (!dragging) return; panel.style.left = (e.clientX - dragOffX) + 'px'; panel.style.top = (e.clientY - dragOffY) + 'px'; panel.style.right = 'auto'; panel.style.bottom = 'auto'; }); $header.addEventListener('pointerup', () => { dragging = false; }); // --- Apply option to hand tracker --- function applyOption(key, value) { const ht = handTrackerGetter(); if (ht) { ht.setOptions({ [key]: value }); } } // --- Feature bar animation --- const FEATURE_LABELS = [ 'pX','pY','tC','iC','mC','rC','pnC','s01','s12','s23','s34','rol','pit','pnc' ]; function drawFeatureBars() { const ht = handTrackerGetter(); const w = $featureBars.width; const h = $featureBars.height; featureCtx.clearRect(0, 0, w, h); if (!ht || !ht.active) { featureCtx.fillStyle = 'rgba(255,255,255,0.1)'; featureCtx.font = '9px monospace'; featureCtx.fillText('No tracking', 60, h / 2 + 3); requestAnimationFrame(drawFeatureBars); return; } const features = ht.features; const barW = (w - 2) / 14; for (let i = 0; i < 14; i++) { const v = features[i]; const x = i * barW + 1; const barH = v * (h - 12); // Bar background featureCtx.fillStyle = 'rgba(255,255,255,0.05)'; featureCtx.fillRect(x, 10, barW - 1, h - 12); // Bar fill featureCtx.fillStyle = `hsl(${(i / 14) * 360}, 70%, 55%)`; featureCtx.fillRect(x, h - barH, barW - 1, barH); // Label featureCtx.fillStyle = 'rgba(255,255,255,0.4)'; featureCtx.font = '6px monospace'; featureCtx.fillText(FEATURE_LABELS[i], x, 8); } requestAnimationFrame(drawFeatureBars); } drawFeatureBars(); return panel; } function formatVal(key, val) { if (key === 'gestureHoldMs') return `${val}`; return val.toFixed(2); }