[object Object]

← 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 →