← back to Dead Agentabrams
docs/TK-11101-perf-a11y.patch
114 lines
--- a/room/index.html 2026-09-02 08:59:47
+++ b/room/index.html 2026-09-02 09:03:51
@@ -237,6 +237,10 @@
}
@media (prefers-reduced-motion: reduce){
.museum,.chapters button,.btn,.skip{transition:none}
+ /* also silence the always-on keyframe loops (live-dot pulse, loading tiles) */
+ *,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important}
+ .musicroom.playing .live-dot{animation:none}
+ .mr-yr.loading{animation:none}
}
</style>
</head>
@@ -266,9 +270,9 @@
</header>
<div class="mrbody" id="mrbody">
<input class="mr-search" id="mrSearch" type="search" placeholder="🔍 Search the archive — date, city, song…" aria-label="Search the Grateful Dead archive">
- <div class="mr-src" id="mrSrc" role="tablist" aria-label="Choose a music source"></div>
+ <div class="mr-src" id="mrSrc" role="group" aria-label="Choose a music source"></div>
<div class="mr-map-head"><span id="mrMapLabel">Tap a year</span><button type="button" class="mr-allbtn on" id="mrAll">All years</button></div>
- <div class="mr-map" id="mrMap" role="listbox" aria-label="Browse shows by year"></div>
+ <div class="mr-map" id="mrMap" role="group" aria-label="Browse shows by year"></div>
<input type="hidden" id="mrYear" value="">
<div class="mr-results" id="mrResults" role="list"></div>
<div class="mr-picks">Curated nights:</div>
@@ -401,6 +405,18 @@
if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches){
S.reduced = true;
}
+// ...and keep honoring it if the visitor flips the OS setting mid-session
+if (window.matchMedia){
+ const _rmq = matchMedia('(prefers-reduced-motion: reduce)');
+ const _sync = e => {
+ S.reduced = e.matches;
+ const rb = document.getElementById('reduceBtn');
+ if (rb) rb.setAttribute('aria-pressed', String(S.reduced));
+ if (S.reduced) ctx.clearRect(0,0,S.W,S.H);
+ };
+ if (_rmq.addEventListener) _rmq.addEventListener('change', _sync);
+ else if (_rmq.addListener) _rmq.addListener(_sync);
+}
// ---------- Palettes + per-chapter choreography ----------
const CHAPTERS = [
@@ -1240,6 +1256,8 @@
const reacting = !!(window.SRM && window.SRM.react);
S.amp = reacting ? (window.SRM.level||0) : 0;
S.pulse = reacting ? (window.SRM.pulse||0) : 0; // sharp per-beat throb for lamps
+ // bridge visual state to the separate WebGL orb loop so it can honor reduced motion + pause
+ if(window.SRM){ window.SRM.reduced = S.reduced; window.SRM.playing = S.playing; }
// fire a burst on each new detected beat
if(reacting && !S.reduced && window.SRM.beat>_seenBeat){ _seenBeat=window.SRM.beat; spawnFirework(); }
else if(!reacting){ _seenBeat=window.SRM?(window.SRM.beat||0):0; } // let fireworks decay naturally
@@ -1619,6 +1637,8 @@
}
let _avg=0, _clock=0, _lastBeat=-99;
function sampleLevel(){
+ // stop the rAF loop when it has nothing to analyze; reactBtn / audio 'play' restart it
+ if(!window.SRM.react || audio.paused){ _looping=false; return; }
if(_analyser){
_analyser.getByteFrequencyData(_freq);
let sum=0, n=Math.max(1,Math.floor(_freq.length/6)); // low-end (bass) energy
@@ -1647,7 +1667,9 @@
announce(on?'Reacting to the music — lanterns, fireflies, and dancers now move to the live audio.':'Back to authored motion.');
});
-audio.addEventListener('play',()=>setPlay(true));
+audio.addEventListener('play',()=>{ setPlay(true);
+ if(window.SRM.react && _actx){ if(_actx.state==='suspended') _actx.resume(); if(!_looping){ _looping=true; sampleLevel(); } }
+});
audio.addEventListener('pause',()=>setPlay(false));
audio.addEventListener('ended',()=>{ if(trackIdx<tracks.length-1) playTrack(trackIdx+1); else setPlay(false); });
audio.addEventListener('timeupdate',()=>{
@@ -1707,7 +1729,7 @@
mapEl.innerHTML='';
YEARS.forEach(y=>{
const b=document.createElement('button'); b.type='button'; b.className='mr-yr loading';
- b.dataset.y=String(y); b.setAttribute('role','option'); b.title=y+' — counting…';
+ b.dataset.y=String(y); b.title=y+' — counting…';
b.innerHTML=`<span class="yl">${y}</span><span class="yc">…</span>`;
b.addEventListener('click',()=>setYear(y));
b.addEventListener('mouseenter',()=>{ const c=counts[ck(window.MR_SRC,y)]; if(c!=null) mapLabel.textContent=`${y} · ${c.toLocaleString()} shows`; });
@@ -1734,11 +1756,11 @@
if(srcEl){
srcEl.innerHTML=Object.entries(window.MR_SOURCES).map(([k,s])=>
- `<button type="button" data-src="${k}" class="${k===window.MR_SRC?'on':''}" role="tab" aria-selected="${k===window.MR_SRC}"><b>${s.label}</b><span class="yr">${s.years}</span></button>`).join('');
+ `<button type="button" data-src="${k}" class="${k===window.MR_SRC?'on':''}" aria-pressed="${k===window.MR_SRC}"><b>${s.label}</b><span class="yr">${s.years}</span></button>`).join('');
srcEl.addEventListener('click',e=>{
const btn=e.target.closest('button[data-src]'); if(!btn||btn.dataset.src===window.MR_SRC) return;
window.MR_SRC=btn.dataset.src;
- [...srcEl.children].forEach(c=>{ const on=c.dataset.src===window.MR_SRC; c.classList.toggle('on',on); c.setAttribute('aria-selected',String(on)); });
+ [...srcEl.children].forEach(c=>{ const on=c.dataset.src===window.MR_SRC; c.classList.toggle('on',on); c.setAttribute('aria-pressed',String(on)); });
yearEl.value=''; allBtn.classList.add('on'); mapLabel.textContent='Tap a year';
buildMap(); searchArchive(searchEl?searchEl.value:'');
});
@@ -1804,8 +1826,16 @@
scene.add(new THREE.AmbientLight(0x402028,0.9));
const col=new THREE.Color();
addEventListener('resize',()=>{ W=innerWidth;H=innerHeight; camera.aspect=W/H; camera.updateProjectionMatrix(); renderer.setSize(W,H,false); });
+ let _orbStatic=false;
function loop(){ requestAnimationFrame(loop);
const s=window.SRM||{}, t=performance.now()/1000;
+ // Honor reduced motion (WCAG 2.3.3): draw one calm static frame, then stop spinning + re-rendering.
+ if(s.reduced){
+ if(!_orbStatic){ group.rotation.set(Math.sin(t*0.3)*0.18,0,0); group.scale.setScalar(1);
+ core.material.emissiveIntensity=0.7; renderer.render(scene,camera); _orbStatic=true; }
+ return;
+ }
+ _orbStatic=false;
const pulse=s.pulse||0, level=(s.react?(s.level||0):0);
const idle=0.5+0.5*Math.sin(t*1.2); // gentle breathing when no music
const energy=s.react ? (level*0.6+pulse*0.7) : idle*0.12;