← 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 →