[object Object]

← back to Playa Ignition

Add 3D spinning orb (Three.js, beat-reactive wireframe icosahedron), a melodic synth arp (minor-pentatonic), and a live spectrum graph of the audio

9a8fcad6ecdfc5feb7489f9c4ce2a14ae678b5bf · 2026-09-01 22:50:20 -0700 · Steve Abrams

Files touched

Diff

commit 9a8fcad6ecdfc5feb7489f9c4ce2a14ae678b5bf
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 1 22:50:20 2026 -0700

    Add 3D spinning orb (Three.js, beat-reactive wireframe icosahedron), a melodic synth arp (minor-pentatonic), and a live spectrum graph of the audio
---
 index.html | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 66 insertions(+), 4 deletions(-)

diff --git a/index.html b/index.html
index 81ccba5..935d0d1 100644
--- a/index.html
+++ b/index.html
@@ -18,6 +18,7 @@
   html,body{margin:0;height:100%;background:#05050f;color:var(--ink);font-family:var(--sans);overflow:hidden}
   #stage{position:fixed;inset:0}
   canvas{position:absolute;inset:0;width:100%;height:100%}
+  #orb{z-index:4;pointer-events:none}
   .grain{position:fixed;inset:0;pointer-events:none;z-index:5;opacity:.08;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")}
 
@@ -80,6 +81,7 @@
   <h1 class="sr">Playa Ignition — an animated generative Burning Man gallery</h1>
   <canvas id="sky" aria-hidden="true"></canvas>
   <canvas id="scene" aria-hidden="true"></canvas>
+  <canvas id="orb" aria-hidden="true"></canvas>
   <div class="grain" aria-hidden="true"></div>
 
   <div class="titleplate">
@@ -153,17 +155,31 @@ const S={ t:0, playing:true, reduced:false, trails:true,
   cam:'wide', burn:0, pulse:0, beatCount:0, _beatPhase:0, _eighthPhase:0,
   sound:false, volume:0.6,
   W:0,H:0,DPR:1 };
+try{ window.__playaS=S; }catch(e){}   // expose state to the Three.js orb
 if(window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) S.reduced=true;
 
 function clamp(v,a,b){return Math.max(a,Math.min(b,v));}
 function announce(m){ live.textContent=m; }
 
 // ---------- synthesized techno beat (original, no audio files) ----------
-let actx=null, master=null;
+let actx=null, master=null, analyser=null, freqData=null;
 function ensureAudio(){ if(actx) return true; try{
   actx=new (window.AudioContext||window.webkitAudioContext)();
-  master=actx.createGain(); master.gain.value=S.volume; master.connect(actx.destination); return true;
+  master=actx.createGain(); master.gain.value=S.volume;
+  analyser=actx.createAnalyser(); analyser.fftSize=128; analyser.smoothingTimeConstant=0.75;
+  freqData=new Uint8Array(analyser.frequencyBinCount);
+  master.connect(analyser); master.connect(actx.destination);   // tap for the live graph
+  try{ window.__playaAnalyser=analyser; }catch(e){}
+  return true;
 }catch(e){ return false; } }
+// melodic synth pluck (the "synthesizer") — minor-pentatonic arp note
+function synth(freq){ if(!actx) return; const t=actx.currentTime, o=actx.createOscillator(), g=actx.createGain(), lp=actx.createBiquadFilter();
+  o.type='square'; o.frequency.value=freq; lp.type='lowpass';
+  lp.frequency.setValueAtTime(2400,t); lp.frequency.exponentialRampToValueAtTime(600,t+0.25);
+  g.gain.setValueAtTime(0.0001,t); g.gain.linearRampToValueAtTime(0.10,t+0.01); g.gain.exponentialRampToValueAtTime(0.001,t+0.28);
+  o.connect(lp); lp.connect(g); g.connect(master); o.start(t); o.stop(t+0.3); }
+const ARP=[220,261.63,293.66,349.23,392,349.23,293.66,261.63];  // A minor pentatonic run
+let arpIdx=0;
 function kick(){ if(!actx) return; const t=actx.currentTime, o=actx.createOscillator(), g=actx.createGain();
   o.frequency.setValueAtTime(165,t); o.frequency.exponentialRampToValueAtTime(48,t+0.12);
   g.gain.setValueAtTime(1,t); g.gain.exponentialRampToValueAtTime(0.001,t+0.20);
@@ -392,6 +408,16 @@ function drawFlares(dt){ if(!flares.length) return; ctx.save(); ctx.globalCompos
     if(p.life<=0){ flares.splice(i,1); continue; } ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle=`hsl(${p.hue},90%,60%)`; ctx.beginPath(); ctx.arc(p.x,p.y,2.2*p.life+0.6,0,7); ctx.fill(); }
   ctx.restore(); ctx.globalAlpha=1; }
 
+// live music graph — spectrum of the synthesized audio
+function drawSpectrum(){
+  if(!analyser || !S.sound) return;
+  analyser.getByteFrequencyData(freqData);
+  const W=S.W,H=S.H, n=freqData.length, bw=W/n, baseY=H-4;
+  ctx.save(); ctx.globalCompositeOperation='lighter';
+  for(let i=0;i<n;i++){ const v=freqData[i]/255, bh=v*H*0.20, hue=(200+i/n*160)%360;
+    ctx.fillStyle=`hsla(${hue},90%,60%,0.85)`; ctx.fillRect(i*bw+1, baseY-bh, bw-2, bh); }
+  ctx.restore();
+}
 function drawEmbers(dt){ if(!embers.length) return; ctx.save(); ctx.globalCompositeOperation='lighter';
   for(let i=embers.length-1;i>=0;i--){ const p=embers[i]; p.x+=p.vx*dt; p.y+=p.vy*dt; p.vy+=6*dt; p.life-=dt*0.5;
     if(p.life<=0){ embers.splice(i,1); continue; } ctx.globalAlpha=Math.max(0,p.life); ctx.fillStyle='#ffb347'; ctx.beginPath(); ctx.arc(p.x,p.y,2*p.life+0.5,0,7); ctx.fill(); }
@@ -414,8 +440,8 @@ function frame(now){
   if(S.playing && !S.reduced){
     S._beatPhase += dt*(S.bpm/60);
     if(S._beatPhase>=1){ S._beatPhase-=1; S.pulse=1; S.beatCount++; if(S.sound){ kick(); bass(); hat(); } }
-    S._eighthPhase += dt*(S.bpm/60)*2;      // hi-hats at 2x (eighth notes)
-    if(S._eighthPhase>=1){ S._eighthPhase-=1; if(S.sound && (S.beatCount&1)) hat(); }
+    S._eighthPhase += dt*(S.bpm/60)*2;      // eighth-note grid
+    if(S._eighthPhase>=1){ S._eighthPhase-=1; if(S.sound){ if(S.beatCount&1) hat(); synth(ARP[arpIdx%ARP.length]); arpIdx++; } }
     S.pulse *= Math.pow(0.06, dt*4);        // decay per beat
   } else S.pulse=0;
   // burn decay
@@ -438,6 +464,7 @@ function frame(now){
     for(let i=0;i<N;i++){ const x=(N===1)?W*0.16:margin+span*(i/(N-1)); bear(x, H*0.86+((i*53)%9-4), size, i*1.3, BEAR_COLORS[i%BEAR_COLORS.length]); } }
   drawEmbers(dt);
   drawFlares(dt);
+  drawSpectrum();
   requestAnimationFrame(frame);
 }
 
@@ -501,5 +528,40 @@ resize();
 requestAnimationFrame(frame);
 })();
 </script>
+
+<!-- 3D spinning orb (Three.js). Classic build so it runs from file://; skips silently if offline. -->
+<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
+<script>
+(function(){
+  if(!window.THREE) return;                       // offline: 2D scene still works
+  const canvas=document.getElementById('orb'); if(!canvas) return;
+  let W=innerWidth,H=innerHeight;
+  const renderer=new THREE.WebGLRenderer({canvas,alpha:true,antialias:true});
+  renderer.setPixelRatio(Math.min(2,devicePixelRatio||1)); renderer.setSize(W,H,false);
+  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=0.35; scene.add(group);
+  const wire=new THREE.Mesh(new THREE.IcosahedronGeometry(1.45,2), new THREE.MeshBasicMaterial({wireframe:true,color:0x2fe6ff,transparent:true,opacity:0.85}));
+  const core=new THREE.Mesh(new THREE.IcosahedronGeometry(1.15,3), new THREE.MeshStandardMaterial({color:0x150022,emissive:0xff2fd0,emissiveIntensity:0.7,roughness:0.35,metalness:0.6,transparent:true,opacity:0.85}));
+  group.add(core,wire);
+  const l1=new THREE.PointLight(0xff2fd0,1.4,60); l1.position.set(4,3,5); scene.add(l1);
+  const l2=new THREE.PointLight(0x2fe6ff,1.2,60); l2.position.set(-4,-2,4); scene.add(l2);
+  scene.add(new THREE.AmbientLight(0x404060,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.__playaS||{}, pulse=s.pulse||0, paused=(s.playing===false)||s.reduced, t=performance.now()/1000;
+    if(!paused) group.rotation.y+=0.012;
+    group.rotation.x=Math.sin(t*0.3)*0.22;
+    group.scale.setScalar(1+pulse*0.20);
+    core.material.emissiveIntensity=0.5+pulse*1.4;
+    const hue=(t*30)%360;
+    wire.material.color.setHSL(hue/360,0.9,0.62);
+    col.setHSL(((hue+130)%360)/360,0.9,0.55); core.material.emissive.copy(col);
+    renderer.render(scene,camera);
+  }
+  loop();
+})();
+</script>
 </body>
 </html>

← 393b5cd Add synthesized techno beat (kick+bass+hats locked to BPM) w  ·  back to Playa Ignition  ·  Add '← The Main Stage' back-link (top-center) 6c2ddb9 →