← back to Dead Agentabrams
Music room: adjustable movie screen (drag+scale) + optional draggable orb (Steve)
a7cda269862f9c88a5afbb67fe2b00ec267a1ad5 · 2026-09-03 12:37:38 -0700 · Steve Abrams
- Movie screen is now draggable to reposition and corner-resizable to scale up/down
(--movie-dx/-dy/-scale, persisted to localStorage); gold corner handle + move overlay.
- Orb is OFF on load ('onload no orb') but a new 🔮 Orb toggle turns it on, and once on it
can be DRAGGED around the room (click = AI VJ, drag = move; position persisted). The WebGL
loop skips all work while the orb is off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit a7cda269862f9c88a5afbb67fe2b00ec267a1ad5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 3 12:37:38 2026 -0700
Music room: adjustable movie screen (drag+scale) + optional draggable orb (Steve)
- Movie screen is now draggable to reposition and corner-resizable to scale up/down
(--movie-dx/-dy/-scale, persisted to localStorage); gold corner handle + move overlay.
- Orb is OFF on load ('onload no orb') but a new 🔮 Orb toggle turns it on, and once on it
can be DRAGGED around the room (click = AI VJ, drag = move; position persisted). The WebGL
loop skips all work while the orb is off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
room/index.html | 97 ++++++++++++++++++++++++++++++++++++++++++++++++---------
1 file changed, 83 insertions(+), 14 deletions(-)
diff --git a/room/index.html b/room/index.html
index 55bc4cd..dfe5441 100644
--- a/room/index.html
+++ b/room/index.html
@@ -24,6 +24,7 @@
#stage{position:fixed;inset:0;display:block}
canvas{position:absolute;inset:0;width:100%;height:100%}
#orb{z-index:4;pointer-events:none}
+ #stage:not(.orb-on) #orb{display:none} /* orb OFF by default (Steve: onload no orb); 🔮 Orb toggles it on */
/* ---- Concert-video backdrop (TK-11178): official GD footage behind the generative scene.
Confined to a positioned box (Steve: "the area in back of where singers would sing") —
defaults roughly over the stage riser, fully dialable via the Videos panel's X/Y/W/H
@@ -48,15 +49,24 @@
#venueVideo). Shown ONLY while a video is playing (.movie-on). ---- */
#stage.movie-on #venueVideo{
opacity:1; z-index:1;
- left:50%; top:33%; transform:translate(-50%,-50%);
- width:min(54vw,760px); height:auto; aspect-ratio:16/9;
+ left:calc(50% + var(--movie-dx, 0px)); top:calc(33% + var(--movie-dy, 0px));
+ transform:translate(-50%,-50%);
+ width:calc(min(54vw,760px) * var(--movie-scale, 1)); height:auto; aspect-ratio:16/9;
background:#000; border-radius:4px; border:2px solid #0a0304;
box-shadow:0 0 0 8px #17070a, 0 0 0 10px rgba(242,182,74,.45),
0 30px 90px rgba(0,0,0,.75), 0 0 70px rgba(242,182,74,.18);
}
#stage.movie-on #sky{opacity:.5}
#stage.movie-on #scene{opacity:1}
- @media (max-width:820px){ #stage.movie-on #venueVideo{width:86vw;top:30%} }
+ @media (max-width:820px){ #stage.movie-on #venueVideo{width:calc(86vw * var(--movie-scale,1));top:30%} }
+ /* drag-to-move + corner-resize handles on the movie screen (Steve: adjust + scale up/down) */
+ #venueVideo .mv-move,#venueVideo .mv-size{position:absolute;pointer-events:auto;display:none}
+ #stage.movie-on #venueVideo .mv-move{display:block;inset:0;cursor:move;z-index:1}
+ #stage.movie-on #venueVideo .mv-size{display:block;right:-9px;bottom:-9px;width:26px;height:26px;z-index:2;
+ cursor:nwse-resize;border-radius:5px;background:rgba(242,182,74,.9);
+ box-shadow:0 2px 10px rgba(0,0,0,.55);border:1px solid #17070a}
+ #stage.movie-on #venueVideo .mv-size::after{content:"⤡";position:absolute;inset:0;display:flex;
+ align-items:center;justify-content:center;color:#17070a;font-size:14px;font-weight:700}
.btn:disabled,button:disabled{opacity:.42;cursor:not-allowed}
/* ---- Controls in a collapsed RIGHT drawer, opened by the top-right hamburger (TK-11178 UI) ---- */
#dock{
@@ -353,7 +363,7 @@
<h1 class="sr">Scarlet Riverboat Masquerade — an animated generative gallery</h1>
<!-- role removed from <main>: role="application" suppressed normal screen-reader navigation over the native controls + prose -->
- <div id="venueVideo" aria-hidden="true"><div id="ytHost"></div></div>
+ <div id="venueVideo" aria-hidden="true"><div id="ytHost"></div><div class="mv-move" title="Drag to move the screen"></div><div class="mv-size" title="Drag to scale the screen"></div></div>
<canvas id="sky" aria-hidden="true"></canvas>
<canvas id="scene" aria-hidden="true"></canvas>
<canvas id="orb" aria-hidden="true"></canvas>
@@ -432,6 +442,7 @@
<button class="btn" id="reactBtn" aria-pressed="false">♫ React to Music</button>
<button class="btn" id="pteroBtn">🦖 Fly Over</button>
<button class="btn" id="venueBtn" aria-pressed="true">🏛 Venue</button>
+ <button class="btn" id="orbBtn" aria-pressed="false" title="Show the orb — then drag it around the room">🔮 Orb</button>
</div>
</div>
<div class="group">
@@ -1598,6 +1609,15 @@ venueBtn.addEventListener('click',()=>{
announce(venueOn?'Concert-video backdrop on.':'Concert-video backdrop off.');
});
+// Orb on/off (Steve: "allow user to turn on the orb"). Off on load; drag it to move it around.
+const orbBtn=document.getElementById('orbBtn');
+if(orbBtn) orbBtn.addEventListener('click',()=>{
+ window.__orbOn=!window.__orbOn;
+ orbBtn.setAttribute('aria-pressed',String(window.__orbOn));
+ document.getElementById('stage').classList.toggle('orb-on', window.__orbOn);
+ announce(window.__orbOn?'Orb on — drag it to move it around the room.':'Orb off.');
+});
+
// keyboard shortcuts
window.addEventListener('keydown',e=>{
const t=e.target;
@@ -1733,6 +1753,34 @@ const MR_VIDEOS=[
});
})();
+// ---- Movie screen: drag anywhere to move, drag the gold corner to scale up/down (Steve). Persisted. ----
+(function(){
+ const stage=document.getElementById('stage'), vv=document.getElementById('venueVideo');
+ if(!stage||!vv) return;
+ const root=document.documentElement.style, KEY='deadroom.movie.v1';
+ const st=Object.assign({dx:0,dy:0,scale:1}, (function(){try{return JSON.parse(localStorage.getItem(KEY))||{};}catch(e){return {};}})());
+ const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
+ function apply(){ root.setProperty('--movie-dx',st.dx+'px'); root.setProperty('--movie-dy',st.dy+'px'); root.setProperty('--movie-scale',String(st.scale)); }
+ function save(){ try{ localStorage.setItem(KEY,JSON.stringify(st)); }catch(e){} }
+ apply();
+ const mover=vv.querySelector('.mv-move');
+ if(mover) mover.addEventListener('pointerdown',e=>{
+ e.preventDefault(); try{mover.setPointerCapture(e.pointerId);}catch(_){}
+ const sx=e.clientX, sy=e.clientY, ox=st.dx, oy=st.dy;
+ const mv=ev=>{ st.dx=clamp(ox+(ev.clientX-sx),-innerWidth*0.45,innerWidth*0.45); st.dy=clamp(oy+(ev.clientY-sy),-innerHeight*0.30,innerHeight*0.60); apply(); };
+ const up=()=>{ mover.removeEventListener('pointermove',mv); mover.removeEventListener('pointerup',up); save(); };
+ mover.addEventListener('pointermove',mv); mover.addEventListener('pointerup',up);
+ });
+ const sizer=vv.querySelector('.mv-size');
+ if(sizer) sizer.addEventListener('pointerdown',e=>{
+ e.preventDefault(); e.stopPropagation(); try{sizer.setPointerCapture(e.pointerId);}catch(_){}
+ const sx=e.clientX, os=st.scale;
+ const mv=ev=>{ st.scale=clamp(os+(ev.clientX-sx)/400,0.35,3.2); apply(); };
+ const up=()=>{ sizer.removeEventListener('pointermove',mv); sizer.removeEventListener('pointerup',up); save(); };
+ sizer.addEventListener('pointermove',mv); sizer.addEventListener('pointerup',up);
+ });
+})();
+
function _yearOf(s){
const m=String((s&&s.date)||'').match(/\b(19\d{2})\b/); if(m) return +m[1];
const m2=String((s&&s.id)||'').match(/gd(\d{2,4})/i);
@@ -2349,9 +2397,11 @@ if(_p.endsWith('/room') || /dead\.agentabrams/i.test(location.hostname)){
<script>
(function(){
if(!window.THREE) return;
- // Steve: "remove the orb. onload no orb." — the WebGL AI-VJ orb is disabled; the concert
- // now plays on a movie screen on the stage instead. Flip ORB_ENABLED to restore it.
- const ORB_ENABLED=false; if(!ORB_ENABLED) return;
+ // Steve: "onload no orb" + "allow user to turn on the orb" + "allow orb to move around the
+ // room". The orb is OFF by default (window.__orbOn=false, #stage lacks .orb-on so #orb is
+ // display:none) and the loop skips rendering; the 🔮 Orb toggle turns it on, and the orb can
+ // be dragged around the room.
+ if(window.__orbOn===undefined) window.__orbOn=false;
const canvas=document.getElementById('orb'); if(!canvas) return;
let W=innerWidth,H=innerHeight;
const renderer=new THREE.WebGLRenderer({canvas,alpha:true,antialias:true});
@@ -2359,6 +2409,8 @@ 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
+ // restore a dragged-around orb position (Steve: "allow orb to move around the room")
+ try{ const op=JSON.parse(localStorage.getItem('deadroom.orbpos.v1')); if(op){ group.position.x=op.x||0; group.position.y=(op.y!=null?op.y:1.45); } }catch(_){}
// ---- 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);
@@ -2439,22 +2491,39 @@ if(_p.endsWith('/room') || /dead\.agentabrams/i.test(location.hostname)){
}
}
- // click the orb to hand it to the AI VJ (autonomous, reads the live audio and choreographs itself)
+ // DRAG the orb to move it around the room; a click (no drag) hands it to the AI VJ.
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=>{
+ if(!window.__orbOn) return; // orb off → ignore
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.';
- }
+ if(Math.hypot(e.clientX-o.x, e.clientY-o.y) >= o.r) return; // not on the orb
+ e.stopPropagation();
+ const sx=e.clientX, sy=e.clientY, px=group.position.x, py=group.position.y, k=(2*5*Math.tan(50*Math.PI/360))/H; // world-units per screen px
+ let moved=false;
+ const mv=ev=>{
+ if(Math.hypot(ev.clientX-sx, ev.clientY-sy)>4) moved=true;
+ group.position.x=Math.max(-3.4, Math.min(3.4, px+(ev.clientX-sx)*k));
+ group.position.y=Math.max(-1.2, Math.min(3.4, py-(ev.clientY-sy)*k));
+ };
+ const up=()=>{
+ removeEventListener('pointermove',mv,true); removeEventListener('pointerup',up,true);
+ if(!moved){ // a click → toggle AI VJ
+ ai=!ai;
+ 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.';
+ } else { // a drag → remember where the orb was moved
+ try{ localStorage.setItem('deadroom.orbpos.v1', JSON.stringify({x:group.position.x,y:group.position.y})); }catch(_){}
+ }
+ };
+ addEventListener('pointermove',mv,true); addEventListener('pointerup',up,true);
}, true);
const easeOut=x=>1-Math.pow(1-x,3);
let last=performance.now();
let _orbStatic=false;
function loop(now){ requestAnimationFrame(loop);
+ if(!window.__orbOn){ return; } // orb off (default) → skip all render work
const dt=Math.min(0.05,((now||performance.now())-last)/1000); last=now||performance.now();
const s=window.SRM||{}, t=(now||performance.now())/1000;
// Honor reduced motion (WCAG 2.3.3): draw one calm static frame, then stop spinning + re-rendering.
← 650582d Music room: concert plays on a movie SCREEN on the stage, or
·
back to Dead Agentabrams
·
Music room: mobile-friendly pass — shrink + fluid-scale text 8769b6f →