All checks were successful
Deploy via FTP / deploy (push) Successful in 4s
195 lines
6.8 KiB
JavaScript
195 lines
6.8 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function(){
|
|
const stage = document.getElementById('stage');
|
|
const touches = new Map(); // id -> {el,x,y,colorIdx}
|
|
const MAX_TOUCHES = /iPhone/.test(navigator.userAgent) ? 5 : 20;
|
|
const COLOR_CLASSES = ['c1','c2','c3','c4','c5','c6','c7','c8'];
|
|
const usedColors = new Set();
|
|
|
|
function pickColor(){
|
|
for(let i=0; i<COLOR_CLASSES.length; i++){
|
|
if(!usedColors.has(i)){ usedColors.add(i); return i; }
|
|
}
|
|
return 0;
|
|
}
|
|
function releaseColor(idx){ usedColors.delete(idx); }
|
|
let selecting = false;
|
|
let selectionDone = false;
|
|
let selectionTimer = null;
|
|
let selectionEpoch = 0;
|
|
|
|
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';
|
|
const colorIdx = pickColor();
|
|
el.classList.add(COLOR_CLASSES[colorIdx]);
|
|
stage.appendChild(el);
|
|
touches.set(id, {el: el, x: t.clientX, y: t.clientY, colorIdx: colorIdx});
|
|
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(); releaseColor(entry.colorIdx); 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;
|
|
selecting = true;
|
|
const epoch = selectionEpoch;
|
|
|
|
const {items} = computeOrderedItems();
|
|
const els = items.map(i=>i.el);
|
|
const n = els.length;
|
|
|
|
if(n === 1){
|
|
if(epoch !== selectionEpoch) return;
|
|
const el = els[0];
|
|
el.classList.add('winner','pulse');
|
|
if(typeof umami !== 'undefined') umami.track('winner-selected', { fingers: 1 });
|
|
selecting = false;
|
|
selectionDone = true;
|
|
return;
|
|
}
|
|
|
|
const rotations = Math.floor(Math.random()*3)+3;
|
|
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;
|
|
cumulative += delay;
|
|
const idx = current % n;
|
|
setTimeout(()=>{
|
|
if(epoch !== selectionEpoch) return;
|
|
els.forEach((el,i)=>{
|
|
if(i===idx) el.classList.add('highlight'); else el.classList.remove('highlight');
|
|
});
|
|
}, cumulative);
|
|
current++;
|
|
}
|
|
|
|
setTimeout(()=>{
|
|
if(epoch !== selectionEpoch) return;
|
|
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'); }
|
|
});
|
|
if(typeof umami !== 'undefined') umami.track('winner-selected', { fingers: n });
|
|
selecting = false;
|
|
selectionDone = true;
|
|
}, cumulative + 80);
|
|
}
|
|
|
|
function cancelSelection(){
|
|
selectionEpoch++;
|
|
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(()=>{
|
|
if(touches.size > 0) return;
|
|
selecting=false; selectionDone=false;
|
|
const nodes = Array.from(stage.querySelectorAll('.finger'));
|
|
nodes.forEach(node=>node.remove());
|
|
touches.clear();
|
|
usedColors.clear();
|
|
},160);
|
|
}
|
|
}
|
|
|
|
// Touch handlers
|
|
stage.addEventListener('touchstart', function(e){
|
|
e.preventDefault();
|
|
const changed = Array.from(e.changedTouches || []);
|
|
const prevSize = touches.size;
|
|
changed.forEach(t=>{ if(touches.size < MAX_TOUCHES) createFinger(t); });
|
|
if(touches.size > prevSize){
|
|
cancelSelection();
|
|
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));
|
|
cancelSelection();
|
|
if(touches.size > 0){
|
|
selectionTimer = setTimeout(startSelectionIfNeeded, 700);
|
|
}
|
|
// touches.size===0: cancelSelection hat State bereits bereinigt,
|
|
// DOM-Elemente durch removeFingerById entfernt.
|
|
// iOS feuert neue touchstart-Events für noch liegende Finger selbst.
|
|
}, {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(); });
|
|
|
|
});
|