All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 36s
Bij veel widgets nam de instellingenstrook te veel hoogte in, vooral bij een smal widgetvenster waar de rij naar meerdere regels uitwaaierde. Nieuw gedeeld patroon: besturing die voortdurend nodig is (kleur, niveau, woordbron) blijft in één compacte rij zichtbaar; besturing die maar zelden wordt aangepast (thema, geluid aan/uit, invoerwijze, bereik) gaat achter een inklapbaar ⚙-paneel. Dit patroon bestond al voor klok/geld/rekengetallen/ wereld (learningShell() in learning.js) - nu als widgetGear()/gearField() in core.js beschikbaar voor de rest van de app, met dezelfde .edu-settings- opmaak zodat het overal hetzelfde oogt en werkt. - kleurplaten: twee volle rijen (thema/plaat + kwast/emmer/gum/kleur/dikte/ hulp/reset/klaar) teruggebracht naar één vaste rij (gereedschap, kleur, dikte, klaar) plus het ingeklapte paneel (thema, plaat, kleurhulp, reset). Losstaand: coloring.css gebruikte @media(max-width:620px) in plaats van de @container widget-conventie die de rest van de app gebruikt, waardoor die regels nooit afgingen bij het verkleinen van het widgetvenster zelf - nu gecorrigeerd. - letterblokken: geluid, invoerwijze en aantal kaarten in het paneel; niveau en woordbron blijven vast. - rekenwidgets (optellen/aftrekken/keer/delen/procenten): geluid en invoerwijze in het paneel; niveau blijft vast. - ballonnenwoord: geluid in het paneel. - flitswoorden: flitstijd in het paneel. - getallenlijn: het hele bereik-instelblok (van/tot/stap/nieuw) in het paneel - dat stond toch al uit voor leerlingen met een beheerd niveau. - zinsbouwer: het rijtje voor eigen woorden toevoegen in het paneel; de speelknoppen (verrassing/ongedaan/wissen/voorlezen) blijven vast. - timer: geluid en weergavevorm in het paneel; starttijden en start/reset blijven vast. - geluidsmeter: de twee drempelschuifjes plus de privacynotitie in het paneel. Widgets die dit patroon al hadden (klok, geld, breuken, plaatswaarde, woordsoorten, bomen/vogels/planten, topografie, stemming, stemming-poll, dagplanning) of waar de rij al compact genoeg was (schrijfblad, hakken en plakken, dobbelstenen, notities, naamtrekker) zijn ongewijzigd gelaten. Geverifieerd door elke aangepaste widget live via de echte makeWidget()/ REGISTRY in een headless browser te openen, het paneel open en dicht te klappen en te controleren op consolefouten - ook op de minimale widgetbreedte (coloring, minW 390). 202 tests groen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011YErxhic5w9K57zmt8Q1uS
128 lines
5.2 KiB
JavaScript
128 lines
5.2 KiB
JavaScript
/* widget: Geluidsmeter */
|
|
/* =========================================================
|
|
Live noise meter — mic input stays local, never recorded or sent anywhere
|
|
==========================================================*/
|
|
function mountNoise(root, initState){
|
|
let zoneGreen = (initState && initState.zoneGreen) || 40;
|
|
let zoneYellow = (initState && initState.zoneYellow) || 70;
|
|
let running = false, audioCtx = null, analyser = null, stream = null, rafId = null, warnTimer = null;
|
|
let level = 0, peak = 0, timeData = null;
|
|
|
|
root.innerHTML = `
|
|
<div class="nm">
|
|
<p class="play-mission nm-mission"></p>
|
|
<div class="nm-stage">
|
|
<div class="nm-gauge">
|
|
<div class="nm-fill"></div>
|
|
<div class="nm-mark green"></div>
|
|
<div class="nm-mark yellow"></div>
|
|
</div>
|
|
<div class="nm-face"></div>
|
|
</div>
|
|
<output class="nm-status" aria-live="polite"></output>
|
|
<button class="tbtn nm-toggle"></button>
|
|
<details class="edu-settings widget-gear"><summary>⚙ <span class="nm-settings-label"></span></summary>
|
|
<div class="edu-controls">
|
|
<div class="nm-zones">
|
|
<label class="nm-zone-row"><span class="nm-zone-lbl"></span><input type="range" class="nm-z1" min="10" max="90" step="5"></label>
|
|
<label class="nm-zone-row"><span class="nm-zone-lbl"></span><input type="range" class="nm-z2" min="10" max="95" step="5"></label>
|
|
</div>
|
|
<p class="nm-privacy"></p>
|
|
</div></details>
|
|
</div>`;
|
|
const gauge = root.querySelector(".nm-gauge"),
|
|
fill = root.querySelector(".nm-fill"),
|
|
markGreen = root.querySelector(".nm-mark.green"),
|
|
markYellow = root.querySelector(".nm-mark.yellow"),
|
|
face = root.querySelector(".nm-face"),
|
|
status = root.querySelector(".nm-status"),
|
|
toggle = root.querySelector(".nm-toggle"),
|
|
z1 = root.querySelector(".nm-z1"),
|
|
z2 = root.querySelector(".nm-z2");
|
|
z1.value = zoneGreen;
|
|
z2.value = zoneYellow;
|
|
|
|
function currentZone(){ return level < zoneGreen ? "green" : level < zoneYellow ? "yellow" : "red"; }
|
|
function draw(){
|
|
fill.style.height = Math.round(level) + "%";
|
|
fill.className = "nm-fill " + currentZone();
|
|
markGreen.style.bottom = zoneGreen + "%";
|
|
markYellow.style.bottom = zoneYellow + "%";
|
|
face.textContent = !running ? "🎙️" : currentZone() === "green" ? "🙂" : currentZone() === "yellow" ? "😐" : "😬";
|
|
status.textContent = !running ? T("noiseOff")
|
|
: T("noiseStatus_" + currentZone());
|
|
gauge.classList.toggle("warn", running && currentZone() === "red");
|
|
}
|
|
function loop(){
|
|
if(!running || !document.body.contains(root)){ stop(); return; }
|
|
analyser.getByteTimeDomainData(timeData);
|
|
let sum = 0;
|
|
for(let i = 0; i < timeData.length; i++){ const v = (timeData[i] - 128) / 128; sum += v * v; }
|
|
const rms = Math.sqrt(sum / timeData.length);
|
|
const pct = Math.min(100, rms * 100 * 4.2);
|
|
level = level * 0.75 + pct * 0.25;
|
|
peak = Math.max(peak * 0.995, level);
|
|
draw();
|
|
rafId = requestAnimationFrame(loop);
|
|
}
|
|
async function start(){
|
|
try{
|
|
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
}catch(e){
|
|
status.textContent = T("noiseDenied");
|
|
return;
|
|
}
|
|
audioCtx = audioCtx || new (window.AudioContext || window.webkitAudioContext)();
|
|
if(audioCtx.state === "suspended") await audioCtx.resume().catch(()=>{});
|
|
const source = audioCtx.createMediaStreamSource(stream);
|
|
analyser = audioCtx.createAnalyser();
|
|
analyser.fftSize = 1024;
|
|
timeData = new Uint8Array(analyser.fftSize);
|
|
source.connect(analyser);
|
|
running = true; level = 0; peak = 0;
|
|
labels();
|
|
loop();
|
|
}
|
|
function stop(){
|
|
running = false;
|
|
if(rafId) cancelAnimationFrame(rafId);
|
|
rafId = null;
|
|
if(stream){ stream.getTracks().forEach(t => t.stop()); stream = null; }
|
|
analyser = null;
|
|
level = 0;
|
|
labels();
|
|
draw();
|
|
}
|
|
toggle.addEventListener("click", ()=>{ if(running) stop(); else start(); });
|
|
z1.addEventListener("input", ()=>{
|
|
zoneGreen = Math.min(+z1.value, +z2.value - 5);
|
|
z1.value = zoneGreen;
|
|
draw();
|
|
});
|
|
z2.addEventListener("input", ()=>{
|
|
zoneYellow = Math.max(+z2.value, zoneGreen + 5);
|
|
z2.value = zoneYellow;
|
|
draw();
|
|
});
|
|
/* geluid over rood laten lopen is een stoorzender op zich - een korte,
|
|
zachte waarschuwing volstaat, geen herhaald alarm */
|
|
const watchWarn = ()=>{
|
|
clearInterval(warnTimer);
|
|
warnTimer = setInterval(()=>{
|
|
if(running && currentZone() === "red") sndBad();
|
|
}, 6000);
|
|
};
|
|
watchWarn();
|
|
function labels(){
|
|
root.querySelector(".nm-mission").textContent = T("wg_noise_d");
|
|
root.querySelector(".nm-settings-label").textContent = T("widgetSettings");
|
|
toggle.textContent = running ? "⏸ " + T("noiseStop") : "🎙️ " + T("noiseStart");
|
|
root.querySelectorAll(".nm-zone-lbl")[0].textContent = T("noiseZoneGreen");
|
|
root.querySelectorAll(".nm-zone-lbl")[1].textContent = T("noiseZoneYellow");
|
|
root.querySelector(".nm-privacy").textContent = T("noisePrivacy");
|
|
draw();
|
|
}
|
|
document.addEventListener("langchange", labels);
|
|
labels();
|
|
return { getState: ()=>({ zoneGreen, zoneYellow }) };
|
|
}
|