← back to Dead Agentabrams
Add ROOM 01: Scarlet Riverboat Masquerade animated listening room at /room/ (streams the same Live Music Archive catalog); wire homepage card + nav link + cover, support internal room links, add favicon
4056d9c6ef046117b3215f467d64f30174d46628 · 2026-09-01 17:14:49 -0700 · Steve Abrams
Files touched
M index.htmlA room/cover.pngA room/index.html
Diff
commit 4056d9c6ef046117b3215f467d64f30174d46628
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Sep 1 17:14:49 2026 -0700
Add ROOM 01: Scarlet Riverboat Masquerade animated listening room at /room/ (streams the same Live Music Archive catalog); wire homepage card + nav link + cover, support internal room links, add favicon
---
index.html | 10 +-
room/cover.png | Bin 0 -> 225856 bytes
room/index.html | 1225 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 1231 insertions(+), 4 deletions(-)
diff --git a/index.html b/index.html
index b16472e..7d58b8c 100644
--- a/index.html
+++ b/index.html
@@ -3,8 +3,9 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
-<meta name="description" content="Five rooms, five eras, and five source-hosted Grateful Dead recordings from the Live Music Archive.">
+<meta name="description" content="Six rooms, five eras, and five source-hosted Grateful Dead recordings from the Live Music Archive — including an animated riverboat listening room.">
<title>DEAD · A Listening Exhibition</title>
+<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' fill='%23090908'/%3E%3Cpath d='M8 3l4 5-4 5-4-5z' fill='%23d7af59'/%3E%3C/svg%3E">
<style>
:root{--ink:#f2ead8;--muted:#aaa18e;--night:#090908;--panel:#11110f;--line:#343127;--gold:#d7af59;--rose:#bd5b60;--blue:#538da2;--cols:3;--serif:Georgia,"Times New Roman",serif;--sans:Inter,Helvetica,Arial,sans-serif;--mono:"SFMono-Regular",Consolas,monospace}
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--night);color:var(--ink);font-family:var(--sans)}button,a,select,input{font:inherit}a{color:inherit}button{color:inherit}.grain{position:fixed;inset:0;pointer-events:none;z-index:50;opacity:.065;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.6'/%3E%3C/svg%3E")}
@@ -20,9 +21,9 @@
</head>
<body>
<div class="grain"></div>
-<header class="top"><div class="mark"><i>◆</i> DEAD</div><nav class="nav"><a href="#listen">Listen</a><a href="#rooms">Gallery rooms</a><a href="#source">The source</a></nav></header>
+<header class="top"><div class="mark"><i>◆</i> DEAD</div><nav class="nav"><a href="#listen">Listen</a><a href="/room/">Riverboat room</a><a href="#rooms">Gallery rooms</a><a href="#source">The source</a></nav></header>
<main>
-<section class="hero"><div class="eyebrow">A listening exhibition · five rooms · five eras</div><h1>Long <em>strange</em></h1><p class="hero-copy">A visual archive with a pulse. Move through five decades of live sound, then enter the rooms the music inspired.</p><div class="hero-meta"><span>1969—1990</span><span>Community recordings</span><span>Source-hosted playback</span><span>No autoplay</span></div></section>
+<section class="hero"><div class="eyebrow">A listening exhibition · six rooms · five eras</div><h1>Long <em>strange</em></h1><p class="hero-copy">A visual archive with a pulse. Move through five decades of live sound, then enter the rooms the music inspired.</p><div class="hero-meta"><span>1969—1990</span><span>Community recordings</span><span>Source-hosted playback</span><span>No autoplay</span></div></section>
<section class="section" id="listen"><div class="section-head"><span class="k">01 / Listening room</span><h2>Choose a night.<br>Enter the room.</h2></div><p class="intro">Five landmark dates selected from Steve’s listen-only music source. Playback stays with the Internet Archive; every recording opens back to its original item page.</p><div class="player-shell"><div class="shows" id="shows"></div><div class="stage"><span class="date" id="stageDate"></span><h3 id="stageTitle"></h3><p class="note" id="stageNote"></p><div class="listen"><button class="listen-btn" id="listenBtn">Load source player</button><div class="frame-wrap" id="frameWrap"><iframe id="player" title="Internet Archive show player" loading="lazy" allow="autoplay"></iframe><div class="source-row"><span id="sourceMeta"></span><a id="sourceLink" target="_blank" rel="noopener">View recording at the Internet Archive ↗</a></div></div></div></div></div></section>
@@ -40,6 +41,7 @@ const shows=[
{year:1990,date:'March 29, 1990',title:'Nassau',sub:'Uniondale · late-era expansion',id:'gd90-03-29.aud-fob.set2.unknown.1317.sbeok.shnf',downloads:'231k+ source listens',note:'A late-period high-water mark where the band’s expanded palette meets the immediacy of a strong audience recording.'}
];
const rooms=[
+{n:'01',id:'riverboat-masquerade',title:'Scarlet Riverboat Masquerade',era:1969,img:'room/cover.png',url:'/room/',desc:'An animated listening room. A paddlewheel riverboat and masked dancers turn to a live show streamed from the archive — motion composed by hand, not by the waveform.'},
{n:'02',id:'long-strange-archive',title:'The Long Strange Archive',era:1965,img:'https://gallery.agentabrams.com/builds/long-strange-archive/cover.png',desc:'Handbills, tickets, photographs, fan objects, and a cultural timeline.'},
{n:'03',id:'deadhead-print-lab',title:'Deadhead Print Lab',era:1967,img:'https://gallery.agentabrams.com/builds/deadhead-print-lab/cover.png',desc:'A working psychedelic print studio of layers, ink, paper, and type.'},
{n:'04',id:'wall-of-sound',title:'Wall of Sound',era:1974,img:'https://gallery.agentabrams.com/builds/wall-of-sound/cover.png',desc:'Live sound as architecture, seen from four listening positions.'},
@@ -52,7 +54,7 @@ function renderShows(){ $('#shows').innerHTML=shows.map((s,i)=>`<button class="s
function selectShow(i){current=i;const s=shows[i];renderShows();$('#stageDate').textContent=s.date;$('#stageTitle').textContent=s.title;$('#stageNote').textContent=s.note;$('#frameWrap').classList.remove('on');$('#player').src='about:blank';$('#listenBtn').hidden=false;$('#listenBtn').textContent='Load source player';$('#sourceLink').href='https://archive.org/details/'+s.id;$('#sourceMeta').textContent=s.downloads+' · Community recording';localStorage.setItem('dead:selected-show',String(i));}
$('#shows').addEventListener('click',e=>{const b=e.target.closest('[data-i]');if(b)selectShow(Number(b.dataset.i));});
$('#listenBtn').addEventListener('click',()=>{const s=shows[current];$('#player').src='https://archive.org/embed/'+s.id;$('#frameWrap').classList.add('on');$('#listenBtn').hidden=true;});
-function renderRooms(){let list=rooms.slice();const sort=$('#sort').value;if(sort==='title')list.sort((a,b)=>a.title.localeCompare(b.title));if(sort==='era')list.sort((a,b)=>a.era-b.era);$('#builds').innerHTML=list.map(r=>`<a class="build" href="https://gallery.agentabrams.com/#${r.id}" target="_blank" rel="noopener"><div class="visual" style="background-image:url('${r.img}')"></div><div class="copy"><span class="n">ROOM ${r.n}</span><h3>${r.title}</h3><p>${r.desc}</p><span class="go">Enter exhibition ↗</span></div></a>`).join('');}
+function renderRooms(){let list=rooms.slice();const sort=$('#sort').value;if(sort==='title')list.sort((a,b)=>a.title.localeCompare(b.title));if(sort==='era')list.sort((a,b)=>a.era-b.era);$('#builds').innerHTML=list.map(r=>{const internal=!!r.url;const href=internal?r.url:('https://gallery.agentabrams.com/#'+r.id);const tgt=internal?'':' target="_blank" rel="noopener"';const go=internal?'Enter the room →':'Enter exhibition ↗';return `<a class="build" href="${href}"${tgt}><div class="visual" style="background-image:url('${r.img}')"></div><div class="copy"><span class="n">ROOM ${r.n}</span><h3>${r.title}</h3><p>${r.desc}</p><span class="go">${go}</span></div></a>`;}).join('');}
const savedShowRaw=localStorage.getItem('dead:selected-show');const savedShow=savedShowRaw===null?NaN:Number(savedShowRaw);if(Number.isInteger(savedShow)&&savedShow>=0&&savedShow<shows.length)current=savedShow;
const savedSort=localStorage.getItem('dead:sort');if(savedSort)$('#sort').value=savedSort;const savedDensity=localStorage.getItem('dead:density');if(savedDensity){$('#density').value=savedDensity;document.documentElement.style.setProperty('--cols',savedDensity)}
$('#sort').addEventListener('change',()=>{localStorage.setItem('dead:sort',$('#sort').value);renderRooms()});$('#density').addEventListener('input',()=>{const v=$('#density').value;document.documentElement.style.setProperty('--cols',v);localStorage.setItem('dead:density',v)});
diff --git a/room/cover.png b/room/cover.png
new file mode 100644
index 0000000..6f56303
Binary files /dev/null and b/room/cover.png differ
diff --git a/room/index.html b/room/index.html
new file mode 100644
index 0000000..d5fe2fd
--- /dev/null
+++ b/room/index.html
@@ -0,0 +1,1225 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<meta name="description" content="Scarlet Riverboat Masquerade — an authored, animated generative gallery of a paddlewheel riverboat drifting through a crimson canyon at dusk. Steer tempo, lanterns, crowd, animals, cameras and chapters.">
+<meta name="color-scheme" content="dark">
+<title>Scarlet Riverboat Masquerade — An Animated Gallery</title>
+<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect width='16' height='16' fill='%237c1522'/><circle cx='8' cy='6' r='3' fill='%23f2b64a'/></svg>">
+<style>
+ :root{
+ --ink:#f4e7d6;
+ --ink-dim:#c9a98c;
+ --gold:#f2b64a;
+ --crimson:#7c1522;
+ --panel:rgba(24,8,12,0.72);
+ --panel-edge:rgba(242,182,74,0.28);
+ --focus:#ffd27a;
+ --serif:"Iowan Old Style","Palatino Linotype",Palatino,"Book Antiqua",Georgia,serif;
+ --sans:"Avenir Next","Segoe UI",system-ui,-apple-system,sans-serif;
+ }
+ *{box-sizing:border-box}
+ html,body{margin:0;height:100%;background:#0a0406;color:var(--ink);font-family:var(--sans);overflow:hidden}
+ #stage{position:fixed;inset:0;display:block}
+ canvas{position:absolute;inset:0;width:100%;height:100%}
+
+ /* ---- Skip link ---- */
+ .skip{
+ position:fixed;top:-60px;left:12px;z-index:20;background:var(--gold);color:#2a0a0e;
+ padding:10px 16px;border-radius:8px;font-weight:600;text-decoration:none;transition:top .2s;
+ }
+ .skip:focus{top:12px}
+
+ /* ---- Back-link (only shown when hosted as a room of the DEAD listening exhibition) ---- */
+ .backlink{
+ position:fixed;top:24px;left:50%;transform:translateX(-50%);z-index:10;display:none;
+ align-items:center;gap:8px;text-decoration:none;pointer-events:auto;
+ font-family:var(--serif);font-style:italic;font-size:12.5px;color:var(--ink-dim);
+ background:var(--panel);border:1px solid var(--panel-edge);border-radius:999px;
+ padding:6px 15px;backdrop-filter:blur(6px);transition:color .3s,background .3s;
+ }
+ .backlink.on{display:inline-flex}
+ .backlink:hover{color:var(--gold);background:rgba(40,12,16,.86)}
+
+ /* ---- Title plate ---- */
+ .titleplate{
+ position:fixed;top:22px;left:26px;z-index:6;max-width:min(46vw,520px);
+ pointer-events:none;text-shadow:0 2px 18px rgba(0,0,0,.7);
+ }
+ .titleplate .kicker{
+ font-family:var(--sans);letter-spacing:.42em;text-transform:uppercase;
+ font-size:11px;color:var(--gold);opacity:.9;margin:0 0 6px 2px;
+ }
+ .titleplate h1{
+ font-family:var(--serif);font-weight:600;font-style:italic;
+ font-size:clamp(26px,4.4vw,52px);line-height:.98;margin:0;color:var(--ink);
+ }
+ .titleplate .chap{
+ font-family:var(--serif);font-size:14px;color:var(--ink-dim);margin-top:8px;font-style:italic;
+ }
+
+ /* ---- Chapter rail ---- */
+ .chapters{
+ position:fixed;top:22px;right:26px;z-index:6;display:flex;gap:8px;flex-direction:column;align-items:flex-end;
+ }
+ .chapters button{
+ font-family:var(--serif);font-size:13px;font-style:italic;
+ background:var(--panel);color:var(--ink-dim);border:1px solid transparent;
+ border-radius:999px;padding:7px 15px;cursor:pointer;backdrop-filter:blur(6px);
+ transition:color .3s,border-color .3s,background .3s;
+ display:flex;align-items:center;gap:8px;
+ }
+ .chapters button .dot{width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.6}
+ .chapters button[aria-current="true"]{color:var(--gold);border-color:var(--panel-edge);background:rgba(40,12,16,.86)}
+ .chapters button:hover{color:var(--ink)}
+
+ /* ---- Control dock ---- */
+ .dock{
+ position:fixed;left:50%;bottom:20px;transform:translateX(-50%);z-index:7;
+ display:flex;gap:18px;align-items:stretch;flex-wrap:wrap;justify-content:center;
+ max-width:min(96vw,1080px);
+ background:var(--panel);border:1px solid var(--panel-edge);border-radius:16px;
+ padding:14px 20px;backdrop-filter:blur(9px);box-shadow:0 12px 40px rgba(0,0,0,.5);
+ }
+ .group{display:flex;gap:12px;align-items:center}
+ .group + .group{border-left:1px solid rgba(242,182,74,.16);padding-left:18px}
+ .field{display:flex;flex-direction:column;gap:5px;min-width:96px}
+ .field label{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-dim)}
+ .field .val{font-family:var(--serif);font-style:italic;color:var(--gold);font-size:12px}
+ input[type=range]{
+ -webkit-appearance:none;appearance:none;height:4px;border-radius:4px;
+ background:linear-gradient(90deg,var(--crimson),var(--gold));outline:none;width:100%;
+ }
+ input[type=range]::-webkit-slider-thumb{
+ -webkit-appearance:none;width:16px;height:16px;border-radius:50%;
+ background:var(--ink);border:2px solid var(--gold);cursor:pointer;box-shadow:0 0 8px rgba(242,182,74,.6);
+ }
+ input[type=range]::-moz-range-thumb{
+ width:16px;height:16px;border-radius:50%;background:var(--ink);border:2px solid var(--gold);cursor:pointer;
+ }
+ .btn{
+ font-family:var(--sans);font-size:12px;letter-spacing:.05em;
+ background:rgba(40,12,16,.7);color:var(--ink);border:1px solid var(--panel-edge);
+ border-radius:10px;padding:9px 13px;cursor:pointer;transition:background .2s,color .2s;
+ display:inline-flex;align-items:center;gap:7px;white-space:nowrap;
+ }
+ .btn:hover{background:rgba(70,20,26,.9);color:var(--gold)}
+ .btn[aria-pressed="true"]{background:var(--gold);color:#2a0a0e;border-color:var(--gold);font-weight:600}
+ .toggle{display:flex;flex-direction:column;gap:6px}
+
+ /* ---- Camera pills ---- */
+ .cams{position:fixed;left:26px;bottom:20px;z-index:7;display:flex;gap:8px;flex-direction:column}
+ .cams .btn{justify-content:flex-start;min-width:118px}
+
+ /* ---- Museum label ---- */
+ .label-toggle{position:fixed;right:26px;bottom:20px;z-index:7}
+ .museum{
+ position:fixed;right:26px;bottom:66px;z-index:8;width:min(88vw,368px);max-height:74vh;overflow:auto;
+ background:linear-gradient(180deg,rgba(28,10,14,.96),rgba(16,6,9,.96));
+ border:1px solid var(--panel-edge);border-radius:14px;padding:20px 22px;
+ box-shadow:0 18px 50px rgba(0,0,0,.6);transform-origin:bottom right;
+ transition:opacity .3s,transform .3s;font-size:13px;line-height:1.55;color:var(--ink-dim);
+ }
+ .museum[hidden]{display:none}
+ .museum h2{font-family:var(--serif);font-style:italic;color:var(--gold);font-size:18px;margin:0 0 4px}
+ .museum .meta{font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-dim);margin-bottom:12px;opacity:.8}
+ .museum p{margin:0 0 10px}
+ .museum strong{color:var(--ink);font-weight:600}
+ .museum .keys{margin:14px 0 0;padding:12px 0 0;border-top:1px solid rgba(242,182,74,.16);
+ font-size:11px;line-height:1.7;letter-spacing:.02em}
+ .museum .keys b{display:inline-block;min-width:74px;color:var(--gold);font-family:var(--serif);font-style:italic;font-weight:600}
+
+ /* ---- Music Room console ---- */
+ .musicroom{
+ position:fixed;left:26px;top:158px;z-index:9;width:min(84vw,300px);
+ background:var(--panel);border:1px solid var(--panel-edge);border-radius:14px;
+ backdrop-filter:blur(9px);box-shadow:0 12px 40px rgba(0,0,0,.5);overflow:hidden;font-size:12px;
+ }
+ .musicroom > header{
+ display:flex;align-items:center;gap:9px;padding:11px 14px;cursor:pointer;
+ font-family:var(--serif);font-style:italic;color:var(--gold);font-size:14px;user-select:none;
+ }
+ .musicroom .live-dot{width:8px;height:8px;border-radius:50%;background:#5a2128;flex:none}
+ .musicroom.playing .live-dot{background:var(--gold);animation:pulse 2s infinite}
+ @keyframes pulse{0%{box-shadow:0 0 0 0 rgba(242,182,74,.5)}70%{box-shadow:0 0 0 8px rgba(242,182,74,0)}100%{box-shadow:0 0 0 0 rgba(242,182,74,0)}}
+ .musicroom .chev{margin-left:auto;transition:transform .3s;font-size:11px}
+ .musicroom[data-open="false"] .chev{transform:rotate(-90deg)}
+ .musicroom[data-open="false"] .mrbody{display:none}
+ .mrbody{padding:0 14px 14px}
+ .mr-show{width:100%;background:rgba(40,12,16,.7);color:var(--ink);border:1px solid var(--panel-edge);
+ border-radius:9px;padding:8px 10px;font-family:var(--serif);font-size:12.5px;cursor:pointer}
+ .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-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-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}
+ .mr-tracks button{display:flex;gap:9px;width:100%;text-align:left;background:none;border:0;color:var(--ink-dim);
+ font-family:var(--sans);font-size:11.5px;padding:5px 4px;cursor:pointer;border-radius:6px;line-height:1.3}
+ .mr-tracks button:hover{color:var(--ink);background:rgba(70,20,26,.5)}
+ .mr-tracks button[aria-current="true"]{color:var(--gold)}
+ .mr-tracks button .n{color:var(--ink-dim);min-width:20px;font-variant-numeric:tabular-nums;opacity:.7;flex:none}
+ .mr-foot{margin-top:11px;font-size:10px;line-height:1.55;color:var(--ink-dim)}
+ .mr-foot a{color:var(--gold);text-decoration:none;border-bottom:1px solid rgba(242,182,74,.4)}
+ .mr-msg{margin-top:9px;font-size:11px;line-height:1.5;color:var(--ink-dim)}
+
+ :focus-visible{outline:3px solid var(--focus);outline-offset:2px;border-radius:6px}
+ .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
+
+ @media (max-width:820px){
+ .titleplate{max-width:64vw}
+ .dock{gap:10px;padding:10px 12px}
+ .group + .group{padding-left:10px}
+ .field{min-width:74px}
+ .cams{flex-direction:row;left:12px;bottom:100px}
+ .cams .btn{min-width:auto;padding:7px 10px}
+ .label-toggle{bottom:100px;right:12px}
+ .museum{bottom:148px;right:12px}
+ .chapters{top:auto;bottom:100px;right:12px;flex-direction:row}
+ .musicroom{top:auto;bottom:148px;left:12px;right:12px;width:auto}
+ .musicroom .mr-tracks{max-height:96px}
+ }
+ @media (prefers-reduced-motion: reduce){
+ .museum,.chapters button,.btn,.skip{transition:none}
+ }
+</style>
+</head>
+<body>
+<a href="#dock" class="skip">Skip to controls</a>
+<a href="/" class="backlink" id="backlink" aria-label="Back to DEAD, the listening exhibition">◆ DEAD · A Listening Exhibition</a>
+<main id="stage" role="application" aria-label="Scarlet Riverboat Masquerade — an animated visual gallery you can steer with the controls and keyboard">
+ <h1 class="sr">Scarlet Riverboat Masquerade — an animated generative gallery</h1>
+ <canvas id="sky" aria-hidden="true"></canvas>
+ <canvas id="scene" aria-hidden="true"></canvas>
+
+ <div class="titleplate">
+ <p class="kicker">A Riverboat Listening Room</p>
+ <h1 aria-hidden="true">Scarlet Riverboat<br>Masquerade</h1>
+ <p class="chap" id="chapLabel">Chapter I · Departure</p>
+ </div>
+
+ <section class="musicroom" id="musicroom" data-open="true" aria-label="Music room — Grateful Dead Live Music Archive">
+ <header id="mrToggle" role="button" tabindex="0" aria-expanded="true" aria-controls="mrbody">
+ <span class="live-dot" aria-hidden="true"></span>
+ <span>Music Room</span>
+ <span class="chev" aria-hidden="true">▾</span>
+ </header>
+ <div class="mrbody" id="mrbody">
+ <select class="mr-show" id="mrShow" aria-label="Choose a recording from the Live Music Archive"></select>
+ <div class="mr-now" id="mrNow">Choose a night, then press play.</div>
+ <div class="mr-sub" id="mrSub"></div>
+ <div class="mr-transport">
+ <button class="btn" id="mrPrev" aria-label="Previous track">⏮</button>
+ <button class="btn play" id="mrPlay" aria-label="Play" aria-pressed="false">▶</button>
+ <button class="btn" id="mrNext" aria-label="Next track">⏭</button>
+ <button class="mr-tracks-toggle" id="mrTracksToggle" aria-expanded="false" aria-controls="mrTracks">Tracks</button>
+ </div>
+ <div class="mr-seek"><input type="range" id="mrSeek" min="0" max="1000" value="0" step="1" aria-label="Seek within track"></div>
+ <div class="mr-time"><span id="mrCur">0:00</span> / <span id="mrDur">0:00</span></div>
+ <div class="mr-tracks" id="mrTracks" role="list" aria-label="Set list"></div>
+ <div class="mr-foot" id="mrFoot"></div>
+ <div class="mr-msg" id="mrMsg" hidden></div>
+ </div>
+ <audio id="mrAudio" preload="none"></audio>
+ </section>
+
+ <nav class="chapters" aria-label="Chapters">
+ <button data-chapter="0" aria-current="true"><span class="dot" aria-hidden="true"></span>I · Departure</button>
+ <button data-chapter="1"><span class="dot" aria-hidden="true"></span>II · Moon Bend</button>
+ <button data-chapter="2"><span class="dot" aria-hidden="true"></span>III · Dawn Landing</button>
+ </nav>
+
+ <div class="cams" role="group" aria-label="Camera view">
+ <button class="btn" data-cam="wide" aria-pressed="true">◈ Wide River</button>
+ <button class="btn" data-cam="deck" aria-pressed="false">◈ Deck</button>
+ <button class="btn" data-cam="over" aria-pressed="false">◈ Overhead</button>
+ </div>
+
+ <div class="dock" id="dock" role="group" aria-label="Visual controls">
+ <div class="group">
+ <div class="toggle">
+ <button class="btn" id="playBtn" aria-pressed="true">❚❚ Pause</button>
+ <button class="btn" id="restartBtn">↻ Restart</button>
+ </div>
+ <div class="toggle">
+ <button class="btn" id="reduceBtn" aria-pressed="false">◐ Reduced Motion</button>
+ <button class="btn" id="trailBtn" aria-pressed="true">✧ Motion Trails</button>
+ </div>
+ </div>
+ <div class="group">
+ <div class="field">
+ <label for="tempo">Visual Tempo <span class="val" id="tempoV">Moderato</span></label>
+ <input type="range" id="tempo" min="0" max="200" value="100" aria-valuetext="Moderato">
+ </div>
+ <div class="field">
+ <label for="lantern">Lantern Glow <span class="val" id="lanternV">Warm</span></label>
+ <input type="range" id="lantern" min="0" max="100" value="70" aria-valuetext="Warm">
+ </div>
+ <div class="field">
+ <label for="crowd">Crowd Density <span class="val" id="crowdV">Full</span></label>
+ <input type="range" id="crowd" min="0" max="100" value="80" aria-valuetext="Full">
+ </div>
+ <div class="field">
+ <label for="animals">Animal Visibility <span class="val" id="animalsV">All</span></label>
+ <input type="range" id="animals" min="0" max="100" value="100" aria-valuetext="All">
+ </div>
+ </div>
+ </div>
+
+ <button class="btn label-toggle" id="labelBtn" aria-expanded="false" aria-controls="museum">🏷 Museum Label</button>
+ <aside class="museum" id="museum" hidden aria-label="Museum label">
+ <h2>Scarlet Riverboat Masquerade</h2>
+ <div class="meta">Generative Canvas · Live Music Archive · Authored Visual Rhythm</div>
+ <p>A fantastical paddlewheel riverboat drifts through a hand-painted crimson canyon while masked revelers fill three decks. This is a <strong>listening room</strong>: press play in the Music Room and a Grateful Dead concert streams from the Internet Archive's <strong>Live Music Archive</strong> while the boat turns around you.</p>
+ <p><strong>The motion is composed, not measured.</strong> The choreography is an <em>authored visual cadence</em> written into the drawing loop itself — it is deliberately <em>not</em> audio analysis and does not react to the waveform. Set the visual tempo to whatever feels right against the night's playing; the two rhythms — heard and drawn — keep company without one driving the other.</p>
+ <p><strong>The recordings are not copied or re-hosted here.</strong> They are community tapes made available through the Grateful Dead collection at the Internet Archive; playback is user-initiated and every set links back to its source item on archive.org.</p>
+ <p>The <strong>original visual language</strong> reduces each dancer to a weighted silhouette: couples pivot in shared arcs, a linked circle turns about the smokestack, soloists trail scarves, and musicians appear only as abstract shadow-forms — a communal, exploratory spirit rendered without any borrowed logo, emblem, or album art.</p>
+ <p>On the shore, four creatures each carry a distinct loop with believable weight — a <strong>trotting fox</strong>, a <strong>slow-stepping heron</strong>, a <strong>squash-and-stretch rabbit</strong>, and a <strong>bear conducting fireflies</strong>. Palette <em>and</em> choreography change across the three chapters as the boat travels from dusk (Departure) through moonrise (Moon Bend) into sunrise (Dawn Landing).</p>
+ <div class="keys">
+ <div><b>Space</b> pause / play</div>
+ <div><b>R</b> restart the journey</div>
+ <div><b>M</b> reduced motion</div>
+ <div><b>T</b> motion trails</div>
+ <div><b>1 2 3</b> wide · deck · overhead</div>
+ <div><b>← →</b> previous / next chapter</div>
+ <div><b>L</b> this museum label</div>
+ </div>
+ </aside>
+
+ <p class="sr" aria-live="polite" id="live">Wide river view. Chapter one, Departure.</p>
+</main>
+
+<script>
+(() => {
+"use strict";
+const sky = document.getElementById('sky');
+const scene = document.getElementById('scene');
+const sctx = sky.getContext('2d');
+const ctx = scene.getContext('2d');
+const live = document.getElementById('live');
+
+// ---------- State ----------
+const S = {
+ t: 0, // authored master clock (visual beats)
+ playing: true,
+ reduced: false,
+ trails: true,
+ tempo: 1.0, // 0..2
+ lantern: 0.7,
+ crowd: 0.8,
+ animals: 1.0,
+ cam: 'wide', // wide | deck | over
+ chapter: 0, // 0..2 (target)
+ chapterMix: 0, // eased 0..2 for palette blend
+ autoJourney: true, // gentle auto-advance until the visitor picks a chapter
+ journeyClock: 0, // seconds accrued toward the next auto-advance
+ W: 0, H: 0, DPR: 1
+};
+const CHAPTER_SECONDS = 55; // dwell before the boat drifts to the next chapter
+
+// Respect OS reduced-motion at load
+if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches){
+ S.reduced = true;
+}
+
+// ---------- Palettes + per-chapter choreography ----------
+const CHAPTERS = [
+ { // Departure — dusk crimson/gold, couples lead, easy circle
+ name:'Departure',
+ sky:['#2a0a12','#6d1220','#c0432b','#e6893f'],
+ canyon:['#3a0d16','#7a1a25','#521018'],
+ river:'#5a1420', riverHi:'#c95a3a',
+ lantern:'#ffcf6e', dancer:'rgba(20,4,8,0.92)', accent:'#f2b64a',
+ choreo:{ ringDir:1, ringSpeed:0.45, coupleTurn:1.0, scarf:0.9, ringSway:1.0 }
+ },
+ { // Moon Bend — deep violet/blue night, the linked circle dominates and quickens
+ name:'Moon Bend',
+ sky:['#0a0620','#241245','#4a1c52','#7a2b46'],
+ canyon:['#180a2a','#341a44','#20102e'],
+ river:'#1a1030', riverHi:'#8a6bd0',
+ lantern:'#ffe08a', dancer:'rgba(8,4,18,0.92)', accent:'#c9a2ff',
+ choreo:{ ringDir:1, ringSpeed:0.9, coupleTurn:0.7, scarf:1.15, ringSway:1.4 }
+ },
+ { // Dawn Landing — rose/amber sunrise, soloists + scarves take the floor, circle reverses
+ name:'Dawn Landing',
+ sky:['#3a1428','#8a3550','#d97a5a','#f6c88a'],
+ canyon:['#4a1a2a','#8a3a44','#5c2230'],
+ river:'#6a2438', riverHi:'#f6b06a',
+ lantern:'#fff0c0', dancer:'rgba(28,10,14,0.9)', accent:'#ffcf8a',
+ choreo:{ ringDir:-1, ringSpeed:0.4, coupleTurn:1.25, scarf:1.5, ringSway:0.8 }
+ }
+];
+
+function lerp(a,b,t){return a+(b-a)*t;}
+function clamp(v,lo,hi){return Math.max(lo,Math.min(hi,v));}
+function hexToRgb(h){h=h.replace('#','');return [parseInt(h.substr(0,2),16),parseInt(h.substr(2,2),16),parseInt(h.substr(4,2),16)];}
+function mixHex(h1,h2,t){const a=hexToRgb(h1),b=hexToRgb(h2);return `rgb(${Math.round(lerp(a[0],b[0],t))},${Math.round(lerp(a[1],b[1],t))},${Math.round(lerp(a[2],b[2],t))})`;}
+// Darken any 'rgb(r,g,b)' (or hex) color toward black by t (0..1). pal() returns rgb() strings,
+// so we must NOT feed those back into hexToRgb — this handles both forms safely.
+function shade(color,t){
+ const m=String(color).match(/\d+/g);
+ let r,g,b;
+ if(String(color).charAt(0)==='#'){ const c=hexToRgb(color); r=c[0];g=c[1];b=c[2]; }
+ else if(m && m.length>=3){ r=+m[0]; g=+m[1]; b=+m[2]; }
+ else return color;
+ const k=1-clamp(t,0,1);
+ const ch=v=>Math.round(clamp(v*k,0,255));
+ return `rgb(${ch(r)},${ch(g)},${ch(b)})`;
+}
+function pal(key,i){ // blended palette between integer chapters
+ const m = S.chapterMix; const lo=Math.floor(m), hi=Math.min(2,lo+1), f=m-lo;
+ const A=CHAPTERS[lo][key], B=CHAPTERS[hi][key];
+ if(Array.isArray(A)) return mixHex(A[i],B[i],f);
+ return mixHex(A,B,f);
+}
+// Choreography snaps to the nearest chapter (palette blends, steps change crisply)
+function CH(){ return CHAPTERS[clamp(Math.round(S.chapterMix),0,2)].choreo; }
+
+// ---------- Resize ----------
+function resize(){
+ S.DPR = Math.min(2, window.devicePixelRatio||1);
+ S.W = window.innerWidth; S.H = window.innerHeight;
+ [sky,scene].forEach(c=>{
+ c.width = Math.max(1,Math.round(S.W*S.DPR)); c.height = Math.max(1,Math.round(S.H*S.DPR));
+ c.getContext('2d').setTransform(S.DPR,0,0,S.DPR,0,0);
+ });
+ buildSky();
+}
+window.addEventListener('resize', resize);
+
+// ---------- Sky (redrawn on chapter change / resize) ----------
+let skyDirty = true;
+function buildSky(){ skyDirty = true; }
+function drawSky(){
+ const W=S.W,H=S.H;
+ const g = sctx.createLinearGradient(0,0,0,H*0.72);
+ g.addColorStop(0, pal('sky',0));
+ g.addColorStop(0.45, pal('sky',1));
+ g.addColorStop(0.8, pal('sky',2));
+ g.addColorStop(1, pal('sky',3));
+ sctx.clearRect(0,0,W,H);
+ sctx.fillStyle=g; sctx.fillRect(0,0,W,H*0.74);
+
+ // Moon / sun — position drifts with the journey
+ const cx = lerp(W*0.78, W*0.28, S.chapterMix/2);
+ const cy = lerp(H*0.18, H*0.30, Math.sin(S.chapterMix/2*Math.PI));
+ const r = lerp(46, 62, S.chapterMix/2);
+ const glow = sctx.createRadialGradient(cx,cy,4,cx,cy,r*3.2);
+ glow.addColorStop(0, pal('lantern',0)); glow.addColorStop(0.15, pal('accent',0));
+ glow.addColorStop(1,'rgba(0,0,0,0)');
+ sctx.fillStyle=glow; sctx.beginPath(); sctx.arc(cx,cy,r*3.2,0,7); sctx.fill();
+ sctx.fillStyle=pal('lantern',0); sctx.beginPath(); sctx.arc(cx,cy,r,0,7); sctx.fill();
+
+ // Stars, densest at Moon Bend
+ const starA = Math.max(0, 1-Math.abs(S.chapterMix-1)) * 0.9;
+ if(starA>0.02){
+ sctx.fillStyle=`rgba(255,240,210,${starA})`;
+ for(let i=0;i<120;i++){
+ const sx=(i*137.5)%W, sy=((i*89.3)%(H*0.5));
+ const tw = 0.4+0.6*Math.abs(Math.sin(i*1.7));
+ sctx.globalAlpha=starA*tw; sctx.fillRect(sx,sy,1.5,1.5);
+ }
+ sctx.globalAlpha=1;
+ }
+ skyDirty=false;
+}
+
+// ---------- Canyon layers (parallax, far -> near) ----------
+function canyonSilhouette(baseY, amp, seed, color, offset){
+ const W=S.W,H=S.H;
+ ctx.fillStyle=color;
+ ctx.beginPath();
+ ctx.moveTo(0,H);
+ ctx.lineTo(0,baseY);
+ const step = W/14;
+ for(let x=0;x<=W+step;x+=step){
+ const n = Math.sin((x*0.006)+seed) + 0.5*Math.sin((x*0.017)+seed*2.3);
+ const y = baseY - amp*(0.6+0.4*n) - offset;
+ ctx.lineTo(x, y);
+ }
+ ctx.lineTo(W,H);
+ ctx.closePath();
+ ctx.fill();
+}
+
+// ---------- Boat transform for the active camera ----------
+function boatTransform(){
+ const W=S.W,H=S.H;
+ const bob = S.reduced?0:Math.sin(S.t*0.6)*6;
+ const sway = S.reduced?0:Math.sin(S.t*0.33)*0.012;
+ let cx=W*0.5, cy=H*0.60+bob, scale=Math.min(W,H)/620, rot=sway;
+ if(S.cam==='deck'){ cx=W*0.5; cy=H*0.66+bob; scale=Math.min(W,H)/300; rot=sway*0.6; }
+ if(S.cam==='over'){ cx=W*0.5; cy=H*0.52; scale=Math.min(W,H)/540; rot=0; }
+ return {cx,cy,scale,rot,bob};
+}
+
+// ---------- Dancers ----------
+function drawFigure(x,y,s,phase,type,accent){
+ const t=S.t, ch=CH();
+ ctx.save();
+ ctx.translate(x,y);
+ ctx.scale(s,s);
+
+ let turn=0, lean=0, bob=0, scarf=0, armSwing=0;
+ const ph = phase;
+ if(type==='couple'){
+ turn = Math.sin(t*1.1+ph)*0.9*ch.coupleTurn; // shared pivot, chapter-weighted
+ lean = Math.sin(t*1.1+ph)*0.12*ch.coupleTurn;
+ bob = Math.abs(Math.sin(t*1.1+ph))* -3; // rise on the turn
+ armSwing = 0.7;
+ } else if(type==='circle'){
+ turn = Math.sin(t*0.8+ph)*0.35;
+ bob = Math.abs(Math.sin(t*1.6+ph))*-2.4;
+ lean = Math.sin(t*0.8+ph)*0.05;
+ armSwing=0.4;
+ } else if(type==='solo'){
+ turn = (t*1.8+ph)%(Math.PI*2); // continuous spin
+ scarf = 1; bob = Math.abs(Math.sin(t*2.2+ph))*-2;
+ armSwing=1.0;
+ } else if(type==='musician'){
+ bob = Math.sin(t*1.4+ph)*-1.6;
+ armSwing = 0.5+0.3*Math.sin(t*2+ph);
+ }
+ ctx.rotate(lean);
+ ctx.translate(0,bob);
+
+ // scarf trail for soloists — length/opacity swell across chapters
+ if(type==='solo' && scarf){
+ ctx.save();
+ ctx.rotate(turn);
+ ctx.strokeStyle=accent; ctx.globalAlpha=0.5*ch.scarf; ctx.lineWidth=2.2;
+ ctx.beginPath();
+ const seg = Math.round(10*ch.scarf);
+ for(let i=0;i<seg;i++){
+ const a=i*0.5, rr=8+i*2.4;
+ const px=Math.cos(a+t*3)* rr, py=-14+Math.sin(a+t*3)*rr*0.4;
+ i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);
+ }
+ ctx.stroke(); ctx.globalAlpha=1; ctx.restore();
+ }
+
+ ctx.rotate(turn*0.28);
+ ctx.fillStyle=pal('dancer',0);
+
+ // head (with a small mask mark)
+ ctx.beginPath(); ctx.arc(0,-22,4.6,0,7); ctx.fill();
+ ctx.fillStyle=accent; ctx.globalAlpha=0.85;
+ ctx.fillRect(-3.4,-24,6.8,2.4); // mask band
+ ctx.globalAlpha=1; ctx.fillStyle=pal('dancer',0);
+
+ // torso (tapered)
+ ctx.beginPath();
+ ctx.moveTo(-4,-18); ctx.quadraticCurveTo(0,-6,-3,4);
+ ctx.lineTo(3,4); ctx.quadraticCurveTo(0,-6,4,-18);
+ ctx.closePath(); ctx.fill();
+
+ // arms — swing conveys weight/effort
+ const sw = Math.sin(t*(type==='musician'?2:1.4)+ph)*armSwing;
+ ctx.lineWidth=2.6; ctx.strokeStyle=pal('dancer',0); ctx.lineCap='round';
+ ctx.beginPath();
+ ctx.moveTo(-3,-15); ctx.lineTo(-9-sw*4,-10+sw*3);
+ ctx.moveTo(3,-15); ctx.lineTo(9-sw*4,-10-sw*3);
+ ctx.stroke();
+
+ // legs — bent-knee push (the source of the bob)
+ const legPush = Math.abs(Math.sin(t*(type==='solo'?2.2:1.1)+ph));
+ ctx.beginPath();
+ ctx.moveTo(-2,3); ctx.lineTo(-3-legPush*1.5, 16);
+ ctx.moveTo(2,3); ctx.lineTo(3+ (1-legPush)*1.5, 16);
+ ctx.stroke();
+
+ // musician's instrument = abstract shadow bar
+ if(type==='musician'){
+ ctx.strokeStyle=accent; ctx.globalAlpha=0.5; ctx.lineWidth=2;
+ ctx.beginPath(); ctx.moveTo(4,-14); ctx.lineTo(14,-18); ctx.stroke();
+ ctx.globalAlpha=1;
+ }
+ ctx.restore();
+}
+
+// Little lantern with glow
+function lantern(x,y,s,glow,color){
+ if(glow>0.02){
+ const g=ctx.createRadialGradient(x,y,1,x,y,26*s*(0.5+glow));
+ g.addColorStop(0,color); g.addColorStop(0.4,`rgba(255,190,90,${0.35*glow})`);
+ g.addColorStop(1,'rgba(0,0,0,0)');
+ ctx.fillStyle=g; ctx.beginPath(); ctx.arc(x,y,26*s*(0.5+glow),0,7); ctx.fill();
+ }
+ ctx.fillStyle=color; ctx.beginPath(); ctx.arc(x,y,2.6*s,0,7); ctx.fill();
+}
+
+// ---------- The Riverboat ----------
+function drawBoat(){
+ const {cx,cy,scale,rot}=boatTransform();
+ const accent=pal('accent',0), lan=pal('lantern',0);
+ const glow=S.lantern;
+ ctx.save();
+ ctx.translate(cx,cy); ctx.rotate(rot); ctx.scale(scale,scale);
+
+ if(S.cam==='over'){ drawBoatOverhead(accent,lan,glow); ctx.restore(); return; }
+
+ // ----- hull -----
+ ctx.fillStyle='rgba(30,8,12,0.96)';
+ ctx.beginPath();
+ ctx.moveTo(-230,40);
+ ctx.quadraticCurveTo(-260,10,-210,4);
+ ctx.lineTo(210,4);
+ ctx.quadraticCurveTo(262,10,232,40);
+ ctx.quadraticCurveTo(0,78,-230,40);
+ ctx.closePath(); ctx.fill();
+ // waterline highlight
+ ctx.strokeStyle=pal('riverHi',0); ctx.globalAlpha=0.5; ctx.lineWidth=2;
+ ctx.beginPath(); ctx.moveTo(-215,42); ctx.quadraticCurveTo(0,72,215,42); ctx.stroke();
+ ctx.globalAlpha=1;
+
+ // ----- three decks -----
+ const deckDefs=[
+ {y:4, w:200, h:-34, n:9}, // lower
+ {y:-30,w:160, h:-30, n:7}, // middle
+ {y:-60,w:120, h:-26, n:5} // upper
+ ];
+ deckDefs.forEach((d,di)=>{
+ ctx.fillStyle='rgba(46,14,18,0.95)';
+ ctx.fillRect(-d.w, d.y+d.h, d.w*2, 6);
+ ctx.strokeStyle='rgba(70,24,28,0.9)'; ctx.lineWidth=2;
+ for(let px=-d.w; px<=d.w; px+=d.w/6){
+ ctx.beginPath(); ctx.moveTo(px,d.y+d.h); ctx.lineTo(px,d.y+d.h+6); ctx.stroke();
+ }
+ const nl = 6;
+ for(let i=0;i<=nl;i++){
+ const lx=-d.w + (2*d.w)*(i/nl);
+ const flick = S.reduced?1:(0.85+0.15*Math.sin(S.t*3+i+di));
+ lantern(lx, d.y+d.h-4, 1, glow*flick, lan);
+ }
+ });
+
+ // ----- smokestacks -----
+ ctx.fillStyle='rgba(24,6,10,0.98)';
+ [-40,40].forEach(sx=>{
+ ctx.fillRect(sx-7,-140,14,86);
+ ctx.fillStyle=accent; ctx.globalAlpha=0.6; ctx.fillRect(sx-9,-142,18,6); ctx.globalAlpha=1;
+ ctx.fillStyle='rgba(24,6,10,0.98)';
+ });
+ // smoke drift
+ if(!S.reduced){
+ ctx.fillStyle=`rgba(200,140,120,0.10)`;
+ for(let i=0;i<6;i++){
+ const t=S.t*0.5+i; const sx=-40+Math.sin(t)*8;
+ ctx.beginPath(); ctx.arc(sx-Math.sin(t*0.4)*30, -150-i*16-(S.t*4%20), 8+i*2,0,7); ctx.fill();
+ }
+ }
+
+ // ----- pilot house -----
+ ctx.fillStyle='rgba(40,12,16,0.98)';
+ ctx.fillRect(-30,-104,60,30);
+ ctx.fillStyle=accent; ctx.globalAlpha=0.35; ctx.fillRect(-24,-98,48,10); ctx.globalAlpha=1;
+
+ // ----- paddlewheel (right side) -----
+ drawPaddlewheel(232,26,accent);
+
+ // ----- populate decks with dancers -----
+ populateDecks(deckDefs,accent);
+
+ ctx.restore();
+}
+
+function drawPaddlewheel(x,y,accent){
+ const spin = S.reduced? 0 : S.t*2.1;
+ ctx.save(); ctx.translate(x,y);
+ ctx.strokeStyle='rgba(60,20,24,0.9)'; ctx.lineWidth=3;
+ ctx.beginPath(); ctx.arc(0,0,34,0,7); ctx.stroke();
+ ctx.save(); ctx.rotate(spin);
+ ctx.strokeStyle=accent; ctx.lineWidth=2.4; ctx.globalAlpha=0.85;
+ for(let i=0;i<8;i++){
+ ctx.rotate(Math.PI/4);
+ ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(0,-32); ctx.stroke();
+ ctx.fillStyle='rgba(50,16,20,0.9)';
+ ctx.fillRect(-9,-34,18,7);
+ }
+ ctx.restore();
+ ctx.globalAlpha=1;
+ ctx.fillStyle=accent; ctx.beginPath(); ctx.arc(0,0,4,0,7); ctx.fill();
+ if(!S.reduced){
+ ctx.fillStyle='rgba(240,200,170,0.18)';
+ for(let i=0;i<5;i++){ const a=(S.t*3+i)%1; ctx.beginPath(); ctx.arc(6+a*24,34+Math.sin(i)*3,3-a*2,0,7); ctx.fill();}
+ }
+ ctx.restore();
+}
+
+function populateDecks(deckDefs, accent){
+ const density = S.crowd, ch = CH();
+ // Deck 0 (lower): couples turning beneath lanterns
+ const d0=deckDefs[0];
+ const nCouples = Math.round(lerp(0,5,density));
+ for(let i=0;i<nCouples;i++){
+ const gx = -d0.w+30 + (2*d0.w-60)*(nCouples<=1?0.5:i/(nCouples-1));
+ const gap = 9+Math.sin(S.t*1.1+i)*3;
+ drawFigure(gx-gap, d0.y+d0.h-2, 0.62, i*1.3, 'couple', accent);
+ drawFigure(gx+gap, d0.y+d0.h-2, 0.62, i*1.3+Math.PI, 'couple', accent);
+ }
+ // Deck 1 (middle): linked circle around the smokestack + a soloist
+ const d1=deckDefs[1];
+ const ringN = Math.round(lerp(3,8,density));
+ const ringR = 46, ringSpin = S.reduced?0:S.t*0.5*ch.ringSpeed*ch.ringDir;
+ // linking ribbon between the circle dancers
+ ctx.strokeStyle=accent; ctx.globalAlpha=0.28; ctx.lineWidth=1.6;
+ ctx.beginPath();
+ for(let i=0;i<=ringN;i++){
+ const a=(i/ringN)*Math.PI*2+ringSpin;
+ const rx=Math.cos(a)*ringR, ry=d1.y+d1.h-2+Math.sin(a)*6*ch.ringSway;
+ i===0?ctx.moveTo(rx,ry):ctx.lineTo(rx,ry);
+ }
+ ctx.stroke(); ctx.globalAlpha=1;
+ for(let i=0;i<ringN;i++){
+ const a = (i/ringN)*Math.PI*2 + ringSpin;
+ const rx = Math.cos(a)*ringR, rz=Math.sin(a)*0.42;
+ const fx = rx, fy = d1.y+d1.h-2 + Math.sin(a)*6*ch.ringSway;
+ drawFigure(fx, fy, 0.5*(0.8+rz*0.2+0.2), i, 'circle', accent);
+ }
+ if(density>0.3) drawFigure(d1.w-30, d1.y+d1.h-2, 0.6, 2.0, 'solo', accent);
+
+ // Deck 2 (upper): musicians as abstract silhouettes + a soloist
+ const d2=deckDefs[2];
+ const nMus = Math.round(lerp(1,4,density));
+ for(let i=0;i<nMus;i++){
+ const gx=-d2.w+26 + (2*d2.w-52)*(i/Math.max(1,nMus));
+ drawFigure(gx, d2.y+d2.h-2, 0.55, i*2.1, 'musician', accent);
+ }
+ if(density>0.5) drawFigure(0, d2.y+d2.h-2, 0.58, 1.1, 'solo', accent);
+}
+
+// Overhead camera: top-down of the circle around the smokestack
+function drawBoatOverhead(accent,lan,glow){
+ const ch=CH();
+ ctx.fillStyle='rgba(38,12,16,0.92)';
+ ctx.beginPath(); ctx.ellipse(0,0,190,110,0,0,7); ctx.fill();
+ ctx.strokeStyle='rgba(70,24,28,0.8)'; ctx.lineWidth=3;
+ ctx.beginPath(); ctx.ellipse(0,0,190,110,0,0,7); ctx.stroke();
+ // bow marker
+ ctx.fillStyle=accent; ctx.globalAlpha=0.5;
+ ctx.beginPath(); ctx.moveTo(0,-118); ctx.lineTo(-10,-102); ctx.lineTo(10,-102); ctx.closePath(); ctx.fill();
+ ctx.globalAlpha=1;
+ // central smokestack (top)
+ ctx.fillStyle='rgba(20,6,10,0.98)';
+ ctx.beginPath(); ctx.arc(0,0,16,0,7); ctx.fill();
+ ctx.strokeStyle=accent; ctx.globalAlpha=0.6; ctx.beginPath(); ctx.arc(0,0,16,0,7); ctx.stroke(); ctx.globalAlpha=1;
+ // linked circle rotating around it — the marquee choreography, seen from above
+ const ringN=Math.round(lerp(4,12,S.crowd)); const R=78; const spin=S.reduced?0:S.t*0.55*ch.ringSpeed*ch.ringDir;
+ ctx.strokeStyle=accent; ctx.globalAlpha=0.35; ctx.lineWidth=2;
+ ctx.beginPath();
+ for(let i=0;i<=ringN;i++){const a=(i/ringN)*Math.PI*2+spin; const x=Math.cos(a)*R,y=Math.sin(a)*R*0.62; i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);}
+ ctx.closePath(); ctx.stroke(); ctx.globalAlpha=1;
+ for(let i=0;i<ringN;i++){
+ const a=(i/ringN)*Math.PI*2+spin;
+ const x=Math.cos(a)*R, y=Math.sin(a)*R*0.62;
+ ctx.save(); ctx.translate(x,y);
+ ctx.fillStyle=pal('dancer',0); ctx.beginPath(); ctx.arc(0,0,5,0,7); ctx.fill();
+ ctx.fillStyle=accent; ctx.globalAlpha=0.8; ctx.fillRect(-4,-1.5,8,3); ctx.globalAlpha=1;
+ ctx.restore();
+ lantern(Math.cos(a)*(R+22),Math.sin(a)*(R+22)*0.62,0.7,glow*0.8,lan);
+ }
+ // couples as small paired dots on the outer deck
+ const nc=Math.round(lerp(0,6,S.crowd));
+ for(let i=0;i<nc;i++){
+ const a=(i/Math.max(1,nc))*Math.PI*2 - spin*0.6; const r=150;
+ const x=Math.cos(a)*r, y=Math.sin(a)*r*0.6;
+ const g=6+Math.sin(S.t*1.1+i)*3;
+ ctx.fillStyle=pal('dancer',0);
+ ctx.beginPath(); ctx.arc(x-g,y,3.4,0,7); ctx.fill();
+ ctx.beginPath(); ctx.arc(x+g,y,3.4,0,7); ctx.fill();
+ }
+}
+
+// ---------- Shoreline animals ----------
+function drawAnimals(){
+ if(S.animals<=0.02) return;
+ const W=S.W,H=S.H;
+ const baseY=H*0.78;
+ const accent=pal('accent',0);
+ ctx.save();
+ ctx.globalAlpha=Math.min(1,S.animals);
+ // shore band
+ ctx.fillStyle=pal('canyon',0);
+ ctx.beginPath(); ctx.moveTo(0,baseY);
+ for(let x=0;x<=W;x+=40){ctx.lineTo(x,baseY+Math.sin(x*0.02)*6);}
+ ctx.lineTo(W,H); ctx.lineTo(0,H); ctx.fill();
+
+ const t=S.t;
+ const scaleA = S.cam==='deck'?1.15:1;
+
+ 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);
+
+ ctx.restore();
+}
+
+function fox(x,y,s,t,accent){
+ const gait=Math.sin(t*3.2);
+ const bob=Math.abs(Math.sin(t*3.2))*-4;
+ ctx.save(); ctx.translate(x + Math.sin(t*0.4)*30, y+bob); ctx.scale(s,s);
+ ctx.fillStyle='rgba(30,8,12,0.95)';
+ ctx.beginPath(); ctx.ellipse(0,-10,15,7,0,0,7); ctx.fill();
+ ctx.beginPath(); ctx.moveTo(12,-14); ctx.lineTo(24,-16); ctx.lineTo(13,-8); ctx.closePath(); ctx.fill();
+ ctx.beginPath(); ctx.moveTo(14,-18); ctx.lineTo(17,-24); ctx.lineTo(19,-17);ctx.closePath(); ctx.fill();
+ ctx.strokeStyle=accent; ctx.lineWidth=5; ctx.lineCap='round'; ctx.globalAlpha=0.8;
+ ctx.beginPath(); ctx.moveTo(-14,-10); ctx.quadraticCurveTo(-26,-14-gait*5,-30,-4-gait*3); ctx.stroke();
+ ctx.globalAlpha=1;
+ ctx.strokeStyle='rgba(30,8,12,0.95)'; ctx.lineWidth=2.4;
+ const lg=gait*4;
+ ctx.beginPath();
+ ctx.moveTo(-8,-4); ctx.lineTo(-8-lg,3);
+ ctx.moveTo(8,-4); ctx.lineTo(8+lg,3);
+ ctx.moveTo(-4,-4); ctx.lineTo(-4+lg,3);
+ ctx.moveTo(4,-4); ctx.lineTo(4-lg,3);
+ ctx.stroke();
+ ctx.restore();
+}
+
+function heron(x,y,s,t,accent){
+ const step=Math.sin(t*0.9);
+ const wing=Math.sin(t*1.2)*0.5;
+ ctx.save(); ctx.translate(x + Math.sin(t*0.25)*18, y); ctx.scale(s,s);
+ ctx.strokeStyle='rgba(24,8,14,0.95)'; ctx.fillStyle='rgba(24,8,14,0.95)';
+ ctx.lineWidth=2;
+ ctx.beginPath();
+ ctx.moveTo(-2,-18); ctx.lineTo(-4,2+ (step>0?0:step*8));
+ ctx.moveTo(3,-18); ctx.lineTo(5,2+ (step>0?step*8:0));
+ ctx.stroke();
+ ctx.beginPath(); ctx.ellipse(0,-24,10,7,-0.2,0,7); ctx.fill();
+ ctx.save(); ctx.translate(0,-26); ctx.rotate(wing*0.6);
+ ctx.beginPath(); ctx.moveTo(0,0); ctx.quadraticCurveTo(-14,-4,-20,4); ctx.quadraticCurveTo(-10,2,0,3); ctx.closePath(); ctx.fill();
+ ctx.restore();
+ ctx.lineWidth=2.4;
+ ctx.beginPath(); ctx.moveTo(6,-28);
+ ctx.quadraticCurveTo(16,-34,12,-46); ctx.stroke();
+ ctx.beginPath(); ctx.arc(12,-48,3,0,7); ctx.fill();
+ ctx.strokeStyle=accent; ctx.beginPath(); ctx.moveTo(14,-48); ctx.lineTo(24,-49); ctx.stroke();
+ ctx.restore();
+}
+
+function rabbit(x,y,s,t,accent){
+ const cyc=(t*1.6)%(Math.PI*2);
+ const air=Math.max(0,Math.sin(cyc));
+ const stretch=1+air*0.4, squash=1-Math.max(0,-Math.sin(cyc))*0.35;
+ const hopY=-air*26;
+ ctx.save();
+ 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);
+ ctx.fillStyle='rgba(34,10,14,0.95)';
+ ctx.beginPath(); ctx.ellipse(0,-8,9,7,0,0,7); ctx.fill();
+ ctx.beginPath(); ctx.arc(8,-12,4.5,0,7); ctx.fill();
+ const ear = air*0.8;
+ ctx.save(); ctx.translate(8,-15); ctx.rotate(ear);
+ ctx.beginPath(); ctx.ellipse(0,-6,2,6,0.2,0,7); ctx.fill();
+ ctx.beginPath(); ctx.ellipse(4,-6,2,6,0.4,0,7); ctx.fill();
+ ctx.restore();
+ ctx.fillStyle=accent; ctx.globalAlpha=0.85;
+ ctx.beginPath(); ctx.arc(-9,-8,3,0,7); ctx.fill(); ctx.globalAlpha=1;
+ ctx.fillStyle='rgba(34,10,14,0.95)';
+ ctx.beginPath(); ctx.ellipse(-3,-2+air*2,4,2,air*0.6,0,7); ctx.fill();
+ ctx.restore();
+}
+
+function bearAndFireflies(x,y,s,t,accent){
+ const wob=Math.sin(t*0.35)*3;
+ ctx.save(); ctx.translate(x+wob,y); ctx.scale(s,s);
+ ctx.fillStyle='rgba(20,6,10,0.97)';
+ ctx.beginPath(); ctx.ellipse(0,-16,18,20,0,0,7); ctx.fill();
+ ctx.beginPath(); ctx.ellipse(-14,-2,10,8,0,0,7); ctx.fill();
+ ctx.beginPath(); ctx.ellipse(14,-2,10,8,0,0,7); ctx.fill();
+ ctx.beginPath(); ctx.arc(0,-36,11,0,7); ctx.fill();
+ ctx.beginPath(); ctx.arc(-9,-45,4,0,7); ctx.fill();
+ ctx.beginPath(); ctx.arc(9,-45,4,0,7); ctx.fill();
+ ctx.strokeStyle='rgba(20,6,10,0.97)'; ctx.lineWidth=6; ctx.lineCap='round';
+ const ax=Math.sin(t*0.7)*16, ay=-Math.abs(Math.cos(t*0.7))*10;
+ ctx.beginPath(); ctx.moveTo(6,-24); ctx.quadraticCurveTo(16,-34,18+ax,-40+ay); ctx.stroke();
+ ctx.strokeStyle=accent; ctx.lineWidth=2; ctx.globalAlpha=0.8;
+ ctx.beginPath(); ctx.moveTo(18+ax,-40+ay); ctx.lineTo(18+ax+6,-44+ay); ctx.stroke();
+ ctx.globalAlpha=1;
+ ctx.restore();
+
+ // fireflies respond to the conducting motion (authored, not audio)
+ const n=14;
+ for(let i=0;i<n;i++){
+ const ph=i*0.7;
+ const orbit=30+ (i%5)*10;
+ const a=t*0.7*(0.6+ (i%3)*0.2) + ph;
+ const fx=x + Math.cos(a)*orbit + Math.sin(t*0.7)*14;
+ const fy=y-40 + Math.sin(a*1.3)*orbit*0.5 - (i%4)*6;
+ const blink=0.35+0.65*Math.abs(Math.sin(t*3+ph));
+ const g=ctx.createRadialGradient(fx,fy,0,fx,fy,7);
+ g.addColorStop(0,`rgba(255,230,140,${blink})`); g.addColorStop(1,'rgba(255,200,90,0)');
+ ctx.fillStyle=g; ctx.beginPath(); ctx.arc(fx,fy,7,0,7); ctx.fill();
+ }
+}
+
+// ---------- River foreground with parallax reflections ----------
+function drawRiver(){
+ const W=S.W,H=S.H;
+ const top=H*0.72;
+ const g=ctx.createLinearGradient(0,top,0,H);
+ g.addColorStop(0, pal('river',0));
+ g.addColorStop(1, shade(pal('river',0),0.5));
+ ctx.fillStyle=g; ctx.fillRect(0,top,W,H-top);
+ const hi=pal('riverHi',0);
+ ctx.strokeStyle=hi;
+ for(let layer=0;layer<3;layer++){
+ const alpha=0.16-layer*0.04, speed=(layer+1)*0.5;
+ ctx.globalAlpha=alpha; ctx.lineWidth=1.5+layer;
+ for(let i=0;i<12;i++){
+ const y=top+ (H-top)*((i+ (S.t*speed*0.02*(layer+1))%1)/12);
+ const amp=(y-top)*0.06;
+ ctx.beginPath();
+ for(let x=0;x<=W;x+=24){
+ const yy=y+Math.sin((x*0.02)+S.t*speed+layer)*amp*(S.reduced?0.3:1);
+ x===0?ctx.moveTo(x,yy):ctx.lineTo(x,yy);
+ }
+ ctx.stroke();
+ }
+ }
+ ctx.globalAlpha=1;
+ if(S.lantern>0.05 && S.cam!=='over'){
+ const {cx}=boatTransform();
+ const rg=ctx.createRadialGradient(cx,top+30,4,cx,top+30,120);
+ rg.addColorStop(0,`rgba(255,190,90,${0.22*S.lantern})`); rg.addColorStop(1,'rgba(0,0,0,0)');
+ ctx.fillStyle=rg; ctx.beginPath(); ctx.ellipse(cx,top+40,120,40,0,0,7); ctx.fill();
+ }
+}
+
+// ---------- Main loop ----------
+let last=performance.now();
+function frame(now){
+ const dt=Math.min(0.05,(now-last)/1000); last=now;
+
+ if(S.playing){
+ const spd = S.reduced ? 0.28 : 1.0;
+ 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){
+ S.journeyClock += dt;
+ if(S.journeyClock >= CHAPTER_SECONDS){
+ S.journeyClock = 0;
+ setChapter((S.chapter+1)%3, {auto:true});
+ }
+ }
+ }
+
+ // ease chapter mix toward target
+ const prevMix=S.chapterMix;
+ S.chapterMix += (S.chapter - S.chapterMix)*Math.min(1,dt*2.2);
+ if(Math.abs(S.chapter-S.chapterMix)>0.001 || Math.abs(prevMix-S.chapterMix)>0.001) skyDirty=true;
+
+ if(skyDirty) drawSky();
+
+ const W=S.W,H=S.H;
+ if(S.trails && !S.reduced && S.playing){
+ ctx.fillStyle='rgba(6,2,4,0.16)';
+ ctx.fillRect(0,0,W,H);
+ } else {
+ ctx.clearRect(0,0,W,H);
+ }
+
+ // parallax canyon (farthest -> nearest)
+ canyonSilhouette(H*0.54, 150, 6.1, shade(pal('canyon',0),0.35), Math.sin(S.t*0.03)*3);
+ canyonSilhouette(H*0.62, 120, 1.2, pal('canyon',0), Math.sin(S.t*0.05)*4);
+ canyonSilhouette(H*0.70, 90, 4.5, pal('canyon',2), Math.sin(S.t*0.05+2)*3);
+
+ drawRiver();
+ drawAnimals();
+ drawBoat();
+
+ requestAnimationFrame(frame);
+}
+
+// ---------- UI wiring ----------
+const tempoWords=['Adagio','Andante','Moderato','Allegro','Vivace'];
+const lanternWords=['Doused','Dim','Warm','Bright','Blazing'];
+const crowdWords=['Empty','Sparse','Lively','Full','Teeming'];
+const animalWords=['Hidden','Few','Some','Most','All'];
+function word(arr,v){return arr[Math.min(arr.length-1,Math.floor(v*arr.length))];}
+
+const tempoEl=document.getElementById('tempo');
+const tempoV=document.getElementById('tempoV');
+tempoEl.addEventListener('input',e=>{
+ S.tempo=e.target.value/100; // 0..2
+ const w=word(tempoWords, Math.min(0.999,S.tempo/2));
+ tempoV.textContent=w;
+ e.target.setAttribute('aria-valuetext',w);
+});
+
+function wireSlider(id,valId,arr,setter){
+ const el=document.getElementById(id), v=document.getElementById(valId);
+ el.addEventListener('input',e=>{
+ const val=e.target.value/100; setter(val);
+ const w=word(arr,val); v.textContent=w; e.target.setAttribute('aria-valuetext',w);
+ });
+}
+wireSlider('lantern','lanternV',lanternWords,val=>S.lantern=val);
+wireSlider('crowd','crowdV',crowdWords,val=>S.crowd=val);
+wireSlider('animals','animalsV',animalWords,val=>S.animals=val);
+
+// Sync every label to its true computed word on load (no hardcoded mismatch)
+['tempo','lantern','crowd','animals'].forEach(id=>{
+ document.getElementById(id).dispatchEvent(new Event('input'));
+});
+
+const playBtn=document.getElementById('playBtn');
+playBtn.addEventListener('click',()=>{
+ S.playing=!S.playing;
+ playBtn.setAttribute('aria-pressed',S.playing);
+ playBtn.textContent=S.playing?'❚❚ Pause':'► Play';
+ announce(S.playing?'Playing.':'Paused.');
+});
+document.getElementById('restartBtn').addEventListener('click',()=>{
+ S.t=0; S.playing=true; S.autoJourney=true; S.journeyClock=0;
+ playBtn.setAttribute('aria-pressed','true'); playBtn.textContent='❚❚ Pause';
+ setChapter(0,{silent:true});
+ ctx.clearRect(0,0,S.W,S.H);
+ announce('Restarted the journey from Departure.');
+});
+const reduceBtn=document.getElementById('reduceBtn');
+reduceBtn.addEventListener('click',()=>{
+ S.reduced=!S.reduced; reduceBtn.setAttribute('aria-pressed',S.reduced);
+ if(S.reduced) ctx.clearRect(0,0,S.W,S.H);
+ announce(S.reduced?'Reduced motion on. Movement gentled.':'Reduced motion off.');
+});
+if(S.reduced) reduceBtn.setAttribute('aria-pressed','true');
+const trailBtn=document.getElementById('trailBtn');
+trailBtn.addEventListener('click',()=>{
+ S.trails=!S.trails; trailBtn.setAttribute('aria-pressed',S.trails);
+ if(!S.trails) ctx.clearRect(0,0,S.W,S.H);
+ announce(S.trails?'Motion trails on.':'Motion trails off.');
+});
+
+// cameras
+document.querySelectorAll('[data-cam]').forEach(b=>{
+ b.addEventListener('click',()=>{
+ S.cam=b.dataset.cam;
+ document.querySelectorAll('[data-cam]').forEach(x=>x.setAttribute('aria-pressed', x===b));
+ ctx.clearRect(0,0,S.W,S.H);
+ const names={wide:'Wide river',deck:'Deck',over:'Overhead'};
+ announce(names[S.cam]+' view.');
+ });
+});
+
+// chapters
+const chapLabel=document.getElementById('chapLabel');
+const romans=['I','II','III'];
+function setChapter(i, opts){
+ opts=opts||{};
+ S.chapter=i; skyDirty=true;
+ if(!opts.auto){ S.autoJourney=false; } // a manual pick takes the wheel from the auto-journey
+ S.journeyClock=0;
+ document.querySelectorAll('[data-chapter]').forEach(b=>b.setAttribute('aria-current', String(+b.dataset.chapter===i)));
+ chapLabel.textContent=`Chapter ${romans[i]} · ${CHAPTERS[i].name}`;
+ if(!opts.silent) announce(`Chapter ${romans[i]}, ${CHAPTERS[i].name}. Palette and choreography shifting.`);
+}
+document.querySelectorAll('[data-chapter]').forEach(b=>{
+ b.addEventListener('click',()=>setChapter(+b.dataset.chapter));
+});
+
+// museum label
+const labelBtn=document.getElementById('labelBtn');
+const museum=document.getElementById('museum');
+labelBtn.addEventListener('click',()=>{
+ const open=museum.hidden;
+ museum.hidden=!open;
+ labelBtn.setAttribute('aria-expanded',String(open));
+});
+
+function announce(msg){ live.textContent=msg; }
+
+// keyboard shortcuts
+window.addEventListener('keydown',e=>{
+ // Let native controls handle their own Space/Enter activation
+ if(e.target.matches('input,button')&&(e.key===' '||e.key==='Enter')) return;
+ switch(e.key.toLowerCase()){
+ case ' ': e.preventDefault(); playBtn.click(); break;
+ case 'r': document.getElementById('restartBtn').click(); break;
+ case 'm': reduceBtn.click(); break;
+ case 't': trailBtn.click(); break;
+ case '1': document.querySelector('[data-cam="wide"]').click(); break;
+ case '2': document.querySelector('[data-cam="deck"]').click(); break;
+ case '3': document.querySelector('[data-cam="over"]').click(); break;
+ case 'arrowright': e.preventDefault(); setChapter(Math.min(2,S.chapter+1)); break;
+ case 'arrowleft': e.preventDefault(); setChapter(Math.max(0,S.chapter-1)); break;
+ case 'l': labelBtn.click(); break;
+ }
+});
+
+// init
+resize();
+requestAnimationFrame(frame);
+})();
+</script>
+
+<script>
+/* ---------- Music Room: streams the Grateful Dead Live Music Archive (archive.org) ----------
+ Reuses the curated show catalog from the dead-agentabrams app. Listen-only:
+ audio streams straight from the Internet Archive, is user-initiated, and links
+ back to each source item. The visual scene stays authored — no audio analysis. */
+(() => {
+"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:'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 = 3; // Cornell '77
+
+const $ = id => document.getElementById(id);
+const audio = $('mrAudio'), room = $('musicroom'), sel = $('mrShow'), live = $('live');
+let showIdx=-1, tracks=[], trackIdx=-1, seeking=false;
+
+CATALOG.forEach((s,i)=>{ const o=document.createElement('option'); o.value=String(i); o.textContent=`${s.date} — ${s.title}`; sel.appendChild(o); });
+
+const fmt = t => { if(!isFinite(t)||t<0) t=0; const m=Math.floor(t/60); return m+':'+String(Math.floor(t%60)).padStart(2,'0'); };
+const announce = m => { if(live) live.textContent=m; };
+
+function setFoot(s){
+ const short = s.id.split('.')[0];
+ $('mrFoot').innerHTML =
+ `Source: <a href="https://archive.org/details/${s.id}" target="_blank" rel="noopener noreferrer">archive.org/details/${short}</a> — `+
+ `a community recording from the Grateful Dead Live Music Archive, streamed from the Internet Archive. Listen-only; nothing is copied or re-hosted here.`;
+}
+
+async function loadShow(i, opts){
+ opts=opts||{};
+ try{ audio.pause(); }catch(e){} // stop the current night before loading the next
+ showIdx=i; tracks=[]; trackIdx=-1;
+ const s=CATALOG[i];
+ sel.value=String(i);
+ $('mrNow').textContent=`${s.title} — loading set list…`;
+ $('mrSub').textContent=`${s.date} · ${s.sub}`;
+ $('mrTracks').innerHTML=''; $('mrMsg').hidden=true; $('mrMsg').innerHTML='';
+ setFoot(s);
+ try{
+ const r=await fetch(`https://archive.org/metadata/${s.id}`,{mode:'cors'});
+ if(!r.ok) throw new Error('metadata '+r.status);
+ const j=await r.json();
+ 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.sort((a,b)=> (a.name>b.name?1:-1));
+ tracks=mp3.map(f=>({
+ title:(f.title&&f.title.trim())?f.title.trim():f.name.replace(/\.mp3$/i,''),
+ url:`https://archive.org/download/${s.id}/${encodeURIComponent(f.name)}`
+ }));
+ if(!tracks.length) throw new Error('no audio files');
+ renderTracks();
+ trackIdx=0; audio.src=tracks[0].url; // cue the opener, paused (playback stays user-initiated)
+ $('mrNow').textContent=`♪ ${tracks[0].title}`;
+ highlight();
+ $('mrDur').textContent='0:00'; $('mrCur').textContent='0:00'; $('mrSeek').value='0';
+ announce(`Loaded ${s.title}, ${s.date}. ${tracks.length} tracks from the Live Music Archive.`);
+ if(opts.play) playTrack(0);
+ }catch(e){
+ fallbackEmbed(s);
+ }
+}
+
+function fallbackEmbed(s){
+ $('mrNow').textContent=s.title;
+ const msg=$('mrMsg'); msg.hidden=false;
+ const fileNote = location.protocol==='file:' ? ' Opening the file directly (file://) can block cross-site loading — serving it over http fixes it.' : '';
+ msg.innerHTML=`Couldn't load the set list here.${fileNote} `+
+ `<button class="btn" id="mrEmbedBtn" style="margin-top:8px">▶ Open the archive.org player</button>`;
+ $('mrEmbedBtn').onclick=()=>{
+ msg.innerHTML=`<iframe title="Internet Archive player for ${s.title}" src="https://archive.org/embed/${s.id}" width="100%" height="150" frameborder="0" style="border-radius:8px;border:1px solid var(--panel-edge)"></iframe>`;
+ room.classList.add('playing');
+ };
+}
+
+function renderTracks(){
+ const c=$('mrTracks'); c.innerHTML='';
+ tracks.forEach((t,i)=>{
+ const b=document.createElement('button'); b.type='button'; b.setAttribute('role','listitem');
+ b.innerHTML=`<span class="n">${String(i+1).padStart(2,'0')}</span><span>${t.title}</span>`;
+ b.addEventListener('click',()=>playTrack(i));
+ c.appendChild(b);
+ });
+}
+function highlight(){ [...$('mrTracks').children].forEach((b,i)=>b.setAttribute('aria-current', String(i===trackIdx))); }
+
+function playTrack(i){
+ if(i<0||i>=tracks.length) return;
+ if(i!==trackIdx || !audio.src){ trackIdx=i; audio.src=tracks[i].url; }
+ audio.play().catch(()=>setPlay(false));
+ $('mrNow').textContent=`♪ ${tracks[i].title}`;
+ highlight();
+ announce(`Now playing ${tracks[i].title}.`);
+}
+function setPlay(on){
+ room.classList.toggle('playing',on);
+ const b=$('mrPlay'); b.textContent=on?'❚❚':'▶';
+ b.setAttribute('aria-pressed',String(on)); b.setAttribute('aria-label',on?'Pause':'Play');
+}
+
+audio.addEventListener('play',()=>setPlay(true));
+audio.addEventListener('pause',()=>setPlay(false));
+audio.addEventListener('ended',()=>{ if(trackIdx<tracks.length-1) playTrack(trackIdx+1); else setPlay(false); });
+audio.addEventListener('timeupdate',()=>{
+ const d=audio.duration||0, c=audio.currentTime||0;
+ $('mrCur').textContent=fmt(c); $('mrDur').textContent=fmt(d);
+ if(d>0 && !seeking) $('mrSeek').value=String(Math.round(c/d*1000));
+});
+audio.addEventListener('error',()=>{ if(showIdx>=0 && !tracks.length) fallbackEmbed(CATALOG[showIdx]); });
+
+$('mrPlay').addEventListener('click',()=>{
+ if(!tracks.length){ loadShow(showIdx<0?DEFAULT_SHOW:showIdx,{play:true}); return; }
+ if(trackIdx<0){ playTrack(0); return; }
+ if(audio.paused) audio.play().catch(()=>{}); else audio.pause();
+});
+$('mrPrev').addEventListener('click',()=>{ if(tracks.length) playTrack(Math.max(0,trackIdx-1)); });
+$('mrNext').addEventListener('click',()=>{ if(tracks.length) playTrack(Math.min(tracks.length-1,trackIdx+1)); });
+
+const seek=$('mrSeek');
+seek.addEventListener('input',()=>{ seeking=true; });
+seek.addEventListener('change',()=>{ const d=audio.duration||0; if(d>0) audio.currentTime=(+seek.value/1000)*d; seeking=false; });
+
+sel.addEventListener('change',()=>loadShow(+sel.value,{play:true}));
+
+$('mrTracksToggle').addEventListener('click',()=>{
+ const on=room.classList.toggle('show-tracks');
+ $('mrTracksToggle').setAttribute('aria-expanded',String(on));
+});
+
+const mrToggle=$('mrToggle');
+function toggleRoom(){
+ const open=room.getAttribute('data-open')!=='false';
+ room.setAttribute('data-open',String(!open));
+ mrToggle.setAttribute('aria-expanded',String(!open));
+}
+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');
+}
+
+// cue the default night (no autoplay — the source policy is user-initiated playback)
+loadShow(DEFAULT_SHOW);
+
+// reveal the DEAD back-link only when this room is served as part of the exhibition
+const _p=location.pathname.replace(/\/(index\.html)?$/,'');
+if(_p.endsWith('/room') || /dead\.agentabrams/i.test(location.hostname)){
+ const bl=$('backlink'); if(bl) bl.classList.add('on');
+}
+})();
+</script>
+</body>
+</html>
← 3a0a387 ship and verify dead.agentabrams.com
·
back to Dead Agentabrams
·
Sync ROOM 01 with codex-hardened riverboat room (race guard, f9a40c3 →