document.addEventListener('DOMContentLoaded', function(){ const stage = document.getElementById('stage'); const touches = new Map(); // id -> {el,x,y} let selecting = false; let selectionDone = false; let selectionTimer = null; function createFinger(t){ const id = (t.identifier !== undefined && t.identifier !== null) ? String(t.identifier) : ('m'+Date.now()); const el = document.createElement('div'); el.className = 'finger'; el.setAttribute('data-id', id); const label = document.createElement('div'); label.className = 'label'; label.textContent = '1'; el.appendChild(label); el.style.left = t.clientX + 'px'; el.style.top = t.clientY + 'px'; stage.appendChild(el); touches.set(id, {el: el, x: t.clientX, y: t.clientY}); updateLabels(); return id; } function updateFinger(t){ const id = (t.identifier !== undefined && t.identifier !== null) ? String(t.identifier) : 'm'; const entry = touches.get(id); if(entry){ entry.x = t.clientX; entry.y = t.clientY; entry.el.style.left = t.clientX + 'px'; entry.el.style.top = t.clientY + 'px'; } } function removeFingerById(id){ const key = String(id); const entry = touches.get(key); if(entry){ entry.el.remove(); touches.delete(key); updateLabels(); } } function updateLabels(){ let i=1; for(const entry of touches.values()){ const lbl = entry.el.querySelector('.label'); if(lbl) lbl.textContent = String(i++); } } function computeOrderedItems(){ const arr = Array.from(touches.entries()).map(([id,entry])=>({id,el:entry.el,x:entry.x,y:entry.y})); const cx = arr.reduce((s,a)=>s+a.x,0)/arr.length; const cy = arr.reduce((s,a)=>s+a.y,0)/arr.length; arr.forEach(a=>{ const ang = Math.atan2(a.y-cy,a.x-cx) * 180/Math.PI; // -180..180 a.angle = (ang+360)%360; // 0..360 a.dist = Math.hypot(a.x-cx,a.y-cy); }); arr.sort((a,b)=>a.angle-b.angle); return {items:arr, center:{x:cx,y:cy}}; } function startSelectionIfNeeded(){ if(selecting || selectionDone) return; if(touches.size < 1) return; // require at least 1 finger selecting = true; const {items} = computeOrderedItems(); const els = items.map(i=>i.el); const n = els.length; // If only one finger, immediately mark it as winner (short visual) if(n === 1){ const el = els[0]; el.classList.add('winner','pulse'); selecting = false; selectionDone = true; return; } const rotations = Math.floor(Math.random()*3)+3; // 3-5 rotations const winnerIndex = Math.floor(Math.random()*n); const totalSteps = rotations * n + winnerIndex; let cumulative = 0; let current = 0; for(let step=0; step<=totalSteps; step++){ const t = step/totalSteps; const delay = 40 + Math.pow(t,2)*520; // ease-out timing cumulative += delay; const idx = current % n; setTimeout(()=>{ els.forEach((el,i)=>{ if(i===idx) el.classList.add('highlight'); else el.classList.remove('highlight'); }); }, cumulative); current++; } setTimeout(()=>{ els.forEach((el,i)=>{ if(i===winnerIndex){ el.classList.remove('highlight'); el.classList.add('winner','pulse'); } else { el.classList.remove('highlight'); el.classList.add('lost'); } }); selecting = false; selectionDone = true; }, cumulative + 80); } function cancelSelection(){ clearTimeout(selectionTimer); selectionTimer = null; selecting = false; selectionDone = false; for(const entry of touches.values()){ entry.el.classList.remove('highlight','lost','winner','pulse'); entry.el.style.opacity = '1'; } } function resetWhenAllUp(){ if(touches.size===0){ setTimeout(()=>{ selecting=false; selectionDone=false; const nodes = Array.from(stage.querySelectorAll('.finger')); nodes.forEach(node=>node.remove()); touches.clear(); },160); } } // Touch handlers stage.addEventListener('touchstart', function(e){ e.preventDefault(); const changed = Array.from(e.changedTouches || []); changed.forEach(t=>createFinger(t)); if(!selectionDone){ clearTimeout(selectionTimer); selectionTimer = setTimeout(startSelectionIfNeeded, 700); } }, {passive:false}); stage.addEventListener('touchmove', function(e){ e.preventDefault(); const changed = Array.from(e.changedTouches || []); changed.forEach(t=>updateFinger(t)); }, {passive:false}); stage.addEventListener('touchend', function(e){ e.preventDefault(); const changed = Array.from(e.changedTouches || []); changed.forEach(t=>removeFingerById(t.identifier)); if(!selectionDone) cancelSelection(); if(touches.size===0) resetWhenAllUp(); }, {passive:false}); stage.addEventListener('touchcancel', function(e){ e.preventDefault(); const changed = Array.from(e.changedTouches || []); changed.forEach(t=>removeFingerById(t.identifier)); if(!selectionDone) cancelSelection(); if(touches.size===0) resetWhenAllUp(); }, {passive:false}); // Support mouse for desktop testing let mouseDown = false; const mouseId = 'mouse'; stage.addEventListener('mousedown', function(e){ mouseDown = true; createFinger({identifier:mouseId, clientX:e.clientX, clientY:e.clientY}); cancelSelection(); startSelectionIfNeeded(); }); window.addEventListener('mousemove', function(e){ if(!mouseDown) return; updateFinger({identifier:mouseId, clientX:e.clientX, clientY:e.clientY}); }); window.addEventListener('mouseup', function(e){ if(!mouseDown) return; mouseDown=false; removeFingerById(mouseId); if(!selectionDone) cancelSelection(); if(touches.size===0) resetWhenAllUp(); }); });