All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 36s
- Geluidsmeter: meet het klasgeluid live via de microfoon in een verticale
meter (rustig/pratend/te luid). Twee aanpasbare streefniveaus ("Rustig
tot" / "Te luid vanaf") laten de leerkracht de meter afstemmen op het
eigen lokaal en apparaat. Microfoon staat standaard uit en start alleen
na een expliciete klik; er wordt niets opgenomen of verstuurd, alles
blijft lokaal in de browser. Stopt de microfoonstream vanzelf zodra de
widget van het bord verdwijnt, ook als "Stop" niet eerst is ingedrukt.
- Gevoelszones: individuele check-in met vier zones (blauw/groen/geel/
rood, Zones of Regulation-stijl) - elke zone geeft een korte uitleg én
een concrete tip om jezelf te helpen. Anders dan de bestaande
Gevoelsmeter (mood.js), die een anonieme klaspoll is.
- Ademhalingsoefening: een groeiende/krimpende cirkel begeleidt rustig
ademhalen in drie instelbare tempo's (4-4, 4-7-8, box breathing),
met rondjesteller.
- Complimentenmachine: trekt een willekeurig compliment uit twintig
kindvriendelijke complimenten, met dezelfde spin-en-onthul-animatie als
de namenkiezer.
- Alle vier volledig tweetalig (nl/en), toegevoegd aan de mobiele-dekking-
test, en geverifieerd in de browser inclusief een echte (fake) micro-
foonstream voor de geluidsmeter.
81 lines
3.6 KiB
JavaScript
81 lines
3.6 KiB
JavaScript
/* 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 = `
|
|
<div class="bre">
|
|
<p class="play-mission bre-mission"></p>
|
|
<div class="bre-stage">
|
|
<div class="bre-circle"><span class="bre-word"></span><span class="bre-count"></span></div>
|
|
</div>
|
|
<output class="bre-rounds" aria-live="polite"></output>
|
|
<div class="bre-row">
|
|
<select class="bre-pattern"></select>
|
|
<button class="tbtn bre-toggle"></button>
|
|
</div>
|
|
</div>`;
|
|
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 }) };
|
|
}
|