Tip: Try clicking Pause and listen to how the audio shifts when the visuals stop.
// Deep Space Ripple - p5.js Sketch

let dotSize=3, angleStep=0.02, radiusStep=5, rows=7;
let gradientMaxAlpha=120, waveSpeed=2.0;
let rippleAmp=20, rippleFreq=0.02, rippleSpeed=0.05;
let gradRippleAmp=15, gradRippleFreq=0.05, gradSegments=200;
let minScale=0.8;
let topColor, bottomColor;
let gradOffset=10, goingUp=true;
let lowDrone, highWhine, glitchNoise, masterGain;
let glitchActive=false, lastGlitchTime=0, nextGlitchInterval=0, glitchDuration=200;
let muted=true, paused=false, audioStarted=false;

function setup(){
  createCanvas(windowWidth, windowHeight);
  frameRate(120);
  noStroke();
  topColor=color(0,0,255);
  bottomColor=color(255,80,40);
  lastGlitchTime=millis();
  nextGlitchInterval=random(5000,10000);
}

function draw(){
  background(0);
  let cx=width/2, cy=height, availH=height-100;
  gradOffset += goingUp?waveSpeed:-waveSpeed;
  if(gradOffset>=availH){gradOffset=availH; goingUp=false;} else if(gradOffset<=0){gradOffset=0; goingUp=true;}
  let scaleF=map(gradOffset,0,availH,1.0,minScale);
  push(); translate(0, height-height*scaleF); scale(scaleF);
  let rp=frameCount*rippleSpeed, rx=sin(rp)*rippleAmp;
  let gcx=cx+rx, gcy=cy-gradOffset;
  for(let gr=availH; gr>=0; gr-=10){
    let rawA=gr<=availH*0.1?255:map(gr,availH*0.1,availH,255,0);
    fill(red(bottomColor), green(bottomColor), blue(bottomColor), rawA*(gradientMaxAlpha/255));
    beginShape();
    for(let i=0;i<=gradSegments;i++){
      let a=map(i,0,gradSegments,0,TWO_PI);
      let rd=gr+sin(a*gradRippleFreq+rp)*gradRippleAmp;
      vertex(gcx+cos(a)*rd, gcy+sin(a)*rd);
    }
    endShape(CLOSE);
  }
  for(let row=rows-1; row>=0; row--){
    let rMax=(availH/rows)*(row+1);
    for(let r=0; r<=rMax; r+=radiusStep){
      fill(lerpColor(topColor,bottomColor,map(r,0,rMax,0,1)));
      let sA=row===0?0:PI, eA=row===0?TWO_PI:0, stA=row===0?angleStep:-angleStep;
      for(let a=sA; stA>0?a<=eA:a>=eA; a+=stA){
        ellipse(cx+cos(a)*r+sin((cy-sin(a)*r)*rippleFreq+rp)*rippleAmp,
                cy-sin(a)*r, dotSize, dotSize);
      }
    }
  }
  pop();
  if(!muted && audioStarted){
    lowDrone.freq(map(gradOffset,0,availH,55,75));
    highWhine.freq(map(abs(sin(rp)*rippleAmp),0,rippleAmp,600,900));
    let now=millis();
    if(glitchActive){
      if(now-lastGlitchTime>=glitchDuration){
        glitchNoise.amp(0);
        glitchActive=false;
        lastGlitchTime=now;
        nextGlitchInterval=random(5000,10000);
      }
    } else if(now-lastGlitchTime>=nextGlitchInterval){
      glitchNoise.amp(random(0.1,0.4));
      glitchActive=true;
      lastGlitchTime=now;
    }
  }
}

function toggleSound(){
  if (/iPhone|iPad|iPod/.test(navigator.userAgent) && muted && !audioStarted) {
    alert("Heads up: If you're on iPhone, make sure your Silent switch is OFF to hear audio.");
  }
  console.log('🔊 toggleSound clicked – muted:', muted, 'audioStarted:', audioStarted);
  if(typeof userStartAudio==='function'){
    userStartAudio().then(initAudio).catch(err=>console.warn('userStartAudio failed',err));
  } else initAudio();
}

function initAudio(){
  if(!audioStarted){
    lowDrone=new p5.Oscillator('sine'); lowDrone.freq(60); lowDrone.amp(0.3); lowDrone.start();
    highWhine=new p5.Oscillator('sine'); highWhine.freq(600); highWhine.amp(0.05); highWhine.start();
    glitchNoise=new p5.Noise('white'); glitchNoise.amp(0); glitchNoise.start();
    masterGain=new p5.Gain(); lowDrone.disconnect(); highWhine.disconnect(); glitchNoise.disconnect();
    lowDrone.connect(masterGain); highWhine.connect(masterGain); glitchNoise.connect(masterGain);
    masterGain.connect(); masterGain.amp(0);
    audioStarted=true;
  }
  if(muted){ masterGain.amp(1,0.5); document.getElementById('sound-btn').textContent='🔊'; }
  else{ masterGain.amp(0,0.5); document.getElementById('sound-btn').textContent='🔇'; }
  muted=!muted;
}

function togglePause(){
  if(paused){ loop(); document.getElementById('pause-btn').textContent='Pause'; }
  else{ noLoop(); document.getElementById('pause-btn').textContent='Resume'; }
  paused=!paused;
}

function windowResized(){ resizeCanvas(windowWidth,windowHeight); }
function touchStarted(){ getAudioContext&&getAudioContext().resume(); }