[object Object]

← back to Playa Ignition

Add synthesized techno beat (kick+bass+hats locked to BPM) with Sound toggle + Volume; move menu to a ☰ hamburger in the upper-left

393b5cd2dddef9b140427de8685ea00ab2731369 · 2026-09-01 22:42:04 -0700 · Steve Abrams

Files touched

Diff

commit 393b5cd2dddef9b140427de8685ea00ab2731369
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Sep 1 22:42:04 2026 -0700

    Add synthesized techno beat (kick+bass+hats locked to BPM) with Sound toggle + Volume; move menu to a ☰ hamburger in the upper-left
---
 index.html | 49 ++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 42 insertions(+), 7 deletions(-)

diff --git a/index.html b/index.html
index b05ef42..81ccba5 100644
--- a/index.html
+++ b/index.html
@@ -24,12 +24,12 @@
   .skip{position:fixed;top:-60px;left:12px;z-index:20;background:var(--neon2);color:#04121a;padding:10px 16px;border-radius:8px;font-weight:600;text-decoration:none;transition:top .2s}
   .skip:focus{top:12px}
 
-  .titleplate{position:fixed;top:22px;left:26px;z-index:6;max-width:min(52vw,560px);pointer-events:none;text-shadow:0 2px 18px rgba(0,0,0,.75)}
+  .titleplate{position:fixed;top:74px;left:26px;z-index:6;max-width:min(52vw,560px);pointer-events:none;text-shadow:0 2px 18px rgba(0,0,0,.75)}
   .titleplate .kicker{letter-spacing:.42em;text-transform:uppercase;font-size:11px;color:var(--neon2);opacity:.95;margin:0 0 6px 2px}
   .titleplate h1{font-family:var(--serif);font-weight:600;font-style:italic;font-size:clamp(26px,4.4vw,52px);line-height:.98;margin:0;color:var(--ink)}
   .titleplate .sub{font-family:var(--serif);font-size:14px;color:var(--ink-dim);margin-top:8px;font-style:italic}
 
-  .chrome-toggle{position:fixed;top:22px;right:26px;z-index:12}
+  .chrome-toggle{position:fixed;top:20px;left:20px;z-index:12;font-size:18px;padding:8px 13px;line-height:1}
   #stage.hide-chrome .dock,#stage.hide-chrome .cams,#stage.hide-chrome .label-toggle,#stage.hide-chrome .museum{display:none!important}
 
   .dock{position:fixed;left:50%;bottom:20px;transform:translateX(-50%);z-index:7;display:flex;gap:18px;align-items:stretch;flex-wrap:wrap;justify-content:center;max-width:min(96vw,1120px);
@@ -88,7 +88,7 @@
     <p class="sub" id="phaseLabel">Deep Playa · after dark</p>
   </div>
 
-  <button class="btn chrome-toggle" id="chromeBtn" aria-pressed="false">☰ Controls</button>
+  <button class="btn chrome-toggle" id="chromeBtn" aria-pressed="false" aria-label="Menu — show or hide controls">☰</button>
 
   <div class="cams" role="group" aria-label="Camera view">
     <button class="btn" data-cam="wide" aria-pressed="true">◈ The Playa</button>
@@ -106,9 +106,13 @@
         <button class="btn burn" id="burnBtn">🔥 Ignite the Man</button>
         <button class="btn" id="trailBtn" aria-pressed="true">✧ Light Trails</button>
       </div>
+      <div class="toggle">
+        <button class="btn" id="soundBtn" aria-pressed="false">🔊 Sound</button>
+      </div>
     </div>
     <div class="group">
       <div class="field"><label for="bpm">Beat <span class="val" id="bpmV">124 BPM</span></label><input type="range" id="bpm" min="60" max="180" value="124"></div>
+      <div class="field"><label for="vol">Volume <span class="val" id="volV">60%</span></label><input type="range" id="vol" min="0" max="100" value="60"></div>
       <div class="field"><label for="neon">Neon <span class="val" id="neonV">Bright</span></label><input type="range" id="neon" min="0" max="100" value="75"></div>
       <div class="field"><label for="crowd">Crowd <span class="val" id="crowdV">Full</span></label><input type="range" id="crowd" min="0" max="100" value="80"></div>
       <div class="field"><label for="dust">Dust <span class="val" id="dustV">Drifting</span></label><input type="range" id="dust" min="0" max="100" value="55"></div>
@@ -146,12 +150,33 @@ const live=document.getElementById('live');
 
 const S={ t:0, playing:true, reduced:false, trails:true,
   bpm:124, neon:0.75, crowd:0.8, dust:0.55, bears:3,
-  cam:'wide', burn:0, pulse:0, beatCount:0, _beatPhase:0,
+  cam:'wide', burn:0, pulse:0, beatCount:0, _beatPhase:0, _eighthPhase:0,
+  sound:false, volume:0.6,
   W:0,H:0,DPR:1 };
 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;
+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;
+}catch(e){ return false; } }
+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);
+  o.connect(g); g.connect(master); o.start(t); o.stop(t+0.22); }
+function bass(){ if(!actx) return; const t=actx.currentTime, o=actx.createOscillator(), g=actx.createGain(), lp=actx.createBiquadFilter();
+  o.type='sawtooth'; o.frequency.value=55; lp.type='lowpass'; lp.frequency.value=420;
+  g.gain.setValueAtTime(0.0001,t); g.gain.linearRampToValueAtTime(0.16,t+0.02); g.gain.exponentialRampToValueAtTime(0.001,t+0.22);
+  o.connect(lp); lp.connect(g); g.connect(master); o.start(t); o.stop(t+0.24); }
+function hat(){ if(!actx) return; const t=actx.currentTime, n=Math.floor(actx.sampleRate*0.045);
+  const buf=actx.createBuffer(1,n,actx.sampleRate), d=buf.getChannelData(0);
+  for(let i=0;i<n;i++) d[i]=(Math.random()*2-1)*Math.pow(1-i/n,3);
+  const src=actx.createBufferSource(); src.buffer=buf; const hp=actx.createBiquadFilter(); hp.type='highpass'; hp.frequency.value=7500;
+  const g=actx.createGain(); g.gain.value=0.22; src.connect(hp); hp.connect(g); g.connect(master); src.start(t); }
 // cutout helper (flat fill + bold outline)
 const OUTLINE='rgba(6,4,14,0.92)';
 function cut(fill,w){ ctx.fillStyle=fill; ctx.strokeStyle=OUTLINE; ctx.lineWidth=w||2.4; ctx.lineJoin='round'; ctx.lineCap='round'; }
@@ -388,8 +413,10 @@ function frame(now){
   // authored beat clock -> pulse
   if(S.playing && !S.reduced){
     S._beatPhase += dt*(S.bpm/60);
-    if(S._beatPhase>=1){ S._beatPhase-=1; S.pulse=1; S.beatCount++; }
-    S.pulse *= Math.pow(0.06, dt*4);       // decay per beat
+    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.pulse *= Math.pow(0.06, dt*4);        // decay per beat
   } else S.pulse=0;
   // burn decay
   if(S.burn>0){ S.burn=Math.max(0,S.burn-dt*0.18); }
@@ -436,13 +463,21 @@ const burnBtn=document.getElementById('burnBtn');
 function ignite(){ S.burn=1; announce('The Man is alight — the burn begins.'); }
 burnBtn.addEventListener('click',ignite);
 
+const soundBtn=document.getElementById('soundBtn');
+soundBtn.addEventListener('click',()=>{
+  if(!S.sound){ if(!ensureAudio()){ announce('Audio unavailable in this browser.'); return; } if(actx.state==='suspended') actx.resume(); }
+  S.sound=!S.sound; soundBtn.setAttribute('aria-pressed',S.sound);
+  announce(S.sound?'Sound on — the beat plays.':'Sound off.');
+});
+document.getElementById('vol').addEventListener('input',e=>{ S.volume=+e.target.value/100; if(master) master.gain.value=S.volume; document.getElementById('volV').textContent=e.target.value+'%'; });
+
 document.querySelectorAll('[data-cam]').forEach(b=>{ b.addEventListener('click',()=>{ S.cam=b.dataset.cam; document.querySelectorAll('[data-cam]').forEach(x=>x.setAttribute('aria-pressed',x===b)); ctx.clearRect(0,0,S.W,S.H); const nm={wide:'The playa',man:'The Man',over:'Overhead'}; announce(nm[S.cam]+' view.'); }); });
 
 const labelBtn=document.getElementById('labelBtn'), museum=document.getElementById('museum');
 labelBtn.addEventListener('click',()=>{ const open=museum.hidden; museum.hidden=!open; labelBtn.setAttribute('aria-expanded',String(open)); });
 
 const stageEl=document.getElementById('stage'), chromeBtn=document.getElementById('chromeBtn');
-function toggleChrome(){ const hidden=stageEl.classList.toggle('hide-chrome'); chromeBtn.setAttribute('aria-pressed',String(!hidden)); chromeBtn.textContent=hidden?'☰ Controls':'✕ Close'; announce(hidden?'Controls hidden.':'Controls shown.'); }
+function toggleChrome(){ const hidden=stageEl.classList.toggle('hide-chrome'); chromeBtn.setAttribute('aria-pressed',String(!hidden)); chromeBtn.textContent=hidden?'☰':'✕'; announce(hidden?'Controls hidden.':'Controls shown.'); }
 chromeBtn.addEventListener('click',toggleChrome);
 
 stageEl.addEventListener('pointerdown',e=>{ if(e.target.closest('button,input,select,a,.dock,.cams,.museum,.label-toggle')) return; const r=scene.getBoundingClientRect(); spawnFlare(e.clientX-r.left,e.clientY-r.top); });

← 7444435 Add deploy config for playa.agentabrams.com (Kamatera + Clou  ·  back to Playa Ignition  ·  Add 3D spinning orb (Three.js, beat-reactive wireframe icosa 9a8fcad →