/* widget: Ademhalingsoefening */ /* ========================================================= Rustgevende ademhaling met een groeiende/krimpende cirkel, drie tempo's. De cirkelgrootte wordt via inline style/transition-duration gestuurd (niet via CSS-klassen) zodat elke fase precies aansluit op de vorige zonder terugspringen. ==========================================================*/ const BREATHE_PATTERNS = { "4-4": [["in",4],["out",4]], "4-7-8": [["in",4],["hold",7],["out",8]], "4-4-4-4": [["in",4],["hold",4],["out",4],["hold",4]] }; const BREATHE_SCALE = { in: 1.4, hold: null, out: .7 }; function mountBreathe(root, initState){ let pattern = (initState && BREATHE_PATTERNS[initState.pattern]) ? initState.pattern : "4-4"; let running = false, phase = -1, phaseStart = 0, rafId = null, rounds = 0; root.innerHTML = `

`; const circle = root.querySelector(".bre-circle"), word = root.querySelector(".bre-word"), count = root.querySelector(".bre-count"), roundsEl = root.querySelector(".bre-rounds"), patternSel = root.querySelector(".bre-pattern"), toggle = root.querySelector(".bre-toggle"); Object.keys(BREATHE_PATTERNS).forEach(k=>patternSel.appendChild(new Option("", k))); patternSel.value = pattern; function updateRounds(){ roundsEl.textContent = T("breRounds").replace("{n}", rounds); } function enterPhase(index){ const [name, dur] = BREATHE_PATTERNS[pattern][index]; circle.style.transitionDuration = dur + "s"; circle.classList.toggle("in", name === "in"); circle.classList.toggle("out", name === "out"); if(BREATHE_SCALE[name] !== null) circle.style.transform = "scale(" + BREATHE_SCALE[name] + ")"; word.textContent = T("bre_" + name); } function step(ts){ if(!running || !document.body.contains(root)) return; const steps = BREATHE_PATTERNS[pattern]; if(phase < 0){ phase = 0; phaseStart = ts; enterPhase(phase); } const [, dur] = steps[phase]; const elapsed = (ts - phaseStart) / 1000; count.textContent = Math.max(0, Math.ceil(dur - elapsed)) || ""; if(elapsed >= dur){ phase = (phase + 1) % steps.length; phaseStart = ts; if(phase === 0){ rounds++; updateRounds(); } enterPhase(phase); } rafId = requestAnimationFrame(step); } function start(){ running = true; phase = -1; rounds = 0; updateRounds(); labels(); rafId = requestAnimationFrame(step); } function stop(){ running = false; if(rafId) cancelAnimationFrame(rafId); rafId = null; circle.classList.remove("in","out"); circle.style.transform = ""; circle.style.transitionDuration = ""; word.textContent = ""; count.textContent = ""; labels(); } toggle.addEventListener("click", ()=>{ if(running) stop(); else start(); }); patternSel.addEventListener("change", ()=>{ pattern = patternSel.value; if(running) stop(); }); function labels(){ root.querySelector(".bre-mission").textContent = T("wg_breathe_d"); toggle.textContent = running ? "⏸ " + T("breStop") : "🌬️ " + T("breStart"); [...patternSel.options].forEach(o=>{ o.textContent = T("brePattern_" + o.value); }); updateRounds(); } document.addEventListener("langchange", labels); labels(); return { getState: ()=>({ pattern }) }; }