/* widget: Timer */ /* ========================================================= Timer widget — digits, visual "kijkklok" disc, or both ==========================================================*/ function mountTimer(root, initState){ const DIAL = 3600; // the disc is a 60-minute dial, like a real kijkklok let total = 300, left = 300, iv = null, mode = "both", // dig | pie | both snd = true; if(initState){ total = initState.total || 300; left = total; mode = initState.mode || "both"; snd = initState.snd !== false; } root.innerHTML = `
05:00
`; const box = root.querySelector(".timer"), svg = root.querySelector(".tpie"), disp = root.querySelector(".tdisp"), arc = root.querySelector(".parc"), knob = root.querySelector(".knob"), bstart = root.querySelector(".bstart"), breset = root.querySelector(".breset"), bsnd = root.querySelector(".bsnd"), presets = root.querySelector(".presets"), hint = root.querySelector(".thint"); const fmt = s => String(Math.floor(s/60)).padStart(2,"0")+":"+String(s%60).padStart(2,"0"); /* minute tick marks (every 5 min) */ const ticks = root.querySelector(".ticks"); for(let i=0;i<12;i++){ const a = i*30*Math.PI/180; const l = document.createElementNS("http://www.w3.org/2000/svg","line"); l.setAttribute("x1", 75+62*Math.sin(a)); l.setAttribute("y1", 75-62*Math.cos(a)); l.setAttribute("x2", 75+69*Math.sin(a)); l.setAttribute("y2", 75-69*Math.cos(a)); l.setAttribute("stroke", "#b9c7d9"); l.setAttribute("stroke-width", i%3===0?2.5:1.5); ticks.appendChild(l); } function drawPie(){ const a = Math.min(360, left/DIAL*360); const rad = a*Math.PI/180; const kx = 75 + 63*Math.sin(rad), ky = 75 - 63*Math.cos(rad); knob.setAttribute("cx", kx.toFixed(2)); knob.setAttribute("cy", ky.toFixed(2)); if(a<=0){ arc.setAttribute("d",""); return; } if(a>=359.99){ arc.setAttribute("d","M75,5 A70,70 0 1 1 74.99,5 Z"); return; } const x = 75 + 70*Math.sin(rad), y = 75 - 70*Math.cos(rad); arc.setAttribute("d",`M75,75 L75,5 A70,70 0 ${a>180?1:0} 1 ${x.toFixed(2)},${y.toFixed(2)} Z`); } function draw(){ disp.textContent = fmt(left); disp.classList.toggle("warn", left<=10 && left>0 && iv!==null); drawPie(); } function labels(){ bstart.textContent = (iv ? "⏸ "+T("pause") : "▶ "+T("start")); breset.textContent = "↺ "+T("reset"); presets.querySelectorAll("button").forEach(b=>b.textContent = b.dataset.m+" "+T("min")); root.querySelectorAll(".tseg button").forEach(b=>{ const key = b.dataset.m==="dig" ? "tmDigits" : b.dataset.m==="pie" ? "tmVisual" : "tmBoth"; b.title = T(key); b.classList.toggle("on", b.dataset.m===mode); }); bsnd.textContent = snd ? "🔔" : "🔕"; bsnd.title = T("tmSound"); bsnd.classList.toggle("on", snd); box.className = "timer m-"+mode + (box.classList.contains("flash")?" flash":""); hint.textContent = mode==="dig" ? "" : T("tmHint"); } [1,5,10,15,30].forEach(m=>{ const b = document.createElement("button"); b.dataset.m = m; b.addEventListener("click", ()=>{ stop(); total=left=m*60; draw(); labels(); }); presets.appendChild(b); }); root.querySelectorAll(".tseg button").forEach(b=>{ b.addEventListener("click", ()=>{ mode = b.dataset.m; labels(); }); }); bsnd.addEventListener("click", ()=>{ snd=!snd; labels(); }); /* drag on the disc to set the time */ function angleFromEvent(ev){ const r = svg.getBoundingClientRect(); const px = (ev.clientX - r.left)*150/r.width - 75; const py = (ev.clientY - r.top)*150/r.height - 75; return (Math.atan2(px, -py)*180/Math.PI + 360) % 360; } svg.addEventListener("pointerdown", e=>{ e.preventDefault(); svg.setPointerCapture(e.pointerId); const setFrom = ev=>{ const mins = Math.min(60, Math.max(1, Math.round(angleFromEvent(ev)/6))); total = left = mins*60; draw(); }; setFrom(e); const move = ev=>setFrom(ev); const up = ()=>{ svg.removeEventListener("pointermove",move); svg.removeEventListener("pointerup",up); }; svg.addEventListener("pointermove",move); svg.addEventListener("pointerup",up); }); function stop(){ if(iv){ clearInterval(iv); iv=null; } } bstart.addEventListener("click", ()=>{ if(iv){ stop(); } else if(left>0){ iv = setInterval(()=>{ left--; if(left<=0){ left=0; stop(); if(snd) sndWin(); box.classList.add("flash"); setTimeout(()=>box.classList.remove("flash"), 2200); labels(); } draw(); },1000); } labels(); draw(); }); breset.addEventListener("click", ()=>{ stop(); left=total; draw(); labels(); }); document.addEventListener("langchange", labels); labels(); draw(); return { getState: ()=>({total, mode, snd}) }; }