← back to Dead Agentabrams
orb: song-change UFO split (5 saucers + light beams), celestial patterns, click-to-AI-VJ takeover; animals roam + move to tempo
4ea4914bf9cc66bc95f9edaec69024f6093b810f · 2026-09-02 08:14:25 -0700 · Steve Abrams
Files touched
Diff
commit 4ea4914bf9cc66bc95f9edaec69024f6093b810f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 2 08:14:25 2026 -0700
orb: song-change UFO split (5 saucers + light beams), celestial patterns, click-to-AI-VJ takeover; animals roam + move to tempo
---
room/index.html | 260 ++++++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 235 insertions(+), 25 deletions(-)
diff --git a/room/index.html b/room/index.html
index d08161d..c8a5337 100644
--- a/room/index.html
+++ b/room/index.html
@@ -918,6 +918,32 @@ 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.7 + S.tempo*0.65; // 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;
@@ -933,13 +959,15 @@ function drawAnimals(){
const t=S.t;
const scaleA = S.cam==='deck'?1.15:1;
+ const hop=_beatHop();
- 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);
+ // 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);
+ drawBearCubs(t, scaleA, baseY); // the troupe dances + sidesteps to tempo
+ drawSongbirds(t, scaleA, baseY); // songbirds now FLY around the sky
ctx.restore();
}
@@ -995,7 +1023,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 + ((t*22)% (S.W*0.14)) - S.W*0.05, y+hopY);
+ ctx.translate(x, y+hopY); // roam is handled by _place(); keep only the vertical hop
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();
@@ -1081,8 +1109,12 @@ 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 x = (N===1) ? W*0.16 : margin + span*(i/(N-1));
+ 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 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]);
}
@@ -1117,10 +1149,15 @@ function songbird(x,y,s,t,ph){
}
}
function drawSongbirds(t,scaleA,baseY){
- 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);
+ 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();
+ }
}
// ---------- River foreground with parallax reflections ----------
@@ -1584,7 +1621,18 @@ 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{
@@ -1600,16 +1648,43 @@ let _avg=0, _clock=0, _lastBeat=-99;
function sampleLevel(){
if(_analyser){
_analyser.getByteFrequencyData(_freq);
- let sum=0, n=Math.max(1,Math.floor(_freq.length/6)); // low-end (bass) energy
+ const L=_freq.length; // 128 bins, ~0..22kHz
+ let sum=0, n=Math.max(1,Math.floor(L/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
+ 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;
// 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)
+ 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'); }
}
requestAnimationFrame(sampleLevel);
}
@@ -1743,28 +1818,163 @@ 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}));
- group.add(core,wire);
+ 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]); }
+
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); });
- 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
+
+ // ---- 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);
const energy=s.react ? (level*0.6+pulse*0.7) : idle*0.12;
- 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
+
+ // 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;
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);
}
- loop();
+ requestAnimationFrame(loop);
})();
</script>
</body>
← 0feb28b snapshot before orb UFO/celestial + roaming-animal build
·
back to Dead Agentabrams
·
Expand curated shows to 17 (1969–1994, verified LMA soundboa f1c7dbc →