← back to Dead Agentabrams
room: tappable year-tile grid + Grateful Dead/Dead & Company source selector (UX-review fixes)
0300e1cb258fcbac468f5f9c4b9d1ee40983a546 · 2026-09-02 08:40:01 -0700 · Steve Abrams
Files touched
M room/index.htmlM scripts/screenrecord-5pass.js
Diff
commit 0300e1cb258fcbac468f5f9c4b9d1ee40983a546
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 2 08:40:01 2026 -0700
room: tappable year-tile grid + Grateful Dead/Dead & Company source selector (UX-review fixes)
---
room/index.html | 468 ++++++++------------------
scripts/screenrecord-5pass.js | 747 +++++++++++++++++++++---------------------
2 files changed, 510 insertions(+), 705 deletions(-)
diff --git a/room/index.html b/room/index.html
index 15f41e5..fa6af65 100644
--- a/room/index.html
+++ b/room/index.html
@@ -154,18 +154,29 @@
.mr-search{width:100%;background:rgba(40,12,16,.8);color:var(--ink);border:1px solid var(--panel-edge);border-radius:9px;padding:8px 10px;font-size:12px;margin-bottom:8px}
.mr-search::placeholder{color:var(--ink-dim)}
.mr-year{width:100%;background:rgba(40,12,16,.8);color:var(--ink);border:1px solid var(--panel-edge);border-radius:9px;padding:7px 9px;font-size:12px;margin-bottom:8px;cursor:pointer}
- /* interactive touring-history year map */
- .mr-map-head{display:flex;justify-content:space-between;align-items:center;font-size:10px;color:var(--ink-dim);margin:2px 0 3px;gap:8px}
+ /* source selector — which free Dead archive to browse */
+ .mr-src{display:flex;gap:6px;margin-bottom:9px}
+ .mr-src button{flex:1;background:rgba(40,12,16,.7);color:var(--ink-dim);border:1px solid var(--panel-edge);border-radius:9px;padding:8px 8px;cursor:pointer;line-height:1.25;text-align:center;font-family:var(--sans)}
+ .mr-src button b{display:block;font-size:12px;color:var(--ink);font-weight:600}
+ .mr-src button .yr{display:block;font-size:9px;color:var(--ink-dim);letter-spacing:.02em;margin-top:1px}
+ .mr-src button:hover{background:rgba(70,20,26,.85)}
+ .mr-src button.on{background:linear-gradient(180deg,#ffd97a,var(--gold));border-color:var(--gold)}
+ .mr-src button.on b,.mr-src button.on .yr{color:#2a0d10}
+ /* tappable touring-history year map — wrapping tile grid (WCAG-sized targets) */
+ .mr-map-head{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--ink-dim);margin:2px 0 5px;gap:8px}
#mrMapLabel{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
- .mr-allbtn{flex:none;background:rgba(70,20,26,.6);color:var(--ink);border:1px solid var(--panel-edge);border-radius:6px;font-size:10px;padding:2px 8px;cursor:pointer}
+ .mr-allbtn{flex:none;background:rgba(70,20,26,.6);color:var(--ink);border:1px solid var(--panel-edge);border-radius:8px;font-size:11px;padding:6px 12px;min-height:32px;cursor:pointer}
.mr-allbtn.on{background:var(--gold);color:#2a0d10;border-color:var(--gold)}
- .mr-map{display:flex;align-items:flex-end;gap:2px;height:60px;overflow-x:auto;overflow-y:hidden;padding:3px 2px 0;margin-bottom:8px;border-bottom:1px solid var(--panel-edge);scrollbar-width:thin}
- .mr-yr{flex:0 0 auto;width:11px;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;height:100%;cursor:pointer;background:none;border:0;padding:0}
- .mr-yr .bar{width:8px;min-height:3px;border-radius:2px 2px 0 0;background:linear-gradient(180deg,var(--gold),#7a2a1e);opacity:.5;transition:opacity .15s,box-shadow .15s}
- .mr-yr:hover .bar{opacity:.9}
- .mr-yr.on .bar{opacity:1;background:linear-gradient(180deg,#ffd97a,var(--gold));box-shadow:0 0 8px rgba(240,190,90,.85)}
- .mr-yr .yl{font-size:7px;line-height:1;color:var(--ink-dim);margin-top:2px;writing-mode:vertical-rl;transform:rotate(180deg);font-variant-numeric:tabular-nums;letter-spacing:-.5px;height:14px}
- .mr-yr.on .yl{color:var(--gold)}
+ .mr-map{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:9px}
+ .mr-yr{flex:1 1 46px;min-width:46px;min-height:44px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;padding:5px 3px;cursor:pointer;color:var(--ink);border:1px solid var(--panel-edge);border-radius:8px;background:rgba(90,30,20,calc(.12 + var(--heat,0) * .55));transition:transform .1s,box-shadow .15s,background .15s}
+ .mr-yr:hover{transform:translateY(-1px);box-shadow:0 3px 8px rgba(0,0,0,.35)}
+ .mr-yr.on{background:linear-gradient(180deg,#ffd97a,var(--gold));border-color:var(--gold);color:#2a0d10;box-shadow:0 0 10px rgba(240,190,90,.6)}
+ .mr-yr .yl{font-size:12px;font-weight:600;line-height:1;font-variant-numeric:tabular-nums}
+ .mr-yr .yc{font-size:8.5px;color:var(--ink-dim);letter-spacing:.02em;line-height:1}
+ .mr-yr.on .yc{color:#6a2c12}
+ .mr-yr.loading{animation:mrPulse 1s ease-in-out infinite}
+ @keyframes mrPulse{0%,100%{opacity:.4}50%{opacity:.8}}
+ .mr-map-err{font-size:11px;color:#e2a08a;padding:6px 4px;font-style:italic}
.mr-results{max-height:150px;overflow:auto;margin-bottom:8px;display:flex;flex-direction:column;gap:2px}
.mr-results:empty{display:none}
.mr-result{display:flex;gap:9px;align-items:baseline;width:100%;text-align:left;background:none;border:0;color:var(--ink-dim);font-family:var(--sans);font-size:11.5px;padding:5px 6px;cursor:pointer;border-radius:6px}
@@ -179,14 +190,14 @@
.mr-now{margin:12px 0 3px;color:var(--ink);font-family:var(--serif);font-style:italic;font-size:13px;min-height:18px;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mr-sub{color:var(--ink-dim);font-size:10.5px;letter-spacing:.03em;margin-bottom:9px}
- .mr-transport{display:flex;align-items:center;gap:8px;margin:6px 0}
- .mr-transport .btn{padding:7px 10px}
- .mr-transport .play{min-width:46px;justify-content:center;font-size:13px}
- .mr-tracks-toggle{margin-left:auto;font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-dim);
- background:none;border:0;cursor:pointer;padding:4px 2px}
+ .mr-transport{display:flex;align-items:center;gap:8px;margin:8px 0}
+ .mr-transport .btn{padding:9px 12px;min-height:44px;min-width:44px;justify-content:center}
+ .mr-transport .play{min-width:54px;justify-content:center;font-size:14px}
+ .mr-tracks-toggle{margin-left:auto;font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-dim);
+ background:none;border:0;cursor:pointer;padding:10px 6px;min-height:44px}
.mr-tracks-toggle:hover{color:var(--gold)}
- .mr-seek{display:flex;align-items:center;margin:9px 0 4px}
- .mr-seek input[type=range]{width:100%}
+ .mr-seek{display:flex;align-items:center;margin:6px 0 2px;padding:9px 0}
+ .mr-seek input[type=range]{width:100%;height:7px}
.mr-time{font-variant-numeric:tabular-nums;color:var(--ink-dim);font-size:10px;text-align:right}
.mr-tracks{max-height:148px;overflow:auto;margin-top:9px;border-top:1px solid rgba(242,182,74,.16);padding-top:8px;display:none}
.musicroom.show-tracks .mr-tracks{display:block}
@@ -254,8 +265,9 @@
<span class="chev" aria-hidden="true">▾</span>
</header>
<div class="mrbody" id="mrbody">
- <input class="mr-search" id="mrSearch" type="search" placeholder="🔍 Search the whole Dead archive — date, city, song…" aria-label="Search the Grateful Dead archive">
- <div class="mr-map-head"><span id="mrMapLabel">Touring history — tap a year</span><button type="button" class="mr-allbtn on" id="mrAll">All years</button></div>
+ <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-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>
<input type="hidden" id="mrYear" value="">
<div class="mr-results" id="mrResults" role="list"></div>
@@ -918,32 +930,6 @@ function drawBoatOverhead(accent,lan,glow){
}
// ---------- Shoreline animals ----------
-// ---- roaming / tempo helpers: animals travel across the scene and step to the beat ----
-// _tempoMul: 1.0 ≈ neutral. Detected music BPM drives it when reacting, else the Visual Tempo slider.
-function _tempoMul(){
- if(window.SRM && window.SRM.react && window.SRM.bpm>40) return clamp(window.SRM.bpm/110, 0.7, 2.2);
- return 0.5 + S.tempo*0.5; // 0.7..2.0 from the slider
-}
-// _roam: a horizontal position that walks across the screen and wraps off-screen; returns x + facing dir.
-// Derived from S.t (which already integrates tempo) so locomotion speeds up/slows with the music.
-function _roam(seed, speed){
- const dir = (seed%2===0) ? 1 : -1;
- const span = S.W*1.30;
- const p = (((S.t*speed*0.06*_tempoMul()) + seed*0.31) % 1 + 1) % 1;
- const x = dir>0 ? (-S.W*0.15 + p*span) : (S.W*1.15 - p*span);
- return {x, dir};
-}
-// every animal gets a small upward HOP on each detected beat when reacting to the music
-function _beatHop(){ return (window.SRM && window.SRM.react) ? (S.pulse||0)*10 : 0; }
-// place a creature at its roamed position, facing its travel direction, hopping on the beat
-function _place(fn, roam, y, s, accent, hop){
- ctx.save();
- ctx.translate(roam.x, y - hop);
- ctx.scale(roam.dir, 1); // flip to face the way it's walking/flying
- fn(0, 0, s, S.t, accent);
- ctx.restore();
-}
-
function drawAnimals(){
if(S.animals<=0.02) return;
const W=S.W,H=S.H;
@@ -959,19 +945,13 @@ function drawAnimals(){
const t=S.t;
const scaleA = S.cam==='deck'?1.15:1;
- const hop=_beatHop();
-
- // walkers now ROAM the shore (each faces its direction of travel, hops on the beat)
- _place(fox, _roam(0,1.00), baseY-6, 1*scaleA, accent, hop);
- _place(heron, _roam(3,0.55), baseY-4, 1*scaleA, accent, hop*0.6);
- _place(rabbit, _roam(4,1.35), baseY-4, 1*scaleA, accent, hop*1.4);
- _place(bearAndFireflies, _roam(1,0.42), baseY-2, 1.1*scaleA, accent, hop*0.8);
- if(!S.playing && !S.reduced){
- drawIdleBackflipBear(scaleA, baseY); // paused: one bear at a time backflipping across the page
- } else {
- drawBearCubs(t, scaleA, baseY); // the troupe dances + sidesteps to tempo
- }
- drawSongbirds(t, scaleA, baseY); // songbirds now FLY around the sky
+
+ fox(W*0.16, baseY-6, 1*scaleA, t, accent);
+ heron(W*0.40, baseY-4, 1*scaleA, t, accent);
+ rabbit(W*0.63, baseY-4, 1*scaleA, t, accent);
+ bearAndFireflies(W*0.85, baseY-2, 1.1*scaleA, t, accent);
+ drawBearCubs(t, scaleA, baseY);
+ drawSongbirds(t, scaleA, baseY);
ctx.restore();
}
@@ -1027,7 +1007,7 @@ function rabbit(x,y,s,t,accent){
const stretch=1+air*0.4, squash=1-Math.max(0,-Math.sin(cyc))*0.35;
const hopY=-air*9; // low, grounded hop — stays on the shore
ctx.save();
- ctx.translate(x, y+hopY); // roam is handled by _place(); keep only the vertical hop
+ ctx.translate(x + ((t*22)% (S.W*0.14)) - S.W*0.05, y+hopY);
ctx.scale(s*(2-stretch)*0.9 + s*0.1, s*stretch*squash);
cut('rgba(120,66,60,0.98)',2);
ctx.beginPath(); ctx.ellipse(0,-8,9,7,0,0,7); fillOutline();
@@ -1107,42 +1087,14 @@ function bearCub(x,y,s,t,ph,FUR){
ctx.fillStyle=OUTLINE; ctx.beginPath(); ctx.arc(0,-29,1.1,0,7); ctx.fill(); // nose
ctx.restore();
}
-// idle state: when nothing is playing, ONE bear at a time backflips slowly across the page
-function drawIdleBackflipBear(scaleA, baseY){
- const W=S.W;
- const cross=8.5, gap=1.4, span=cross+gap; // slow & groovy; one bear at a time
- const tt=S.idleT||0;
- const idx=Math.floor(tt/span);
- const local=tt%span;
- if(local>cross) return; // brief empty beat before the next bear enters
- const prog=local/cross; // 0..1 travelling across the page
- const FUR=BEAR_COLORS[idx%BEAR_COLORS.length];
- const size=1.55*scaleA;
- const margin=64;
- const x=-margin + prog*(W+margin*2); // left -> right across the page
- const flips=3; // full backflips per crossing
- const fp=(prog*flips)%1; // phase within the current flip
- const lift=Math.sin(fp*Math.PI)*54*scaleA; // one jump-arc per flip, peaks mid-air
- const spin=-(prog*flips)*Math.PI*2; // backflip: rotates backward while moving right
- const groundY=baseY+8;
- ctx.save();
- ctx.translate(x, groundY - lift);
- ctx.rotate(spin);
- bearCub(0, 0, size, (S.idleT||0)*0.8, idx*1.3, FUR);
- ctx.restore();
-}
// a user-sized colorful dancing-bear troupe (1..10), auto-distributed + scaled across the shore (our own design, not the GD trademark)
const BEAR_COLORS=['rgba(206,66,116,0.98)','rgba(232,160,52,0.98)','rgba(72,172,150,0.98)','rgba(124,110,224,0.98)','rgba(226,96,60,0.98)','rgba(96,182,96,0.98)','rgba(232,120,182,0.98)','rgba(82,158,224,0.98)','rgba(214,182,60,0.98)','rgba(154,92,204,0.98)'];
function drawBearCubs(t,scaleA,baseY){
const W=S.W, N=Math.max(1,Math.min(10,Math.round(S.bears)));
const margin=W*0.05, span=W-2*margin;
const size=Math.max(0.9, Math.min(2.1, 13/(N+2.5)))*scaleA; // fewer bears bigger, more bears smaller to fit
- const mul=_tempoMul();
- const groupDrift=Math.sin(t*0.22*mul)*W*0.05; // the whole troupe sways across the shore to tempo
for(let i=0;i<N;i++){
- const home = (N===1) ? W*0.16 : margin + span*(i/(N-1));
- const step = Math.sin(t*2.0*mul + i*1.3)*(W*0.022+8); // each cub sidesteps/boogies in time
- const x = Math.max(margin*0.4, Math.min(W-margin*0.4, home + groupDrift + step));
+ const x = (N===1) ? W*0.16 : margin + span*(i/(N-1));
const yj = ((i*53)%9)-4; // slight stagger so the row isn't a flat line
bearCub(x, baseY+8+yj, size, t, i*1.3, BEAR_COLORS[i%BEAR_COLORS.length]);
}
@@ -1177,15 +1129,10 @@ function songbird(x,y,s,t,ph){
}
}
function drawSongbirds(t,scaleA,baseY){
- const W=S.W, H=S.H, N=5, mul=_tempoMul(); // songbirds now FLY around the sky, facing their heading
- for(let i=0;i<N;i++){
- const r=_roam(10+i, 0.85+i*0.14);
- const lane=0.16+0.30*(0.5+0.5*Math.sin(i*1.7)); // spread across sky lanes
- const y=H*lane + Math.sin(t*2.2*mul + i*1.5)*24; // rise/dip flight-path, cadence follows tempo
- ctx.save(); ctx.translate(r.x, y); ctx.scale(r.dir,1);
- songbird(0, 0, (0.85+0.2*Math.sin(i))*scaleA, t, i*1.3);
- ctx.restore();
- }
+ const W=S.W; // kept clear of the center control dock
+ songbird(W*0.075, baseY-9, 1.0*scaleA, t, 0.0);
+ songbird(W*0.235, baseY-7, 0.9*scaleA, t, 2.1);
+ songbird(W*0.93, baseY-11, 1.05*scaleA, t, 4.2);
}
// ---------- River foreground with parallax reflections ----------
@@ -1288,7 +1235,6 @@ function drawPtero(dt){
let last=performance.now();
function frame(now){
const dt=Math.min(0.05,(now-last)/1000); last=now;
- S.idleT=(S.idleT||0)+dt; // advances even while paused, drives the idle backflip bear
// live audio energy (0..1) when "React to Music" is on — otherwise motion stays authored
const reacting = !!(window.SRM && window.SRM.react);
@@ -1300,7 +1246,7 @@ function frame(now){
if(S.playing){
const spd = S.reduced ? 0.28 : 1.0;
- S.t += dt * S.tempo * spd * 0.85; // slowed — jazzy/groovy pacing
+ S.t += dt * S.tempo * spd * 1.4;
// Gentle auto-journey through the three chapters (until the visitor takes over)
if(S.autoJourney && !S.reduced){
@@ -1517,25 +1463,13 @@ requestAnimationFrame(frame);
(() => {
"use strict";
const CATALOG = [
- {id:"gd69-02-27.sbd.16track.kaplan.6315.sbeok.shnf", date:"February 27, 1969", title:"Fillmore West", sub:"San Francisco · primal psychedelic era"},
- {id:"gd70-05-24.sbd.hanno.6481.sbeok.shnf", date:"May 24, 1970", title:"Newcastle-Under-Lyme", sub:"Newcastle · acoustic-and-electric era"},
- {id:"gd71-04-14.sbd.lai.6250.sbefail.shnf", date:"April 14, 1971", title:"Christy Mathewson Stadium, Bucknell U.", sub:"Lewisburg · Skull & Roses era"},
- {id:"gd72-08-27.sbd.orf.3328.sbeok.shnf", date:"August 27, 1972", title:"Veneta", sub:"Oregon · field, heat, and open sky"},
- {id:"gd73-02-15.sbd.hall.1580.sbeok.shnf", date:"February 15, 1973", title:"Dane County Coliseum", sub:"Madison · jazz-loose / Wake of the Flood era"},
- {id:"gd74-06-23.sbd.cribbs.16780.sbeok.shnf", date:"June 23, 1974", title:"Jai-Alai Fronton", sub:"Miami · Wall of Sound era"},
- {id:"gd76-06-10.sbd.bertha-ashley.23370.sbeok.shnf", date:"June 10, 1976", title:"Boston Music Hall", sub:"Boston · return-from-hiatus era"},
- {id:"gd77-05-08.sbd.hicks.4982.sbeok.shnf", date:"May 8, 1977", title:"Cornell", sub:"Ithaca · Barton Hall"},
- {id:"gd78-02-01.sbd.parillo.1017.sbeok.shnf", date:"February 1, 1978", title:"Uptown Theater", sub:"Chicago · tight & funky late-’70s era"},
- {id:"gd81-03-21.sbd.clugston.6666.sbeok.shnf", date:"March 21, 1981", title:"Rainbow Theatre", sub:"London · European-tour era"},
- {id:"gd85-04-27.sbd.jerugim.359.sbeok.shnf", date:"April 27, 1985", title:"Frost Amphitheatre", sub:"Palo Alto · 20th-anniversary / Frost era"},
- {id:"gd1987-01-28.sbd.walker-scotton.miller.83286.flac16", date:"January 28, 1987", title:"S.F Civic Auditorium", sub:"San Francisco · In the Dark comeback era"},
- {id:"gd89-12-31.sbd.bertha-ashley.18674.sbeok.shnf", date:"December 31, 1989", title:"Oakland-Alameda County Coliseum", sub:"Oakland · late-peak / Built to Last era"},
- {id:"gd90-03-29.aud-fob.set2.unknown.1317.sbeok.shnf", date:"March 29, 1990", title:"Nassau", sub:"Uniondale · late-era expansion"},
- {id:"gd91-10-31.sbd.gardner.2897.sbeok.shnf", date:"October 31, 1991", title:"Oakland-Alameda County Coliseum", sub:"Oakland · post-Brent / Bruce Hornsby era"},
- {id:"gd93-03-24.sbd.keyser.25664.sbeok.flacf", date:"March 24, 1993", title:"Dean Smith Center", sub:"Chapel Hill · Vince era / stadium years"},
- {id:"gd94-10-05.sbd.unknown.8030.sbeok.shnf", date:"October 5, 1994", title:"The Spectrum", sub:"Philadelphia · final-years era"}
+ {id:'gd69-02-27.sbd.16track.kaplan.6315.sbeok.shnf', date:'February 27, 1969', title:'Fillmore West', sub:'San Francisco · primal psychedelic era'},
+ {id:'gd72-08-27.sbd.orf.3328.sbeok.shnf', date:'August 27, 1972', title:'Veneta', sub:'Oregon · field, heat, and open sky'},
+ {id:'gd74-06-23.sbd.cribbs.16780.sbeok.shnf', date:'June 23, 1974', title:'Jai-Alai Fronton', sub:'Miami · Wall of Sound era'},
+ {id:'gd77-05-08.sbd.hicks.4982.sbeok.shnf', date:'May 8, 1977', title:'Cornell', sub:'Ithaca · Barton Hall'},
+ {id:'gd90-03-29.aud-fob.set2.unknown.1317.sbeok.shnf',date:'March 29, 1990', title:'Nassau', sub:'Uniondale · late-era expansion'}
];
-const DEFAULT_SHOW = 7; // Cornell '77
+const DEFAULT_SHOW = 3; // Cornell '77
const $ = id => document.getElementById(id);
const audio = $('mrAudio'), room = $('musicroom'), sel = $('mrShow'), live = $('live');
@@ -1599,8 +1533,9 @@ async function searchArchive(q){
const res=$('mrResults'); if(!res) return;
q=(q||'').trim();
const yr=($('mrYear')&&$('mrYear').value)||'';
+ const coll='collection:'+((window.MR_SOURCES&&window.MR_SOURCES[window.MR_SRC||'gd'])||{id:'GratefulDead'}).id;
const filters=[]; if(q) filters.push('('+q+')'); if(yr) filters.push('year:'+yr);
- const query = filters.length ? 'collection:GratefulDead AND '+filters.join(' AND ') : 'collection:GratefulDead';
+ const query = filters.length ? coll+' AND '+filters.join(' AND ') : coll;
res.innerHTML='<div class="mr-hint">'+(filters.length?'Searching the archive…':'Loading the most-played shows…')+'</div>';
const sort = (q||yr) ? 'downloads+desc' : 'downloads+desc';
try{
@@ -1662,18 +1597,7 @@ function setPlay(on){
// Default OFF — visuals stay authored until the visitor flips "React to Music".
audio.crossOrigin='anonymous'; // set before any src load so the CDN stream is analyzable
window.SRM = window.SRM || { level:0, react:false };
-// song-analysis fields the orb + animals read: bpm (detected tempo), vocal (0..1 lyric presence),
-// change (a counter that ticks on a NOTICEABLE song change), changeKind ('track'|'tempo'|'vocal-in'|'vocal-out')
-Object.assign(window.SRM,{ bpm:0, vocal:0, change:window.SRM.change||0, changeKind:'', changeAt:0 });
let _actx, _analyser, _freq, _looping=false;
-// beat-interval ring buffer -> BPM, plus running baselines for change detection
-let _beatTimes=[], _bpmBase=0, _vocalSm=0, _vocalState=false, _lastTrack='', _lastChangeClock=-999;
-function _median(a){ const b=a.slice().sort((x,y)=>x-y); return b.length? b[(b.length>>1)] : 0; }
-function _fireChange(kind){
- if(_clock-_lastChangeClock < 90) return; // rate-limit: ~1.5s between change events
- _lastChangeClock=_clock;
- window.SRM.change++; window.SRM.changeKind=kind; window.SRM.changeAt=performance.now();
-}
function ensureGraph(){
if(_actx) return true;
try{
@@ -1689,43 +1613,16 @@ let _avg=0, _clock=0, _lastBeat=-99;
function sampleLevel(){
if(_analyser){
_analyser.getByteFrequencyData(_freq);
- const L=_freq.length; // 128 bins, ~0..22kHz
- let sum=0, n=Math.max(1,Math.floor(L/6)); // low-end (bass) energy
+ let sum=0, n=Math.max(1,Math.floor(_freq.length/6)); // low-end (bass) energy
for(let i=0;i<n;i++) sum+=_freq[i];
const raw=(sum/n)/255;
- window.SRM.level = window.SRM.level*0.6 + raw*0.4; // smoothed 0..1
- // vocal-band (~300Hz..3.4kHz -> mid bins) energy, normalised against overall energy:
- // a strong, sustained mid presence over the bass floor reads as "lyrics / a voice is in"
- let vsum=0, vn=0, tsum=0;
- for(let i=3;i<24 && i<L;i++){ vsum+=_freq[i]; vn++; }
- for(let i=0;i<L;i++) tsum+=_freq[i];
- const vocalRaw = tsum>1 ? Math.min(1,(vsum/Math.max(1,vn))/ (tsum/L) * 0.5) : 0;
- _vocalSm = _vocalSm*0.9 + vocalRaw*0.1;
- window.SRM.vocal = _vocalSm;
+ window.SRM.level = window.SRM.level*0.6 + raw*0.4; // smoothed 0..1
// beat detection: a bass spike well above the running average, rate-limited
_avg = _avg*0.92 + raw*0.08; _clock++;
if(raw > _avg*1.10 && raw > 0.12 && (_clock-_lastBeat) > 5){
- const now=performance.now();
- if(window.SRM.beat){ const dtb=now-(window.SRM._lastBeatMs||now); if(dtb>240 && dtb<1400){ _beatTimes.push(dtb); if(_beatTimes.length>8) _beatTimes.shift(); } }
- window.SRM._lastBeatMs=now;
_lastBeat=_clock; window.SRM.beat=(window.SRM.beat||0)+1; window.SRM.pulse=1;
- // BPM from the median beat interval, smoothed
- if(_beatTimes.length>=4){ const bpm=60000/_median(_beatTimes); window.SRM.bpm = window.SRM.bpm? window.SRM.bpm*0.8+bpm*0.2 : bpm; }
- }
- window.SRM.pulse = (window.SRM.pulse||0)*0.86; // decaying throb (spikes to 1 on a beat)
-
- // ---- NOTICEABLE-CHANGE detection (drives the orb's UFO-split & celestial morphs) ----
- // 1) a new track started
- const tr=window.SRM.track||'';
- if(tr && tr!==_lastTrack){ _lastTrack=tr; _bpmBase=0; _beatTimes=[]; _fireChange('track'); }
- // 2) a real tempo shift vs the running baseline
- if(window.SRM.bpm>40){
- if(!_bpmBase) _bpmBase=window.SRM.bpm;
- else { if(Math.abs(window.SRM.bpm-_bpmBase)/_bpmBase > 0.18) _fireChange('tempo'); _bpmBase=_bpmBase*0.98+window.SRM.bpm*0.02; }
}
- // 3) lyrics-vs-just-music crossover (hysteresis so it doesn't flicker)
- if(!_vocalState && _vocalSm>0.42){ _vocalState=true; _fireChange('vocal-in'); }
- else if(_vocalState && _vocalSm<0.26){ _vocalState=false; _fireChange('vocal-out'); }
+ window.SRM.pulse = (window.SRM.pulse||0)*0.86; // decaying throb (spikes to 1 on a beat)
}
requestAnimationFrame(sampleLevel);
}
@@ -1774,47 +1671,81 @@ seek.addEventListener('change',()=>{ const d=audio.duration||0; if(d>0) audio.cu
sel.addEventListener('change',()=>loadShow(+sel.value,{play:true}));
-// archive search wiring — type anything, debounced; Enter searches immediately
+// archive browse wiring — source selector + tappable year-tile grid + debounced search
const searchEl=$('mrSearch'), yearEl=$('mrYear');
-// Interactive touring-history map: one bar per year, height ∝ shows surviving that year.
-const mapEl=$('mrMap'), mapLabel=$('mrMapLabel'), allBtn=$('mrAll');
-if(mapEl && yearEl){
- const YEARS=[]; for(let y=1965;y<=1995;y++) YEARS.push(y);
- const counts={};
- function setYear(y){
- yearEl.value=y?String(y):'';
- [...mapEl.children].forEach(c=>c.classList.toggle('on', c.dataset.y===String(y)));
- allBtn.classList.toggle('on', !y);
- mapLabel.textContent = y ? `${y} · ${(counts[y]||0).toLocaleString()} shows` : 'Touring history — tap a year';
- searchArchive(searchEl?searchEl.value:'');
- }
+const mapEl=$('mrMap'), mapLabel=$('mrMapLabel'), allBtn=$('mrAll'), srcEl=$('mrSrc');
+
+// Two FREE, band-sanctioned Dead archives on archive.org (CORS-open, streamable).
+window.MR_SOURCES={
+ gd:{id:'GratefulDead', label:'Grateful Dead', years:'1965–1995', y0:1965, y1:1995},
+ dc:{id:'DeadAndCompany', label:'Dead & Company', years:'2015–2023', y0:2015, y1:2023}
+};
+window.MR_SRC='gd';
+const counts={}; // per (source|year) show-count cache
+const ck=(s,y)=>s+':'+y;
+
+function setYear(y){
+ yearEl.value=y?String(y):'';
+ [...mapEl.children].forEach(c=>c.classList&&c.classList.toggle('on', c.dataset.y===String(y)));
+ allBtn.classList.toggle('on', !y);
+ const c=counts[ck(window.MR_SRC,y)];
+ mapLabel.textContent = y ? `${y}${c!=null?' · '+c.toLocaleString()+' shows':''}` : 'Tap a year';
+ searchArchive(searchEl?searchEl.value:'');
+}
+
+function buildMap(){
+ const src=window.MR_SOURCES[window.MR_SRC], mySrc=window.MR_SRC;
+ const YEARS=[]; for(let y=src.y0;y<=src.y1;y++) YEARS.push(y);
+ mapEl.innerHTML='';
YEARS.forEach(y=>{
- const b=document.createElement('button'); b.type='button'; b.className='mr-yr'; b.dataset.y=String(y); b.setAttribute('role','option');
- b.title=y+' — counting…';
- b.innerHTML=`<span class="bar" style="height:3px"></span><span class="yl">${y%5===0?y:''}</span>`;
+ 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.innerHTML=`<span class="yl">${y}</span><span class="yc">…</span>`;
b.addEventListener('click',()=>setYear(y));
- b.addEventListener('mouseenter',()=>{ if(counts[y]!=null) mapLabel.textContent=`${y} · ${counts[y].toLocaleString()} shows`; });
+ b.addEventListener('mouseenter',()=>{ const c=counts[ck(window.MR_SRC,y)]; if(c!=null) mapLabel.textContent=`${y} · ${c.toLocaleString()} shows`; });
mapEl.appendChild(b);
});
- mapEl.addEventListener('mouseleave',()=>{ const on=yearEl.value; mapLabel.textContent = on?`${on} · ${(counts[on]||0).toLocaleString()} shows`:'Touring history — tap a year'; });
- allBtn.addEventListener('click',()=>setYear(0));
- // Fetch per-year show counts in parallel, then scale bars (sqrt to tame the '77–'90 spikes).
+ // per-year show counts → heat-color the tiles (sqrt to tame the '77/'89 spikes)
Promise.all(YEARS.map(y=>
- fetch('https://archive.org/advancedsearch.php?q='+encodeURIComponent('collection:GratefulDead AND year:'+y)+'&rows=0&output=json',{mode:'cors'})
- .then(r=>r.json()).then(j=>({y,n:(j.response&&j.response.numFound)||0})).catch(()=>({y,n:0}))
+ fetch('https://archive.org/advancedsearch.php?q='+encodeURIComponent('collection:'+src.id+' AND year:'+y)+'&rows=0&output=json',{mode:'cors'})
+ .then(r=>r.json()).then(j=>({y,n:(j.response&&j.response.numFound)||0})).catch(()=>({y,n:-1}))
)).then(rows=>{
- const max=Math.max(1,...rows.map(r=>Math.sqrt(r.n)));
- rows.forEach(({y,n})=>{ counts[y]=n; const el=mapEl.querySelector('[data-y="'+y+'"]'); if(!el) return;
- el.querySelector('.bar').style.height=Math.round(3+(Math.sqrt(n)/max)*40)+'px';
- el.title=y+' — '+n.toLocaleString()+' shows';
+ if(mySrc!==window.MR_SRC) return; // source switched mid-flight
+ if(!rows.some(r=>r.n>=0)){ mapEl.innerHTML='<div class="mr-map-err">Couldn’t load the tour history — archive.org may be unreachable. Search still works.</div>'; return; }
+ const max=Math.max(1,...rows.filter(r=>r.n>0).map(r=>Math.sqrt(r.n)));
+ rows.forEach(({y,n})=>{ const el=mapEl.querySelector('[data-y="'+y+'"]'); if(!el) return;
+ el.classList.remove('loading'); const nn=Math.max(0,n); counts[ck(mySrc,y)]=nn;
+ el.style.setProperty('--heat',(Math.sqrt(nn)/max).toFixed(3));
+ el.querySelector('.yc').textContent=nn.toLocaleString();
+ el.title=y+' — '+nn.toLocaleString()+' shows';
});
- const cur=yearEl.value; if(cur) mapLabel.textContent=`${cur} · ${(counts[cur]||0).toLocaleString()} shows`;
- }).catch(()=>{});
+ const cur=yearEl.value, c=cur?counts[ck(mySrc,cur)]:null;
+ if(cur&&c!=null) mapLabel.textContent=`${cur} · ${c.toLocaleString()} shows`;
+ });
+}
+
+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('');
+ 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)); });
+ yearEl.value=''; allBtn.classList.add('on'); mapLabel.textContent='Tap a year';
+ buildMap(); searchArchive(searchEl?searchEl.value:'');
+ });
}
+
+if(mapEl && yearEl){
+ mapEl.addEventListener('mouseleave',()=>{ const on=yearEl.value, c=on?counts[ck(window.MR_SRC,on)]:null; mapLabel.textContent = on?`${on}${c!=null?' · '+c.toLocaleString()+' shows':''}`:'Tap a year'; });
+ allBtn.addEventListener('click',()=>setYear(0));
+ buildMap();
+}
+
if(searchEl){
searchEl.addEventListener('input',()=>{ clearTimeout(searchTimer); searchTimer=setTimeout(()=>searchArchive(searchEl.value),450); });
searchEl.addEventListener('keydown',e=>{ if(e.key==='Enter'){ e.preventDefault(); clearTimeout(searchTimer); searchArchive(searchEl.value); } });
- searchArchive(''); // populate the most-played shows from the whole archive on load
+ searchArchive(''); // populate the most-played shows on load
}
$('mrTracksToggle').addEventListener('click',()=>{
@@ -1831,10 +1762,8 @@ function toggleRoom(){
mrToggle.addEventListener('click',toggleRoom);
mrToggle.addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); toggleRoom(); } });
-// small screens: start collapsed so the art leads
-if(window.matchMedia && matchMedia('(max-width:820px)').matches){
- room.setAttribute('data-open','false'); mrToggle.setAttribute('aria-expanded','false');
-}
+// 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);
@@ -1859,163 +1788,28 @@ if(_p.endsWith('/room') || /dead\.agentabrams/i.test(location.hostname)){
const scene=new THREE.Scene();
const camera=new THREE.PerspectiveCamera(50,W/H,0.1,100); camera.position.z=5;
const group=new THREE.Group(); group.position.y=1.45; scene.add(group); // floats up in the sky over the boat
-
- // ---- the core orb (its own sub-group so effects can rotate it WITHOUT tilting the world-down beams) ----
- const orb=new THREE.Group(); group.add(orb);
const wire=new THREE.Mesh(new THREE.IcosahedronGeometry(0.95,2), new THREE.MeshBasicMaterial({wireframe:true,color:0xf2b64a,transparent:true,opacity:0.8}));
const core=new THREE.Mesh(new THREE.IcosahedronGeometry(0.76,3), new THREE.MeshStandardMaterial({color:0x2a0810,emissive:0xc9432b,emissiveIntensity:0.7,roughness:0.35,metalness:0.6,transparent:true,opacity:0.85}));
- orb.add(core,wire);
-
- // ---- 5 UFO saucers, each casting a downward light beam (searchlight cone, additive) ----
- const ufo=new THREE.Group(); ufo.visible=false; group.add(ufo);
- const saucers=[];
- for(let i=0;i<5;i++){
- const u=new THREE.Group();
- const body=new THREE.Mesh(new THREE.SphereGeometry(0.24,20,12), new THREE.MeshStandardMaterial({color:0x0d1220,emissive:0x24406e,emissiveIntensity:0.7,roughness:0.25,metalness:0.85}));
- body.scale.set(1,0.32,1);
- const dome=new THREE.Mesh(new THREE.SphereGeometry(0.13,16,10,0,Math.PI*2,0,Math.PI/2), new THREE.MeshStandardMaterial({color:0xbdf0ff,emissive:0x46c4ff,emissiveIntensity:1.4,roughness:0.15,metalness:0.4,transparent:true,opacity:0.9}));
- dome.position.y=0.055;
- const rim=new THREE.Mesh(new THREE.TorusGeometry(0.215,0.028,8,26), new THREE.MeshBasicMaterial({color:0x9fe8ff,transparent:true,opacity:0.9}));
- rim.rotation.x=Math.PI/2;
- // searchlight: cone apex at the saucer (top), wide base sweeping DOWN to the boat
- const beam=new THREE.Mesh(new THREE.ConeGeometry(0.42,1.7,22,1,true), new THREE.MeshBasicMaterial({color:0x8fe0ff,transparent:true,opacity:0.14,side:THREE.DoubleSide,depthWrite:false,blending:THREE.AdditiveBlending}));
- beam.position.y=-0.9; // hangs below the saucer
- u.add(beam,body,rim,dome);
- ufo.add(u); saucers.push({g:u,beam,dome,rim,ang:i/5*Math.PI*2,spin:0.4+i*0.12});
- }
-
- // ---- celestial patterns: a morphing point-cloud + constellation lines ----
- const CN=224;
- const cur=new Float32Array(CN*3), targ=new Float32Array(CN*3);
- const pgeo=new THREE.BufferGeometry(); pgeo.setAttribute('position',new THREE.BufferAttribute(cur,3));
- const stars=new THREE.Points(pgeo,new THREE.PointsMaterial({color:0xfff1cf,size:0.055,transparent:true,opacity:0.95,depthWrite:false,blending:THREE.AdditiveBlending}));
- const MAXSEG=110, lpos=new Float32Array(MAXSEG*2*3), lpair=[];
- const lgeo=new THREE.BufferGeometry(); lgeo.setAttribute('position',new THREE.BufferAttribute(lpos,3));
- const lines=new THREE.LineSegments(lgeo,new THREE.LineBasicMaterial({color:0x9fd0ff,transparent:true,opacity:0.28,depthWrite:false,blending:THREE.AdditiveBlending}));
- const celest=new THREE.Group(); celest.visible=false; celest.add(stars,lines); group.add(celest);
- // pattern builders write into targ[] and rebuild the constellation line pairs
- const PATTERNS=['galaxy','orbits','shell','starburst'];
- function buildPattern(name){
- for(let i=0;i<CN;i++){
- let x=0,y=0,z=0; const f=i/CN;
- if(name==='galaxy'){ const arm=i%3, a=f*7.5+arm*2.094, r=0.18+f*1.35; x=Math.cos(a)*r; y=Math.sin(a)*r*0.55; z=Math.sin(i*0.7)*0.14; }
- else if(name==='orbits'){ const ring=i%5, a=(i/5)*0.9+ring*1.3, r=0.5+ring*0.22, tilt=ring*0.5; x=Math.cos(a)*r; y=Math.sin(a)*r*Math.cos(tilt); z=Math.sin(a)*r*Math.sin(tilt); }
- else if(name==='shell'){ const gr=(1+Math.sqrt(5))/2, th=2*Math.PI*i/gr, ph=Math.acos(1-2*(i+0.5)/CN), r=1.25; x=Math.sin(ph)*Math.cos(th)*r; y=Math.cos(ph)*r; z=Math.sin(ph)*Math.sin(th)*r; }
- else { const pt=Math.floor(f*10), sub=(f*10)%1, base=(pt%2?0.45:1.3), a=pt*Math.PI/5; const r=base*(1-sub*0.15); x=Math.cos(a)*r; y=Math.sin(a)*r; z=(Math.random()-0.5)*0.18; } // starburst
- targ[i*3]=x; targ[i*3+1]=y; targ[i*3+2]=z;
- }
- // nearest-neighbour constellation edges (from the target shape)
- lpair.length=0;
- for(let i=0;i<CN && lpair.length<MAXSEG;i++){
- let bj=-1,bd=1e9;
- for(let j=i+1;j<CN;j++){ const dx=targ[i*3]-targ[j*3],dy=targ[i*3+1]-targ[j*3+1],dz=targ[i*3+2]-targ[j*3+2],d=dx*dx+dy*dy+dz*dz; if(d<bd){bd=d;bj=j;} }
- if(bj>=0 && bd<0.16) lpair.push([i,bj]);
- }
- }
- let patIdx=0; buildPattern(PATTERNS[0]);
- for(let i=0;i<CN*3;i++) cur[i]=targ[i]*0.2; // start collapsed toward the core
- function cyclePattern(){ patIdx=(patIdx+1)%PATTERNS.length; buildPattern(PATTERNS[patIdx]); }
-
+ group.add(core,wire);
const l1=new THREE.PointLight(0xff8a4a,1.3,60); l1.position.set(4,3,5); scene.add(l1);
const l2=new THREE.PointLight(0xf2b64a,1.0,60); l2.position.set(-4,-1,4); scene.add(l2);
- const l3=new THREE.PointLight(0x7fd0ff,0.0,60); l3.position.set(0,3,4); scene.add(l3); // cool light, rises in UFO/celestial modes
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); });
-
- // ---- effect state machine: 'none' (orb), 'ufo', 'celestial', 'bloom' ----
- let fx='none', fxT=0, fxDur=7, w=0, seenChange=(window.SRM&&window.SRM.change)||0;
- let ai=false, quietT=0;
- function enterFx(name,dur){ fx=name; fxT=0; fxDur=dur||7; if(name==='celestial') cyclePattern(); }
- // AI VJ picks the next effect from the live audio; when off, a noticeable change still splits the orb
- function director(kind,level,vocal,bpm){
- if(ai){
- if(level>0.55 || bpm>132) enterFx('ufo',6);
- else if(vocal<0.24 || kind==='vocal-out') enterFx('celestial',9);
- else enterFx(Math.random()<0.55?'ufo':'bloom', 5);
- } else {
- if(kind==='vocal-out') enterFx('celestial',8); // instrumental stretch -> stars
- else enterFx('ufo',7); // any other noticeable change -> 5 UFOs
- }
- }
-
- // click the orb to hand it to the AI VJ (autonomous, reads the live audio and choreographs itself)
- function orbScreen(){ const v=group.position.clone().project(camera); return {x:(v.x*0.5+0.5)*W, y:(-v.y*0.5+0.5)*H, r:Math.max(60,Math.min(W,H)*0.16)}; }
- addEventListener('pointerdown', e=>{
- const o=orbScreen();
- if(Math.hypot(e.clientX-o.x, e.clientY-o.y) < o.r){
- ai=!ai; e.stopPropagation();
- if(ai){ enterFx('celestial',9); if(window.SRM) window.SRM.aiVJ=true; }
- else { fx='none'; if(window.SRM) window.SRM.aiVJ=false; }
- const lv=document.getElementById('live'); if(lv) lv.textContent = ai ? 'The orb is taken over by the AI VJ — it choreographs UFOs, celestial patterns and blooms to the music.' : 'AI VJ released. The orb responds to song changes on its own.';
- }
- }, true);
-
- const easeOut=x=>1-Math.pow(1-x,3);
- let last=performance.now();
- function loop(now){ requestAnimationFrame(loop);
- const dt=Math.min(0.05,((now||performance.now())-last)/1000); last=now||performance.now();
- const s=window.SRM||{}, t=(now||performance.now())/1000;
- const pulse=s.pulse||0, level=(s.react?(s.level||0):0), vocal=(s.react?(s.vocal||0):0), bpm=s.bpm||0;
- const idle=0.5+0.5*Math.sin(t*1.2);
+ function loop(){ requestAnimationFrame(loop);
+ const s=window.SRM||{}, t=performance.now()/1000;
+ 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;
-
- // fire effects on a NOTICEABLE song change; the AI VJ also self-triggers on quiet stretches
- const chg=(s.change||0);
- if(chg>seenChange){ seenChange=chg; director(s.changeKind||'', level, vocal, bpm); }
- if(s.react && level<0.16 && vocal<0.18){ quietT+=dt; if(quietT>9 && fx==='none'){ enterFx('celestial', ai?10:7); quietT=0; } } else quietT=0;
-
- // blend weight w: ramp up while the effect holds, ramp back down, then release to the plain orb
- const active = fx!=='none' && fxT<fxDur;
- if(fx!=='none'){ fxT+=dt; if(fxT>=fxDur+0.7 && w<0.02){ fx='none'; } }
- w += ((active?1:0)-w)*Math.min(1,dt*4.5);
- const ew=easeOut(Math.max(0,Math.min(1,w)));
-
- // ---- the plain orb (always present; shrinks away as an effect takes hold) ----
- orb.visible = w<0.995;
- orb.rotation.y+=0.008+energy*0.02; orb.rotation.x=Math.sin(t*0.3)*0.18;
- const baseScale=(1+energy*0.35)*(fx==='bloom'? (1+ew*(0.8+pulse*1.4)) : (1-ew));
- orb.scale.setScalar(Math.max(0.0001,baseScale));
- core.material.emissiveIntensity=0.5+energy*1.6 + (fx==='bloom'?ew*2.5:0);
- const hue=(28 + Math.sin(t*0.2)*16 + level*40)%360;
+ group.rotation.y+=0.008+energy*0.02; group.rotation.x=Math.sin(t*0.3)*0.18;
+ group.scale.setScalar(1+energy*0.35);
+ core.material.emissiveIntensity=0.5+energy*1.6;
+ const hue=(28 + Math.sin(t*0.2)*16 + level*40)%360; // warm ember range, shifts with the music
wire.material.color.setHSL(((hue+18)%360)/360,0.85,0.6);
col.setHSL(hue/360,0.85,0.5); core.material.emissive.copy(col);
- l3.intensity = ew*1.4;
-
- // ---- UFO formation ----
- ufo.visible = (fx==='ufo') && w>0.002;
- if(ufo.visible){
- const spread=0.35+ew*0.95, hover=1.6; // saucers hover above the boat then beam down
- for(let i=0;i<saucers.length;i++){ const S5=saucers[i];
- const a=S5.ang + t*0.25;
- S5.g.position.set(Math.cos(a)*spread, (i-2)*0.06 + Math.sin(t*1.3+i)*0.08, Math.sin(a)*spread*0.65);
- S5.g.rotation.y += (S5.spin+pulse*2.0)*dt*2.2;
- S5.g.scale.setScalar(ew);
- S5.beam.material.opacity = (0.10+pulse*0.22+level*0.14)*ew; // beams throb on the beat
- S5.beam.scale.y = 1+pulse*0.5 + Math.sin(t*3+i)*0.08;
- const bh=(200+ i*26 + (bpm||0))%360; S5.dome.material.emissive.setHSL(bh/360,0.9,0.6);
- }
- }
-
- // ---- celestial point-cloud ----
- celest.visible = (fx==='celestial') && w>0.002;
- if(celest.visible){
- for(let i=0;i<CN*3;i++) cur[i]+=(targ[i]-cur[i])*Math.min(1,dt*2.6);
- pgeo.attributes.position.needsUpdate=true;
- for(let k=0;k<lpair.length;k++){ const a=lpair[k][0]*3,b=lpair[k][1]*3,o=k*6;
- lpos[o]=cur[a];lpos[o+1]=cur[a+1];lpos[o+2]=cur[a+2]; lpos[o+3]=cur[b];lpos[o+4]=cur[b+1];lpos[o+5]=cur[b+2]; }
- lgeo.setDrawRange(0,lpair.length*2); lgeo.attributes.position.needsUpdate=true;
- celest.rotation.y += (0.12+energy*0.4)*dt; celest.rotation.x=Math.sin(t*0.2)*0.25;
- celest.scale.setScalar(ew*(1+pulse*0.12));
- stars.material.opacity=0.7+0.3*Math.sin(t*4)+pulse*0.3;
- stars.material.color.setHSL(((t*10+level*60)%360)/360,0.5,0.85);
- // re-morph to the next celestial pattern on a strong beat, a few seconds in
- if(fxT>3 && pulse>0.85 && Math.random()<0.04) cyclePattern();
- }
-
renderer.render(scene,camera);
}
- requestAnimationFrame(loop);
+ loop();
})();
</script>
</body>
diff --git a/scripts/screenrecord-5pass.js b/scripts/screenrecord-5pass.js
index af8493a..660a73b 100644
--- a/scripts/screenrecord-5pass.js
+++ b/scripts/screenrecord-5pass.js
@@ -1,38 +1,33 @@
'use strict';
// 5-pass screen recording + click-every-element debug agent
// dead.agentabrams.com/room/ — Grateful Dead music room
-// Skill: screenrecord/SKILL.md
-// Usage: NODE_PATH=$HOME/.npm-global/lib/node_modules node scripts/screenrecord-5pass.js
+// NODE_PATH=$HOME/.npm-global/lib/node_modules node scripts/screenrecord-5pass.js
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
+const { execSync } = require('child_process');
const URL = 'https://dead.agentabrams.com/room/';
const CHROME_EXE = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
const LOG = '/Users/macstudio3/Projects/dead-agentabrams/screenrecord/debug-log.jsonl';
const REC_BASE = '/Users/macstudio3/Projects/dead-agentabrams/screenrecord/rec';
-// Ensure dirs exist
fs.mkdirSync(path.dirname(LOG), { recursive: true });
for (let r = 0; r < 5; r++) fs.mkdirSync(path.join(REC_BASE, `run${r}`), { recursive: true });
-// Load prior log for run4 "errors-first" strategy
const prior = fs.existsSync(LOG)
? fs.readFileSync(LOG, 'utf8').trim().split('\n').filter(Boolean).map(l => {
try { return JSON.parse(l); } catch { return null; }
}).filter(Boolean)
: [];
const priorErrors = new Set(prior.filter(r => r.errors && r.errors.length > 0).map(r => r.selector));
-
console.log(`Prior log entries: ${prior.length}, prior errored selectors: ${priorErrors.size}`);
-// Append one JSONL line
function append(obj) {
fs.appendFileSync(LOG, JSON.stringify(obj) + '\n');
}
-// Deterministic seeded shuffle (run3)
function seededShuffle(arr, seed) {
const s = [...arr];
let state = seed;
@@ -44,35 +39,19 @@ function seededShuffle(arr, seed) {
return s;
}
-// Build click order for each run
function orderFor(run, els) {
- const byType = t => t === 'range';
switch(run) {
- case 0: return [...els]; // DOM order
- case 1: return [...els].reverse(); // reverse DOM
- case 2: return [...els].sort((a, b) => byType(b.type) - byType(a.type)); // sliders first
- case 3: return seededShuffle(els, 42 * 13 + run * 7); // seeded shuffle
+ case 0: return [...els];
+ case 1: return [...els].reverse();
+ case 2: return [...els].sort((a, b) => (b.type === 'range' ? 1 : 0) - (a.type === 'range' ? 1 : 0));
+ case 3: return seededShuffle(els, 42 * 13 + run * 7);
case 4: {
- // errors-first: put previously errored selectors at front
const bad = sel => priorErrors.has(sel) ? 0 : 1;
return [...els].sort((a, b) => bad(a.selector) - bad(b.selector));
}
}
}
-// Measure how long archive.org fetch takes (31 parallel count queries)
-async function measureBarHeightTiming(page) {
- return await page.evaluate(() => {
- return new Promise(resolve => {
- const bars = Array.from(document.querySelectorAll('.mr-yr .bar, .bar'));
- if (!bars.length) { resolve({ barCount: 0, hasRealHeights: false }); return; }
- // Check if bars have data-loaded or non-zero height from parent
- const parentHeights = Array.from(document.querySelectorAll('.mr-yr')).map(b => b.style.height || window.getComputedStyle(b).height);
- resolve({ barCount: bars.length, parentHeights: parentHeights.slice(0, 5) });
- });
- });
-}
-
async function runPass(runIdx) {
console.log(`\n===== RUN ${runIdx} =====`);
const recDir = path.join(REC_BASE, `run${runIdx}`);
@@ -80,297 +59,285 @@ async function runPass(runIdx) {
const browser = await chromium.launch({
executablePath: CHROME_EXE,
headless: true,
- args: ['--no-sandbox', '--disable-web-security']
+ args: ['--no-sandbox']
});
const ctx = await browser.newContext({
viewport: { width: 1600, height: 900 },
- recordVideo: { dir: recDir, size: { width: 1600, height: 900 } },
- userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
+ recordVideo: { dir: recDir, size: { width: 1600, height: 900 } }
});
const page = await ctx.newPage();
const consoleErrors = [];
const pageErrors = [];
+ const archiveRequests = [];
+ const archiveErrors = [];
+
page.on('console', m => {
- if (m.type() === 'error') consoleErrors.push(m.text().slice(0, 300));
+ const txt = m.text().slice(0, 300);
+ if (m.type() === 'error') consoleErrors.push(txt);
+ if (txt.includes('archive') || txt.includes('fetch') || txt.includes('Error')) {
+ consoleErrors.push(`[console:${m.type()}] ${txt}`);
+ }
});
page.on('pageerror', e => pageErrors.push(('' + e).slice(0, 300)));
-
- // Track network requests to archive.org
- const archiveRequests = [];
- const archiveErrors = [];
- page.on('requestfinished', async req => {
+ page.on('requestfailed', req => {
if (req.url().includes('archive.org')) {
- try {
- const resp = await req.response();
- archiveRequests.push({ url: req.url().slice(0, 100), status: resp ? resp.status() : null });
- } catch {
- archiveRequests.push({ url: req.url().slice(0, 100), status: 'err' });
- }
+ archiveErrors.push({ url: req.url().slice(0, 120), err: req.failure()?.errorText });
}
});
- page.on('requestfailed', req => {
- if (req.url().includes('archive.org')) archiveErrors.push({ url: req.url().slice(0, 100), err: req.failure()?.errorText });
+ page.on('response', resp => {
+ if (resp.url().includes('archive.org')) {
+ archiveRequests.push({ url: resp.url().slice(0, 120), status: resp.status() });
+ }
});
const loadStart = Date.now();
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
const domReadyMs = Date.now() - loadStart;
+ await page.waitForTimeout(2000);
- // Wait for initial JS execution
- await page.waitForTimeout(3000);
- const postInitMs = Date.now() - loadStart;
-
- // Check bars immediately after load (before archive.org fills heights)
+ // Capture bar heights at load (BEFORE archive.org fills them)
const barsAtLoad = await page.evaluate(() => {
const bars = Array.from(document.querySelectorAll('.mr-yr'));
- return bars.map(b => ({ style: b.style.height || 'none', computed: window.getComputedStyle(b).height })).slice(0, 3);
+ return {
+ count: bars.length,
+ sampleStyles: bars.slice(0, 3).map(b => ({ style: b.style.height, computed: window.getComputedStyle(b).height }))
+ };
});
- append({
- run: runIdx, ts: new Date().toISOString(), selector: 'PAGE_LOAD', label: 'initial load',
+ append({ run: runIdx, ts: new Date().toISOString(), selector: 'PAGE_LOAD', label: 'page load',
action: 'navigate', ok: true,
- effect: `domReady=${domReadyMs}ms postInit=${postInitMs}ms barsAtLoad=${JSON.stringify(barsAtLoad)}`,
- errors: [...consoleErrors.splice(0), ...pageErrors.splice(0)]
- });
+ effect: `domReady=${domReadyMs}ms barsAtLoad=${JSON.stringify(barsAtLoad)}`,
+ errors: [...consoleErrors.splice(0), ...pageErrors.splice(0)] });
- // ---- Step 1: Click hamburger to reveal panel stack ----
- const beforeHamburger = await page.screenshot({ clip: { x: 1400, y: 0, width: 200, height: 80 } });
+ // ---- Step 1: Open hamburger ----
let hamburgerOk = false;
try {
- await page.click('#chromeBtn', { timeout: 5000 });
+ await page.click('#chromeBtn', { force: true, timeout: 5000 });
await page.waitForTimeout(800);
hamburgerOk = true;
- } catch (e) {
- consoleErrors.push('hamburger click failed: ' + e.message.slice(0, 100));
+ } catch(e) {
+ append({ run: runIdx, ts: new Date().toISOString(), selector: '#chromeBtn', label: 'hamburger',
+ action: 'click', ok: false, effect: 'FAILED: ' + e.message.slice(0,100), errors: [] });
}
- const afterHamburger = await page.screenshot({ clip: { x: 0, y: 0, width: 500, height: 400 } });
- append({
- run: runIdx, ts: new Date().toISOString(), selector: '#chromeBtn', label: 'hamburger ☰ Controls',
- action: 'click', ok: hamburgerOk,
- effect: `panel_revealed=${hamburgerOk}`,
- errors: [...consoleErrors.splice(0), ...pageErrors.splice(0)]
- });
+ append({ run: runIdx, ts: new Date().toISOString(), selector: '#chromeBtn', label: 'hamburger ☰ Controls',
+ action: 'click', ok: hamburgerOk, effect: `panel_revealed=${hamburgerOk}`,
+ errors: [...consoleErrors.splice(0)] });
+
+ await page.screenshot({ path: path.join(recDir, '01-after-hamburger.png') });
- // ---- Discover all interactive elements (now panel is open) ----
- const elements = await page.evaluate(() => {
- const els = [];
+ // ---- Discover all visible elements after panel open ----
+ // IMPORTANT: We do NOT include #mrToggle in click list (it would collapse the music room section)
+ const rawEls = await page.evaluate(() => {
+ const result = [];
const seen = new Set();
- // Priority elements: hamburger, MR panel controls, year bars, show controls
- const selectors = [
- { sel: '#chromeBtn', label: 'hamburger toggle' },
- { sel: '#mrToggle', label: 'Music Room collapse toggle' },
- { sel: '#mrSearch', label: 'search box' },
- { sel: '#mrAll', label: 'All years button' },
- { sel: '.mr-yr:nth-child(1)', label: 'year bar 1965' },
- { sel: '.mr-yr:nth-child(6)', label: 'year bar 1970' },
- { sel: '.mr-yr:nth-child(11)', label: 'year bar 1975' },
- { sel: '.mr-yr:nth-child(16)', label: 'year bar 1980' },
- { sel: '.mr-yr:nth-child(21)', label: 'year bar 1985' },
- { sel: '.mr-yr:nth-child(26)', label: 'year bar 1990' },
- { sel: '.mr-yr:nth-child(31)', label: 'year bar 1995' },
- { sel: '#mrShow', label: 'show select dropdown' },
- { sel: '#mrPrev', label: 'prev show ⏮' },
- { sel: '#mrPlay', label: 'play button ▶' },
- { sel: '#mrNext', label: 'next show ⏭' },
- { sel: '#mrTracksToggle', label: 'Tracks toggle' },
- { sel: '#mrSeek', label: 'seek slider' },
- { sel: '#playBtn', label: 'scene pause/play' },
- { sel: '#restartBtn', label: 'restart scene' },
- { sel: '#reduceBtn', label: 'reduced motion' },
- { sel: '#trailBtn', label: 'motion trails' },
- { sel: '#reactBtn', label: 'react to music' },
- { sel: '#pteroBtn', label: 'fly over dino' },
- { sel: '#venueBtn', label: 'venue button' },
- { sel: '#tempo', label: 'tempo slider' },
- { sel: '#lantern', label: 'lantern slider' },
- { sel: '#crowd', label: 'crowd slider' },
- { sel: '#animals', label: 'animals slider' },
- { sel: '#bears', label: 'bears slider' },
- { sel: '#labelBtn', label: 'museum label toggle' },
- { sel: '#backlink', label: 'back to main stage' },
- { sel: '[id=""][class=""][text="I · Departure"]', label: 'chapter I departure' },
- ];
-
- // Also enumerate all remaining buttons dynamically
- document.querySelectorAll('button, [role=button], header[id]').forEach(el => {
- const id = el.id ? `#${el.id}` : null;
- const cls = el.className ? `.${el.className.trim().split(/\s+/).join('.')}` : null;
- const key = id || cls || el.tagName;
- if (!seen.has(key)) {
- seen.add(key);
- const rect = el.getBoundingClientRect();
- els.push({
- selector: id || (cls ? (el.tagName.toLowerCase() + cls) : el.tagName.toLowerCase()),
- label: el.textContent?.trim().slice(0, 50) || id || 'unlabeled',
- type: el.tagName.toLowerCase() === 'input' ? el.type : 'button',
- rect: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
- visible: rect.width > 0 && rect.height > 0
- });
- }
+ document.querySelectorAll('button, [role=button], input, select, a[href]').forEach(el => {
+ const id = el.id;
+ const cls = el.className.toString().trim();
+ const key = id ? `#${id}` : (cls ? `${el.tagName.toLowerCase()}.${cls.split(/\s+/).join('.')}` : null);
+ if (!key || seen.has(key)) return;
+ seen.add(key);
+
+ const rect = el.getBoundingClientRect();
+ if (rect.width === 0 && rect.height === 0) return; // skip invisible
+
+ result.push({
+ selector: id ? `#${id}` : `${el.tagName.toLowerCase()}.${cls.split(/\s+/)[0]}`,
+ label: el.textContent?.trim().slice(0, 50) || el.id || el.placeholder || 'unlabeled',
+ type: el.tagName === 'INPUT' ? el.type : (el.tagName === 'SELECT' ? 'select' : 'button'),
+ rect: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) }
+ });
});
- // Inputs
- document.querySelectorAll('input, select').forEach(el => {
- const id = el.id ? `#${el.id}` : null;
- if (id && !seen.has(id)) {
- seen.add(id);
- const rect = el.getBoundingClientRect();
- els.push({
- selector: id,
- label: el.id || 'input',
- type: el.type || 'select',
- rect: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
- visible: rect.width > 0 && rect.height > 0
- });
- }
+ return result;
+ });
+
+ // Filter out navigation links (would leave page), and mrToggle (would collapse music room section)
+ const els = rawEls.filter(e =>
+ e.selector !== '#backlink' &&
+ e.selector !== '#mrToggle' &&
+ e.selector !== '#chromeBtn' // already clicked
+ );
+
+ console.log(` Run ${runIdx}: ${els.length} elements to interact with`);
+
+ // ---- Year bar measurements BEFORE clicking ----
+ const yearBarMeasure = await page.evaluate(() => {
+ const bars = Array.from(document.querySelectorAll('.mr-yr'));
+ const containerEl = bars[0]?.parentElement;
+ if (!containerEl) return { found: false };
+ const cRect = containerEl.getBoundingClientRect();
+ const barRects = bars.map(b => {
+ const r = b.getBoundingClientRect();
+ return { text: b.textContent?.trim(), x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) };
});
+ return {
+ found: true,
+ containerW: Math.round(cRect.width),
+ containerScrollW: containerEl.scrollWidth,
+ overflows: containerEl.scrollWidth > cRect.width + 2,
+ barCount: bars.length,
+ firstBar: barRects[0],
+ lastBar: barRects[barRects.length - 1],
+ allBarsW11: barRects.every(b => b.w === 11),
+ narrowBars: barRects.filter(b => b.w < 20).length,
+ sampleRects: barRects.slice(0, 3)
+ };
+ });
+
+ append({ run: runIdx, ts: new Date().toISOString(), selector: 'YEAR_MAP_MEASURE',
+ label: 'year bar measurements', action: 'measure', ok: true,
+ effect: JSON.stringify(yearBarMeasure), errors: [] });
+
+ console.log(` Year bars: count=${yearBarMeasure.barCount} overflow=${yearBarMeasure.overflows} narrow=${yearBarMeasure.narrowBars} first=${JSON.stringify(yearBarMeasure.firstBar)}`);
- return els;
+ // ---- "All years" button size ----
+ const allBtnMeasure = await page.evaluate(() => {
+ const btn = document.querySelector('#mrAll');
+ if (!btn) return { found: false };
+ const r = btn.getBoundingClientRect();
+ return { found: true, w: Math.round(r.width), h: Math.round(r.height), x: Math.round(r.x), y: Math.round(r.y), text: btn.textContent?.trim() };
});
- const visibleEls = elements.filter(e => e.visible);
- console.log(` Run ${runIdx}: ${visibleEls.length} visible elements, ${elements.length} total`);
+ append({ run: runIdx, ts: new Date().toISOString(), selector: '#mrAll:measure',
+ label: '"All years" button size', action: 'measure', ok: true,
+ effect: JSON.stringify(allBtnMeasure), errors: [] });
- // Apply run-specific ordering
- const ordered = orderFor(runIdx, visibleEls);
+ // ---- Seek slider height ----
+ const seekMeasure = await page.evaluate(() => {
+ const el = document.querySelector('#mrSeek');
+ if (!el) return { found: false };
+ const r = el.getBoundingClientRect();
+ return { found: true, w: Math.round(r.width), h: Math.round(r.height) };
+ });
- // ---- Iterate and click/interact every element ----
- for (const el of ordered) {
- const errsBefore = [...consoleErrors];
- const pageErrsBefore = [...pageErrors];
+ append({ run: runIdx, ts: new Date().toISOString(), selector: '#mrSeek:measure',
+ label: 'seek slider dimensions', action: 'measure', ok: true,
+ effect: JSON.stringify(seekMeasure), errors: [] });
- // Capture before-state for key elements
- let beforeState = '';
- try {
- if (el.selector === '#mrAll') {
- beforeState = await page.evaluate(() => {
- const btn = document.querySelector('#mrAll');
- return btn ? `classList=${btn.className} text=${btn.textContent?.trim()}` : 'not found';
- });
- } else if (el.selector && el.selector.includes('mr-yr')) {
- beforeState = await page.evaluate(sel => {
- const btn = document.querySelector(sel);
- return btn ? `classList=${btn.className} aria=${btn.getAttribute('aria-pressed')||'none'}` : 'not found';
- }, el.selector);
- }
- } catch {}
+ // ---- Header / label visibility check ----
+ const headerCheck = await page.evaluate(() => {
+ const header = document.querySelector('.mr-header, [class*=header]');
+ const label = document.querySelector('[aria-label*="Touring"], [aria-label*="year"], h2, h3');
+ const allEl = document.querySelector('#mrAll');
+ const searchEl = document.querySelector('#mrSearch');
+ return {
+ hasHeader: !!header,
+ headerText: header?.textContent?.trim().slice(0, 80),
+ hasLabel: !!label,
+ labelText: label?.textContent?.trim().slice(0, 80),
+ allBtnFound: !!allEl,
+ searchFound: !!searchEl,
+ bodyHTML_sample: document.querySelector('.musicroom')?.innerHTML?.slice(0, 600)
+ };
+ });
+ append({ run: runIdx, ts: new Date().toISOString(), selector: 'HEADER_CHECK',
+ label: 'header/label visibility', action: 'measure', ok: true,
+ effect: JSON.stringify(headerCheck), errors: [] });
+
+ // ---- Apply ordering for this run ----
+ const ordered = orderFor(runIdx, els);
+
+ // ---- Click/interact every element ----
+ for (const el of ordered) {
+ const errsBefore = consoleErrors.length;
let ok = false;
let effect = '';
let action = 'click';
let errMsg = '';
+ // Snapshot before for key elements
+ let beforeState = '';
+ if (el.selector === '#mrAll') {
+ beforeState = await page.evaluate(() => {
+ const b = document.querySelector('#mrAll');
+ return b ? `cls=${b.className} text=${b.textContent?.trim()}` : '';
+ });
+ }
+
try {
if (el.type === 'range') {
- // Sliders: drag to middle, then to each extreme
action = 'drag';
- const sliderEl = await page.$(el.selector);
- if (sliderEl) {
- await sliderEl.scrollIntoViewIfNeeded();
- const box = await sliderEl.boundingBox();
- if (box && box.width > 0) {
- // drag to 50%
- await page.mouse.move(box.x + box.width * 0.5, box.y + box.height * 0.5);
- await page.mouse.click(box.x + box.width * 0.5, box.y + box.height * 0.5);
- await page.waitForTimeout(200);
- // drag to max
- await page.mouse.move(box.x + box.width * 0.9, box.y + box.height * 0.5);
- await page.mouse.click(box.x + box.width * 0.9, box.y + box.height * 0.5);
- await page.waitForTimeout(200);
- ok = true;
- effect = `slider dragged box=${JSON.stringify(box)}`;
- } else {
- effect = `slider not visible/zero-width box=${JSON.stringify(box)}`;
- }
- }
- } else if (el.selector === '#mrSearch') {
+ // Click at center, then at 80% position using coordinates
+ const cx = el.rect.x + el.rect.w * 0.5;
+ const cy = el.rect.y + el.rect.h * 0.5;
+ const r8 = el.rect.x + el.rect.w * 0.8;
+ await page.mouse.click(cx, cy);
+ await page.waitForTimeout(100);
+ await page.mouse.click(r8, cy);
+ await page.waitForTimeout(100);
+ const val = await page.evaluate(sel => { const e = document.querySelector(sel); return e?.value; }, el.selector);
+ effect = `dragged box=${el.rect.w}x${el.rect.h} val=${val}`;
+ ok = true;
+ } else if (el.type === 'search' || el.selector === '#mrSearch') {
action = 'type';
- const input = await page.$('#mrSearch');
- if (input) {
- await input.scrollIntoViewIfNeeded();
- await input.click();
- await input.fill('1972');
- await page.waitForTimeout(500);
- const results = await page.evaluate(() => {
- const list = document.querySelectorAll('#mrList option, .mr-list option, .mr-result');
- return list.length;
- });
- effect = `typed 1972, results visible: ${results}`;
- await input.fill(''); // clear
- ok = true;
- }
- } else if (el.selector === '#mrShow') {
+ await page.mouse.click(el.rect.x + el.rect.w * 0.5, el.rect.y + el.rect.h * 0.5);
+ await page.waitForTimeout(200);
+ await page.keyboard.type('1972');
+ await page.waitForTimeout(600);
+ const srchResult = await page.evaluate(() => {
+ const sel = document.querySelector('#mrShow');
+ return sel ? `${Array.from(sel.options).length} options visible` : 'select not found';
+ });
+ effect = `typed '1972', results: ${srchResult}`;
+ // Clear
+ await page.keyboard.selectAll();
+ await page.keyboard.press('Backspace');
+ await page.waitForTimeout(300);
+ ok = true;
+ } else if (el.type === 'select') {
action = 'select';
- const select = await page.$('#mrShow');
- if (select) {
- await select.scrollIntoViewIfNeeded();
- const opts = await page.evaluate(() => {
- const s = document.querySelector('#mrShow');
- return s ? Array.from(s.options).length : 0;
- });
- effect = `select has ${opts} options`;
- // Pick option at index 2 if it exists
- if (opts > 2) {
- await page.selectOption('#mrShow', { index: 2 });
- await page.waitForTimeout(500);
- }
- ok = true;
+ const optCount = await page.evaluate(sel => {
+ const s = document.querySelector(sel);
+ return s ? Array.from(s.options).length : 0;
+ }, el.selector);
+ effect = `${optCount} options`;
+ if (optCount > 1) {
+ // Pick index 1 (second item)
+ await page.selectOption(el.selector, { index: 1 });
+ await page.waitForTimeout(500);
}
- } else if (['#backlink'].includes(el.selector)) {
- // Don't navigate away — just note it
- action = 'skip-navigate';
ok = true;
- effect = 'navigation link — skipped to avoid leaving page';
} else {
- // Standard click
- const elHandle = await page.$(el.selector);
- if (elHandle) {
- await elHandle.scrollIntoViewIfNeeded();
- const box = await elHandle.boundingBox();
- if (box) {
- effect = `box=${Math.round(box.width)}x${Math.round(box.height)} at (${Math.round(box.x)},${Math.round(box.y)})`;
- }
- await elHandle.click({ timeout: 3000 });
- await page.waitForTimeout(400);
- ok = true;
+ // Click by coordinates (bypasses scrollIntoView stability checks)
+ const cx = el.rect.x + el.rect.w * 0.5;
+ const cy = el.rect.y + el.rect.h * 0.5;
+ if (cx < 0 || cy < 0 || cx > 1600 || cy > 900) {
+ effect = `out of viewport at (${cx},${cy}) -- skipped`;
+ ok = false;
} else {
- effect = 'element not found in DOM at click time';
+ await page.mouse.click(cx, cy);
+ await page.waitForTimeout(350);
+ effect = `clicked at (${Math.round(cx)},${Math.round(cy)}) box=${el.rect.w}x${el.rect.h}`;
+ ok = true;
}
}
- } catch (e) {
+ } catch(e) {
errMsg = e.message.slice(0, 200);
effect = `ERROR: ${errMsg}`;
}
// Capture after-state
let afterState = '';
- try {
- if (el.selector === '#mrAll') {
- afterState = await page.evaluate(() => {
- const btn = document.querySelector('#mrAll');
- const yr = document.querySelector('#mrYear');
- return btn ? `classList=${btn.className} yearInput=${yr?.value}` : 'not found';
- });
- } else if (el.type === 'range') {
- afterState = await page.evaluate(sel => {
- const inp = document.querySelector(sel);
- return inp ? `value=${inp.value}` : 'not found';
- }, el.selector);
- } else if (el.selector === '#mrPlay' || el.selector === '#playBtn') {
- afterState = await page.evaluate(() => {
- const mp = document.querySelector('#mrPlay');
- return mp ? `mrPlay text=${mp.textContent?.trim()}` : '';
- });
- }
- } catch {}
+ if (el.selector === '#mrAll') {
+ afterState = await page.evaluate(() => {
+ const b = document.querySelector('#mrAll');
+ const yi = document.querySelector('#mrYear');
+ return b ? `cls=${b.className} text=${b.textContent?.trim()} yearVal=${yi?.value}` : '';
+ });
+ } else if (el.selector && el.selector.match(/mrPlay|playBtn/)) {
+ afterState = await page.evaluate(() => {
+ const mp = document.querySelector('#mrPlay');
+ return mp ? `mrPlay=${mp.textContent?.trim()}` : '';
+ });
+ }
- const newErrs = consoleErrors.slice(errsBefore.length);
- const newPageErrs = pageErrors.slice(pageErrsBefore.length);
- const allNewErrs = [...newErrs, ...newPageErrs];
- if (errMsg) allNewErrs.push(errMsg);
+ const newErrs = consoleErrors.slice(errsBefore);
+ const newPageErrs = [...pageErrors.splice(0)];
+ const allNewErrs = [...newErrs, ...newPageErrs, ...(errMsg ? [errMsg] : [])];
const logEntry = {
run: runIdx,
@@ -379,176 +346,220 @@ async function runPass(runIdx) {
label: el.label,
action,
ok,
- effect: `${effect}${afterState ? ` | after:${afterState}` : ''}${beforeState ? ` | before:${beforeState}` : ''}`,
+ effect: [effect, afterState ? `after:${afterState}` : '', beforeState ? `before:${beforeState}` : ''].filter(Boolean).join(' | '),
errors: allNewErrs
};
if (allNewErrs.length) {
- console.log(` [RUN${runIdx}] ERROR ${el.label}: ${allNewErrs[0]}`);
+ console.log(` [R${runIdx}] ERR ${el.label.slice(0,30)}: ${allNewErrs[0].slice(0,80)}`);
} else {
- console.log(` [RUN${runIdx}] OK ${el.label}: ${effect.slice(0, 80)}`);
+ console.log(` [R${runIdx}] OK ${el.label.slice(0,30)}: ${effect.slice(0, 60)}`);
}
append(logEntry);
- // After clicking mrAll or a year bar, capture quick state snapshot
- if (el.selector === '#mrAll' || (el.selector && el.selector.includes('mr-yr'))) {
- try {
- const yrState = await page.evaluate(() => {
- const allBtn = document.querySelector('#mrAll');
- const yearBars = Array.from(document.querySelectorAll('.mr-yr'));
- const active = yearBars.filter(b => b.classList.contains('active') || b.getAttribute('aria-pressed') === 'true');
- const resultList = document.querySelector('#mrList, .mr-results, [id*=List]');
- return {
- allBtnClasses: allBtn?.className,
- activeBarCount: active.length,
- activeBarTexts: active.map(b => b.textContent?.trim()).slice(0, 3),
- resultListCount: resultList ? resultList.querySelectorAll('option, li, .mr-result').length : 'N/A'
- };
- });
- append({
- run: runIdx, ts: new Date().toISOString(),
- selector: el.selector + ':state-snapshot', label: 'year map state after click',
- action: 'snapshot', ok: true,
- effect: JSON.stringify(yrState), errors: []
- });
- } catch {}
+ // After clicking a year bar, record state
+ if (el.selector && (el.selector.includes('mr-yr') || el.label.match(/196[0-9]|197[0-9]|198[0-9]|199[0-9]/))) {
+ const yrState = await page.evaluate(() => {
+ const allBtn = document.querySelector('#mrAll');
+ const bars = Array.from(document.querySelectorAll('.mr-yr'));
+ const active = bars.filter(b => b.classList.contains('active') || b.getAttribute('aria-pressed') === 'true');
+ return {
+ allBtnCls: allBtn?.className,
+ activeCount: active.length,
+ activeTexts: active.map(b => b.textContent?.trim())
+ };
+ });
+ append({ run: runIdx, ts: new Date().toISOString(),
+ selector: el.selector + ':yrstate', label: 'year state snapshot',
+ action: 'snapshot', ok: true, effect: JSON.stringify(yrState), errors: [] });
}
}
- // ---- Post-pass: check for bar height timing ----
- await page.waitForTimeout(5000); // wait extra for archive.org responses
- const archiveStatus = await page.evaluate(() => {
+ // ---- Screenshot mid-run ----
+ await page.screenshot({ path: path.join(recDir, '02-mid-run.png') });
+
+ // ---- Wait for archive.org bar height animation ----
+ console.log(` Waiting 6s for archive.org bar heights to load...`);
+ const waitStart = Date.now();
+ await page.waitForTimeout(6000);
+
+ const barsAfterWait = await page.evaluate(() => {
const bars = Array.from(document.querySelectorAll('.mr-yr'));
- const heights = bars.map(b => b.style.height || window.getComputedStyle(b).height);
- const nonZero = heights.filter(h => h && h !== '0px' && h !== 'none');
- const barSpans = Array.from(document.querySelectorAll('.mr-yr .bar, .bar'));
- const spanHeights = barSpans.map(s => window.getComputedStyle(s).height);
+ const barSpans = Array.from(document.querySelectorAll('.mr-yr .bar, .mr-yr > span'));
return {
barCount: bars.length,
- nonZeroHeightCount: nonZero.length,
- sampleHeights: heights.slice(0, 5),
- barSpanCount: barSpans.length,
- sampleSpanHeights: spanHeights.slice(0, 5)
+ sampleParentStyles: bars.slice(0, 5).map(b => ({ style: b.style.height, computed: window.getComputedStyle(b).height })),
+ spanCount: barSpans.length,
+ sampleSpanHeights: barSpans.slice(0, 5).map(s => window.getComputedStyle(s).height),
+ hasRealHeights: bars.some(b => b.style.height && b.style.height !== '0px' && b.style.height !== 'auto'),
+ archiveDataAttr: bars.slice(0,3).map(b => ({
+ dataCount: b.getAttribute('data-count'),
+ dataYear: b.textContent?.trim()
+ }))
};
});
- append({
- run: runIdx, ts: new Date().toISOString(),
- selector: 'YEAR_BARS', label: 'bar height timing check (5s after load)',
- action: 'measure', ok: true,
- effect: JSON.stringify(archiveStatus),
- errors: [...archiveErrors.map(r => `archive.org FAIL: ${r.url} ${r.err}`)]
- });
+ append({ run: runIdx, ts: new Date().toISOString(), selector: 'BAR_HEIGHT_TIMING',
+ label: `bar heights after ${Math.round((Date.now() - waitStart + 6000) / 1000)}s wait`,
+ action: 'measure', ok: true, effect: JSON.stringify(barsAfterWait),
+ errors: archiveErrors.map(e => `archive FAIL: ${e.url} ${e.err}`) });
- // ---- Check for horizontal overflow (all 31 bars visible?) ----
- const overflowCheck = await page.evaluate(() => {
- const container = document.querySelector('.mr-map, .year-map, [class*=map], [id*=map], .mr-yr')?.parentElement;
- if (!container) return { found: false };
- const cRect = container.getBoundingClientRect();
- const bars = Array.from(container.querySelectorAll('.mr-yr'));
- const lastBar = bars[bars.length - 1];
- const lastRect = lastBar ? lastBar.getBoundingClientRect() : null;
- return {
- found: true,
- containerW: Math.round(cRect.width),
- containerScrollW: container.scrollWidth,
- containerOverflows: container.scrollWidth > cRect.width,
- barCount: bars.length,
- firstBarX: bars[0] ? Math.round(bars[0].getBoundingClientRect().x) : null,
- lastBarX: lastRect ? Math.round(lastRect.x + lastRect.width) : null,
- lastBarVisible: lastRect ? lastRect.right <= cRect.right + 2 : null
- };
- });
+ console.log(` Archive reqs: ${archiveRequests.length} ok, ${archiveErrors.length} fail`);
+ console.log(` Bars have real heights: ${barsAfterWait.hasRealHeights}`);
- append({
- run: runIdx, ts: new Date().toISOString(),
- selector: 'YEAR_MAP_CONTAINER', label: 'horizontal overflow check',
- action: 'measure', ok: true,
- effect: JSON.stringify(overflowCheck),
- errors: []
+ // ---- Click a specific year bar to test selection feedback ----
+ const bar1980 = await page.evaluate(() => {
+ const bars = Array.from(document.querySelectorAll('.mr-yr'));
+ const b = bars.find(b => b.textContent?.trim() === '1980') || bars[15];
+ if (!b) return null;
+ const r = b.getBoundingClientRect();
+ return { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) };
});
- // ---- Check "All years" reset after year selection ----
- const resetCheck = await page.evaluate(() => {
- const allBtn = document.querySelector('#mrAll');
- const yearInput = document.querySelector('#mrYear');
- return {
- allBtnClasses: allBtn?.className,
- yearInputVal: yearInput?.value,
- allBtnText: allBtn?.textContent?.trim()
- };
- });
+ if (bar1980) {
+ const cx = bar1980.x + bar1980.w * 0.5;
+ const cy = bar1980.y + bar1980.h * 0.5;
+ console.log(` Clicking 1980 bar at (${cx},${cy}) size ${bar1980.w}x${bar1980.h}`);
+
+ // Take screenshot before click
+ await page.screenshot({ path: path.join(recDir, '03-before-year-click.png') });
+
+ await page.mouse.click(cx, cy);
+ await page.waitForTimeout(1000);
+
+ // Check selection feedback
+ const selectionFeedback = await page.evaluate(() => {
+ const bars = Array.from(document.querySelectorAll('.mr-yr'));
+ const active = bars.filter(b => b.classList.contains('active') || b.getAttribute('aria-pressed') === 'true' || b.style.opacity === '1' || b.style.background);
+ const resultList = document.querySelector('#mrList, [id*=List], [class*=result]');
+ const header = document.querySelector('.mr-header, h3, .section-label');
+ return {
+ activeBarCount: active.length,
+ activeBarTexts: active.map(b => b.textContent?.trim()),
+ resultItems: resultList ? resultList.querySelectorAll('option, li, .item').length : 0,
+ hasVisualFeedback: active.length > 0,
+ headerText: header?.textContent?.trim()
+ };
+ });
- append({
- run: runIdx, ts: new Date().toISOString(),
- selector: 'RESET_STATE', label: 'all-years reset state check end of run',
- action: 'measure', ok: true,
- effect: JSON.stringify(resetCheck),
- errors: []
- });
+ append({ run: runIdx, ts: new Date().toISOString(), selector: '.mr-yr[1980]',
+ label: 'click 1980 bar — selection feedback', action: 'click', ok: true,
+ effect: JSON.stringify(selectionFeedback), errors: [...consoleErrors.splice(0)] });
+
+ console.log(` Selection feedback: ${JSON.stringify(selectionFeedback)}`);
- // ---- Final screenshot for visual record ----
- const screenshotPath = path.join(recDir, 'final-state.png');
- await page.screenshot({ path: screenshotPath, fullPage: false });
+ await page.screenshot({ path: path.join(recDir, '04-after-year-click.png') });
+
+ // Click "All years" to reset
+ const allRect = await page.evaluate(() => {
+ const b = document.querySelector('#mrAll');
+ if (!b) return null;
+ const r = b.getBoundingClientRect();
+ return { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) };
+ });
+
+ if (allRect) {
+ await page.mouse.click(allRect.x + allRect.w * 0.5, allRect.y + allRect.h * 0.5);
+ await page.waitForTimeout(500);
+ const afterReset = await page.evaluate(() => {
+ const b = document.querySelector('#mrAll');
+ const yi = document.querySelector('#mrYear');
+ return { allBtnCls: b?.className, yearVal: yi?.value };
+ });
+ append({ run: runIdx, ts: new Date().toISOString(), selector: '#mrAll:reset',
+ label: 'All years reset after 1980 click', action: 'click', ok: true,
+ effect: JSON.stringify(afterReset), errors: [] });
+ }
+ }
+
+ // ---- State leak check: reload and measure ----
+ // (done only on run 2 to check if selected year persists across reload)
+ if (runIdx === 2) {
+ const stateBeforeReload = await page.evaluate(() => {
+ return {
+ mrYearVal: document.querySelector('#mrYear')?.value,
+ allBtnCls: document.querySelector('#mrAll')?.className,
+ localStorageKeys: Object.keys(localStorage)
+ };
+ });
+
+ await page.reload({ waitUntil: 'domcontentloaded', timeout: 20000 });
+ await page.waitForTimeout(2000);
+ // Reopen panel
+ await page.click('#chromeBtn', { force: true, timeout: 5000 }).catch(() => {});
+ await page.waitForTimeout(500);
+
+ const stateAfterReload = await page.evaluate(() => {
+ return {
+ mrYearVal: document.querySelector('#mrYear')?.value,
+ allBtnCls: document.querySelector('#mrAll')?.className,
+ localStorageKeys: Object.keys(localStorage)
+ };
+ });
+
+ append({ run: runIdx, ts: new Date().toISOString(), selector: 'STATE_LEAK_CHECK',
+ label: 'state leak: reload test', action: 'reload', ok: true,
+ effect: `before=${JSON.stringify(stateBeforeReload)} after=${JSON.stringify(stateAfterReload)}`,
+ errors: [] });
+ console.log(` State leak check: before=${JSON.stringify(stateBeforeReload)} after=${JSON.stringify(stateAfterReload)}`);
+ }
+
+ // ---- Final screenshot ----
+ await page.screenshot({ path: path.join(recDir, '05-final.png') });
- // Close and let video finalize
await ctx.close();
await browser.close();
- // Convert webm to mp4 if ffmpeg available
+ // Convert webm -> mp4
const recFiles = fs.readdirSync(recDir).filter(f => f.endsWith('.webm'));
+ const mp4Files = [];
for (const webm of recFiles) {
const webmPath = path.join(recDir, webm);
const mp4Path = webmPath.replace('.webm', '.mp4');
try {
- const { execSync } = require('child_process');
- execSync(`ffmpeg -y -i "${webmPath}" -c:v libx264 -preset fast -crf 28 "${mp4Path}" 2>/dev/null`, { timeout: 60000 });
- console.log(` Converted: ${mp4Path}`);
- } catch (e) {
- console.log(` ffmpeg convert failed for ${webm}: ${e.message.slice(0, 100)}`);
+ execSync(`ffmpeg -y -i "${webmPath}" -c:v libx264 -preset fast -crf 28 "${mp4Path}" 2>/dev/null`, { timeout: 90000 });
+ mp4Files.push(mp4Path);
+ console.log(` Converted: ${path.basename(mp4Path)}`);
+ } catch(e) {
+ console.log(` ffmpeg failed: ${e.message.slice(0,80)}`);
+ mp4Files.push(webmPath); // keep webm reference
}
}
- // Summary
- const runEntries = prior.filter(e => e.run === runIdx);
- const runErrors = runEntries.filter(e => e.errors && e.errors.length > 0);
- console.log(` Run ${runIdx} complete. Archive requests: ${archiveRequests.length}, errors: ${archiveErrors.length}`);
- console.log(` Bar height status: ${JSON.stringify(archiveStatus)}`);
-
return {
- runIdx,
+ runIdx, recDir,
+ recordings: mp4Files,
archiveRequests: archiveRequests.length,
archiveErrors: archiveErrors.length,
- archiveStatus,
- overflowCheck,
- resetCheck,
- screenshotPath,
- recDir,
- recFiles: fs.readdirSync(recDir)
+ archiveStatus: barsAfterWait,
+ yearBarMeasure,
+ allBtnMeasure,
+ seekMeasure,
+ headerCheck
};
}
-// Main
(async () => {
const results = [];
for (let r = 0; r < 5; r++) {
const res = await runPass(r);
results.push(res);
- // Brief pause between runs
- if (r < 4) await new Promise(resolve => setTimeout(resolve, 2000));
+ if (r < 4) await new Promise(resolve => setTimeout(resolve, 1500));
}
- console.log('\n===== ALL RUNS COMPLETE =====');
+ console.log('\n===== ALL 5 RUNS COMPLETE =====');
for (const res of results) {
- console.log(`Run ${res.runIdx}: archive=${res.archiveRequests} reqs, ${res.archiveErrors} errs, overflow=${JSON.stringify(res.overflowCheck)}`);
+ console.log(`Run ${res.runIdx}: archive=${res.archiveRequests} reqs/${res.archiveErrors} errs recs=${res.recordings.map(p => path.basename(p))}`);
}
- // Output results as JSON for report generation
- fs.writeFileSync('/Users/macstudio3/Projects/dead-agentabrams/screenrecord/run-results.json', JSON.stringify(results, null, 2));
- console.log('\nRun results saved to screenrecord/run-results.json');
- console.log('Log saved to ' + LOG);
+ fs.writeFileSync(
+ '/Users/macstudio3/Projects/dead-agentabrams/screenrecord/run-results.json',
+ JSON.stringify(results, null, 2)
+ );
+ console.log('\nLog: ' + LOG);
+ console.log('Results: /Users/macstudio3/Projects/dead-agentabrams/screenrecord/run-results.json');
})().catch(e => {
- console.error('FATAL:', e.message);
+ console.error('FATAL:', e.message, e.stack?.slice(0, 400));
process.exit(1);
});
← f1c7dbc Expand curated shows to 17 (1969–1994, verified LMA soundboa
·
back to Dead Agentabrams
·
dock UI: lit-lantern active state (soft gold wash, not solid c1728cc →