[object Object]

← back to Main Stage

The Main Stage: animated gallery hub linking the generative rooms (Riverboat + Playa) + deploy config for stage.agentabrams.com

9a919544caebfce26d065d7f168baa3bc6ebf303 · 2026-09-02 07:10:39 -0700 · Steve Abrams

Files touched

Diff

commit 9a919544caebfce26d065d7f168baa3bc6ebf303
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 2 07:10:39 2026 -0700

    The Main Stage: animated gallery hub linking the generative rooms (Riverboat + Playa) + deploy config for stage.agentabrams.com
---
 .gitignore                             |   5 ++
 README.md                              |  11 +++
 deploy/deploy.sh                       |  33 +++++++
 deploy/stage.agentabrams.com.nginx     |   8 ++
 deploy/stage.agentabrams.com.ssl.nginx |  19 ++++
 index.html                             | 159 +++++++++++++++++++++++++++++++++
 6 files changed, 235 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f8dc3e8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+.env*
+*.log
+.DS_Store
+.playwright-mcp/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8e58349
--- /dev/null
+++ b/README.md
@@ -0,0 +1,11 @@
+# The Main Stage — agentabrams gallery hub
+
+An animated single-file exhibition landing that showcases and links into the
+interactive generative rooms:
+
+- **Room 01 · Scarlet Riverboat Masquerade** → https://dead.agentabrams.com/room/
+- **Room 02 · Playa Ignition** → https://playa.agentabrams.com/
+
+Live: https://stage.agentabrams.com
+
+Add a room by appending to the `PIECES` array in `index.html`.
diff --git a/deploy/deploy.sh b/deploy/deploy.sh
new file mode 100755
index 0000000..beb0165
--- /dev/null
+++ b/deploy/deploy.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+set -euo pipefail
+KAMATERA=root@45.61.58.125
+SITE=stage.agentabrams.com
+HERE="$(cd "$(dirname "$0")/.." && pwd)"
+
+export $(grep -E '^CLOUDFLARE_API_TOKEN=' ~/Projects/secrets-manager/.env)
+ZONE=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "https://api.cloudflare.com/client/v4/zones?name=agentabrams.com" | python3 -c "import sys,json; print(json.load(sys.stdin)['result'][0]['id'])")
+EXISTS=$(curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records?name=$SITE" | python3 -c "import sys,json; print(len(json.load(sys.stdin)['result']))")
+if [ "$EXISTS" = "0" ]; then
+  curl -s -X POST -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" -H 'Content-Type: application/json' "https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records" -d '{"type":"A","name":"stage","content":"45.61.58.125","ttl":300,"proxied":false}' | python3 -c "import sys,json; r=json.load(sys.stdin); assert r['success'],r['errors']; print('DNS record created')"
+else
+  echo 'DNS record already exists'
+fi
+
+rsync -az --delete --exclude .git --exclude deploy --exclude README.md --exclude verification "$HERE/" "$KAMATERA:/var/www/$SITE/"
+CERT="/etc/letsencrypt/live/$SITE/fullchain.pem"
+if ssh "$KAMATERA" "test -f $CERT"; then
+  scp -q "$HERE/deploy/$SITE.ssl.nginx" "$KAMATERA:/etc/nginx/sites-available/$SITE"
+  ssh "$KAMATERA" "ln -sf /etc/nginx/sites-available/$SITE /etc/nginx/sites-enabled/$SITE && nginx -t && systemctl reload nginx"
+else
+  scp -q "$HERE/deploy/$SITE.nginx" "$KAMATERA:/etc/nginx/sites-available/$SITE"
+  ssh "$KAMATERA" "ln -sf /etc/nginx/sites-available/$SITE /etc/nginx/sites-enabled/$SITE && nginx -t && systemctl reload nginx"
+  for _ in $(seq 1 30); do [ -n "$(dig +short "$SITE" @1.1.1.1)" ] && break; sleep 10; done
+  ssh "$KAMATERA" "certbot certonly --webroot -w /var/www/$SITE -d $SITE --non-interactive --agree-tos -m steve@designerwallcoverings.com"
+  scp -q "$HERE/deploy/$SITE.ssl.nginx" "$KAMATERA:/etc/nginx/sites-available/$SITE"
+  ssh "$KAMATERA" "nginx -t && systemctl reload nginx"
+fi
+sleep 2
+code=$(curl -s -o /dev/null -w '%{http_code}' "https://$SITE/")
+echo "https://$SITE -> HTTP $code"
+[ "$code" = '200' ] && echo 'DEPLOY OK' || exit 1
+
diff --git a/deploy/stage.agentabrams.com.nginx b/deploy/stage.agentabrams.com.nginx
new file mode 100644
index 0000000..cbd7d1e
--- /dev/null
+++ b/deploy/stage.agentabrams.com.nginx
@@ -0,0 +1,8 @@
+server {
+  listen 45.61.58.125:80;
+  server_name stage.agentabrams.com;
+  root /var/www/stage.agentabrams.com;
+  index index.html;
+  location /.well-known/acme-challenge/ { root /var/www/stage.agentabrams.com; }
+  location / { try_files $uri $uri/ =404; }
+}
diff --git a/deploy/stage.agentabrams.com.ssl.nginx b/deploy/stage.agentabrams.com.ssl.nginx
new file mode 100644
index 0000000..f0e2ff3
--- /dev/null
+++ b/deploy/stage.agentabrams.com.ssl.nginx
@@ -0,0 +1,19 @@
+server {
+  listen 45.61.58.125:80;
+  server_name stage.agentabrams.com;
+  location /.well-known/acme-challenge/ { root /var/www/stage.agentabrams.com; }
+  location / { return 301 https://$host$request_uri; }
+}
+server {
+  listen 45.61.58.125:443 ssl;
+  server_name stage.agentabrams.com;
+  root /var/www/stage.agentabrams.com;
+  index index.html;
+  ssl_certificate /etc/letsencrypt/live/stage.agentabrams.com/fullchain.pem;
+  ssl_certificate_key /etc/letsencrypt/live/stage.agentabrams.com/privkey.pem;
+  include /etc/letsencrypt/options-ssl-nginx.conf;
+  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
+  add_header X-Content-Type-Options nosniff always;
+  add_header Referrer-Policy strict-origin-when-cross-origin always;
+  location / { try_files $uri $uri/ =404; }
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..cc833f7
--- /dev/null
+++ b/index.html
@@ -0,0 +1,159 @@
+<!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="The Main Stage — an animated exhibition of interactive generative rooms by Agent Abrams. Enter the Scarlet Riverboat Masquerade or Playa Ignition.">
+<meta name="color-scheme" content="dark">
+<title>The Main Stage — An Animated Exhibition</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='%230a0812'/><circle cx='8' cy='8' r='4' fill='none' stroke='%23f2b64a' stroke-width='1.2'/><circle cx='8' cy='8' r='1.6' fill='%23ff5aa0'/></svg>">
+<style>
+  :root{
+    --ink:#efe8f2; --dim:#a99fc0; --gold:#f2b64a; --line:rgba(150,120,220,.22);
+    --serif:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
+    --sans:"Avenir Next","Segoe UI",system-ui,-apple-system,sans-serif;
+  }
+  *{box-sizing:border-box}
+  html{scroll-behavior:smooth}
+  body{margin:0;min-height:100%;background:#070510;color:var(--ink);font-family:var(--sans);overflow-x:hidden}
+  #bg{position:fixed;inset:0;z-index:0;display:block}
+  .wrap{position:relative;z-index:1;max-width:1180px;margin:0 auto;padding:clamp(40px,7vw,90px) clamp(20px,5vw,56px) 60px}
+  .grain{position:fixed;inset:0;pointer-events:none;z-index:2;opacity:.06;mix-blend-mode:overlay;
+    background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 220 220' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}
+
+  header.top{display:flex;flex-direction:column;gap:14px;margin-bottom:clamp(38px,6vw,64px)}
+  .kicker{font-size:11px;letter-spacing:.42em;text-transform:uppercase;color:var(--gold);opacity:.95;margin:0}
+  h1{font-family:var(--serif);font-weight:600;font-style:italic;font-size:clamp(40px,8vw,96px);line-height:.92;margin:0;letter-spacing:-.02em}
+  .lead{max-width:640px;font-family:var(--serif);font-size:clamp(16px,2.2vw,22px);line-height:1.5;color:var(--dim);margin:6px 0 0}
+
+  .pieces{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:clamp(20px,3vw,32px)}
+  .piece{position:relative;display:flex;flex-direction:column;text-decoration:none;color:inherit;
+    border:1px solid var(--line);border-radius:18px;overflow:hidden;background:rgba(16,12,28,.55);
+    backdrop-filter:blur(6px);transition:transform .28s,border-color .28s,box-shadow .28s}
+  .piece:hover,.piece:focus-visible{transform:translateY(-6px);border-color:var(--accent);box-shadow:0 24px 60px rgba(0,0,0,.5),0 0 0 1px var(--accent) inset}
+  .thumb{height:220px;position:relative;overflow:hidden}
+  .thumb canvas{position:absolute;inset:0;width:100%;height:100%}
+  .thumb .veil{position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,0) 40%,rgba(10,8,18,.85))}
+  .body{padding:22px 24px 26px;display:flex;flex-direction:column;gap:10px;flex:1}
+  .body .n{font:11px/1 var(--sans);letter-spacing:.24em;color:var(--accent);text-transform:uppercase}
+  .body h2{font-family:var(--serif);font-weight:600;font-style:italic;font-size:clamp(24px,3vw,32px);line-height:1.02;margin:2px 0}
+  .body p{margin:0;font-size:14px;line-height:1.55;color:var(--dim)}
+  .body .tags{display:flex;gap:8px;flex-wrap:wrap;margin-top:4px}
+  .tag{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);border:1px solid var(--line);border-radius:999px;padding:4px 10px}
+  .go{margin-top:auto;padding-top:16px;font:11px/1 var(--sans);letter-spacing:.14em;text-transform:uppercase;color:var(--accent);display:inline-flex;gap:8px;align-items:center}
+  .piece:hover .go span{transform:translateX(4px)}
+  .go span{transition:transform .2s}
+
+  footer{margin-top:clamp(46px,7vw,80px);padding-top:26px;border-top:1px solid var(--line);display:flex;gap:18px;flex-wrap:wrap;justify-content:space-between;font:11px/1.6 var(--sans);letter-spacing:.06em;color:var(--dim)}
+  footer a{color:var(--gold);text-decoration:none}
+  :focus-visible{outline:3px solid #8fe0ff;outline-offset:3px;border-radius:8px}
+  @media(prefers-reduced-motion:reduce){.piece{transition:none}}
+</style>
+</head>
+<body>
+<canvas id="bg" aria-hidden="true"></canvas>
+<div class="grain" aria-hidden="true"></div>
+<div class="wrap">
+  <header class="top">
+    <p class="kicker">An Animated Exhibition · Agent Abrams</p>
+    <h1>The Main Stage</h1>
+    <p class="lead">A small gallery of interactive, generative rooms — each an original animated world you can steer. Choose a room and step in.</p>
+  </header>
+
+  <main class="pieces" id="pieces"></main>
+
+  <footer>
+    <span>The Main Stage · agentabrams.com</span>
+    <span>Original generative works · listen-only music streamed from the Internet Archive</span>
+  </footer>
+</div>
+
+<script>
+(() => {
+"use strict";
+// ---- pieces (extensible) ----
+const PIECES=[
+  { n:'Room 01', title:'Scarlet Riverboat Masquerade', href:'https://dead.agentabrams.com/room/', accent:'#e0663c',
+    theme:'river', tags:['Grateful Dead','Live Music Archive','Search any show'],
+    desc:'A paddlewheel riverboat drifts a hand-painted crimson canyon while masked dancers turn on three decks — to a live Grateful Dead show you stream and search from the archive. Cutout troupe, beat-synced bears, a 3D orb overhead.' },
+  { n:'Room 02', title:'Playa Ignition', href:'https://playa.agentabrams.com/', accent:'#2fe6ff',
+    theme:'playa', tags:['Burning Man','Synth beat','Ignite the Man'],
+    desc:'A night on the open playa: the neon Man effigy you can set alight, fire-poi dancers, an art car and a spinning 3D orb — all pulsing to a synthesized techno beat with a live spectrum graph.' }
+];
+
+const host=document.getElementById('pieces');
+const thumbs=[];
+PIECES.forEach(p=>{
+  const a=document.createElement('a'); a.className='piece'; a.href=p.href; a.style.setProperty('--accent',p.accent);
+  a.setAttribute('aria-label',`Enter ${p.title}`);
+  a.innerHTML=`<div class="thumb"><canvas></canvas><div class="veil"></div></div>
+    <div class="body"><span class="n">${p.n}</span><h2>${p.title}</h2>
+      <div class="tags">${p.tags.map(t=>`<span class="tag">${t}</span>`).join('')}</div>
+      <p>${p.desc}</p><span class="go">Enter the room <span>→</span></span></div>`;
+  host.appendChild(a);
+  thumbs.push({cv:a.querySelector('canvas'), theme:p.theme, accent:p.accent});
+});
+
+// ---- animated thumbs + background starfield ----
+const bg=document.getElementById('bg'), bx=bg.getContext('2d');
+let W,H,DPR,stars=[];
+function resize(){
+  DPR=Math.min(2,devicePixelRatio||1); W=innerWidth; H=innerHeight;
+  bg.width=W*DPR; bg.height=H*DPR; bx.setTransform(DPR,0,0,DPR,0,0);
+  stars=[]; const n=Math.round(W*H/6000);
+  for(let i=0;i<n;i++) stars.push({x:Math.random()*W,y:Math.random()*H,r:Math.random()*1.3+.2,tw:Math.random()*6.28,vy:.05+Math.random()*.2});
+  thumbs.forEach(t=>{ const c=t.cv, r=c.getBoundingClientRect(); c.width=Math.max(1,r.width*DPR); c.height=Math.max(1,r.height*DPR); c.getContext('2d').setTransform(DPR,0,0,DPR,0,0); c._w=r.width; c._h=r.height; });
+}
+addEventListener('resize',resize);
+const reduce=matchMedia('(prefers-reduced-motion: reduce)').matches;
+
+function drawThumb(t,time){
+  const c=t.cv, g=c.getContext('2d'), w=c._w||300, h=c._h||220;
+  g.clearRect(0,0,w,h);
+  if(t.theme==='river'){
+    const sky=g.createLinearGradient(0,0,0,h); sky.addColorStop(0,'#2a0a12'); sky.addColorStop(.6,'#7c1522'); sky.addColorStop(1,'#c0432b');
+    g.fillStyle=sky; g.fillRect(0,0,w,h);
+    // moon
+    g.fillStyle='rgba(255,207,110,.85)'; g.beginPath(); g.arc(w*.8,h*.3,16,0,7); g.fill();
+    // canyon
+    g.fillStyle='#3a0d16'; g.beginPath(); g.moveTo(0,h); g.lineTo(0,h*.6);
+    for(let x=0;x<=w;x+=30) g.lineTo(x,h*.55+Math.sin(x*.03)*10); g.lineTo(w,h); g.fill();
+    // river shimmer
+    g.strokeStyle='rgba(201,90,58,.5)';
+    for(let i=0;i<4;i++){ g.beginPath(); const y=h*.78+i*8; for(let x=0;x<=w;x+=14) g.lineTo(x,y+Math.sin(x*.05+time*2+i)*2); g.stroke(); }
+    // little boat
+    g.fillStyle='#1e0810'; g.beginPath(); g.ellipse(w*.5,h*.72,34,7,0,0,7); g.fill();
+    g.fillStyle='#f2b64a'; for(let i=0;i<5;i++){ g.beginPath(); g.arc(w*.5-24+i*12,h*.68,1.6,0,7); g.fill(); }
+  } else {
+    const sky=g.createLinearGradient(0,0,0,h); sky.addColorStop(0,'#07061a'); sky.addColorStop(.7,'#2a1836'); sky.addColorStop(1,'#5a4436');
+    g.fillStyle=sky; g.fillRect(0,0,w,h);
+    for(let i=0;i<40;i++){ g.globalAlpha=.5+.5*Math.abs(Math.sin(i+time)); g.fillStyle='#eef0ff'; g.fillRect((i*53)%w,(i*31)%(h*.6),1.4,1.4); }
+    g.globalAlpha=1;
+    // playa
+    g.fillStyle='#6a5040'; g.fillRect(0,h*.72,w,h*.28);
+    // neon man
+    const cx=w*.5, by=h*.72; g.strokeStyle=`hsl(${(time*40)%360},90%,62%)`; g.shadowColor=g.strokeStyle; g.shadowBlur=10; g.lineWidth=3; g.lineCap='round';
+    g.beginPath(); g.moveTo(cx,by-40); g.lineTo(cx-14,by); g.moveTo(cx,by-40); g.lineTo(cx+14,by);
+    g.moveTo(cx-8,by-40); g.lineTo(cx-8,by-70); g.moveTo(cx+8,by-40); g.lineTo(cx+8,by-70);
+    g.moveTo(cx-8,by-68); g.lineTo(cx-22,by-88); g.moveTo(cx+8,by-68); g.lineTo(cx+22,by-88); g.stroke();
+    g.beginPath(); g.arc(cx,by-78,7,0,7); g.stroke(); g.shadowBlur=0;
+  }
+}
+
+let t0=performance.now();
+function loop(now){
+  const time=(now-t0)/1000;
+  // background
+  bx.clearRect(0,0,W,H);
+  const bgGrad=bx.createLinearGradient(0,0,0,H); bgGrad.addColorStop(0,'#07040f'); bgGrad.addColorStop(1,'#120a1e');
+  bx.fillStyle=bgGrad; bx.fillRect(0,0,W,H);
+  for(const s of stars){ if(!reduce){ s.y+=s.vy; if(s.y>H)s.y=0; } const tw=.4+.6*Math.abs(Math.sin(s.tw+time)); bx.globalAlpha=tw; bx.fillStyle='#dfe4ff'; bx.beginPath(); bx.arc(s.x,s.y,s.r,0,7); bx.fill(); }
+  bx.globalAlpha=1;
+  thumbs.forEach(t=>drawThumb(t,time));
+  requestAnimationFrame(loop);
+}
+resize(); requestAnimationFrame(loop);
+})();
+</script>
+</body>
+</html>

(oldest)  ·  back to Main Stage  ·  Add Room 03 · Aurora Drift to The Main Stage (animated auror 1f83efa →