← back to Dead Agentabrams
room(TK-11101): apply perf+a11y fixes (Steve-approved deploy)
ebd5538946c3e240936887d0eaa2fc348f26c39e · 2026-09-02 09:19:05 -0700 · Steve Abrams
Files touched
Diff
commit ebd5538946c3e240936887d0eaa2fc348f26c39e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 2 09:19:05 2026 -0700
room(TK-11101): apply perf+a11y fixes (Steve-approved deploy)
---
room/index.html | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 85 insertions(+), 8 deletions(-)
diff --git a/room/index.html b/room/index.html
index dbd0271..ffa3b1c 100644
--- a/room/index.html
+++ b/room/index.html
@@ -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 @@ const CHAPTER_SECONDS = 55; // dwell before the boat drifts to the next chapter
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 @@ function frame(now){
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 @@ function ensureGraph(){
}
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 @@ if(reactBtn) reactBtn.addEventListener('click',()=>{
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 @@ function buildMap(){
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 @@ function buildMap(){
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:'');
});
@@ -1773,8 +1795,55 @@ mrToggle.addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.pre
// Music Room stays EXPANDED (incl. small screens) so revealing the chrome shows a
// working player in one tap — no hidden second collapse (CTA/screenrecord CRITICAL #1).
-// cue the default night (no autoplay — the source policy is user-initiated playback)
-loadShow(DEFAULT_SHOW);
+// ---- Autostart on load: queue "Me and My Uncle → Bertha → Deal" and play ----
+const WISHLIST=['Me and My Uncle','Bertha','Deal'];
+function _norm(t){ return (t||'').toLowerCase().replace(/&/g,' and ').replace(/[^a-z0-9 ]+/g,' ').replace(/\bthe\b/g,' ').replace(/\s+/g,' ').trim(); }
+function _matches(title,want){ const n=_norm(title), w=_norm(want); if(!n||!w) return false;
+ return new RegExp('\\b'+w.replace(/[.*+?^${}()|[\]\\]/g,'\\$&').replace(/\s+/g,'\\s+')+'\\b').test(n); }
+// fan out to the curated shows in parallel, pool every mp3 track (title + direct url)
+async function _poolFromShows(){
+ const results=await Promise.allSettled(CATALOG.map(s=>
+ fetch(`https://archive.org/metadata/${s.id}`,{mode:'cors'}).then(r=>r.ok?r.json():null).then(j=>({s,j}))));
+ const pool=[];
+ for(const res of results){
+ if(res.status!=='fulfilled' || !res.value || !res.value.j) continue;
+ const s=res.value.s, j=res.value.j;
+ let mp3=(j.files||[]).filter(f=>/vbr mp3/i.test(f.format||'')); if(!mp3.length) mp3=(j.files||[]).filter(f=>/mp3/i.test(f.format||''));
+ mp3.forEach(f=>pool.push({ title:(f.title&&f.title.trim())?f.title.trim():f.name.replace(/\.mp3$/i,''),
+ url:`https://archive.org/download/${s.id}/${encodeURIComponent(f.name)}`, show:s }));
+ }
+ return pool;
+}
+// browsers block silent autoplay — try, and if blocked, start on the first tap/keypress
+function _tryAutoplay(){
+ const go=()=>audio.play().then(()=>setPlay(true)).catch(()=>{});
+ audio.play().then(()=>setPlay(true)).catch(()=>{
+ const unlock=()=>{ go(); document.removeEventListener('pointerdown',unlock,true); document.removeEventListener('keydown',unlock,true); };
+ document.addEventListener('pointerdown',unlock,true);
+ document.addEventListener('keydown',unlock,true);
+ announce('Tap or press any key to start the music — Me and My Uncle, Bertha, Deal.');
+ });
+}
+async function bootMusic(){
+ $('mrNow').textContent='Cueing the set…';
+ try{
+ const pool=await _poolFromShows();
+ const list=[]; for(const w of WISHLIST){ const hit=pool.find(p=>_matches(p.title,w)); if(hit) list.push(hit); }
+ if(list.length){
+ tracks=list; trackIdx=0; showIdx=-1; renderTracks(); highlight();
+ audio.src=tracks[0].url; if(window.SRM) window.SRM.track=tracks[0].title;
+ $('mrNow').textContent=`♪ ${tracks[0].title}`;
+ $('mrSub').textContent='Autostart — '+list.map(t=>t.title).join(' · ');
+ setFoot(list[0].show);
+ announce('Autostarting: '+list.map(t=>t.title).join(', ')+'.');
+ _tryAutoplay();
+ return;
+ }
+ }catch(e){}
+ await loadShow(DEFAULT_SHOW); // fallback: archive slow/down → cue the default night, still try to start
+ _tryAutoplay();
+}
+bootMusic();
// reveal the DEAD back-link only when this room is served as part of the exhibition
const _p=location.pathname.replace(/\/(index\.html)?$/,'');
@@ -1804,8 +1873,16 @@ if(_p.endsWith('/room') || /dead\.agentabrams/i.test(location.hostname)){
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;
← 062e20e docs(TK-11101): perf+a11y audit of room/index.html — finding
·
back to Dead Agentabrams
·
room: re-apply bears+slowdown+18-show catalog (atomic, race- ab66c92 →