← Mike Higgins Home
Custom Code Experiment •

Lightening

Source Code View HTML / CSS / JavaScript
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Arc Signal Loader</title>
<style>
  :root{ --bg:#050505; --ring:rgba(255,255,255,0.08); }
  :root:not([data-theme="light"]){ --bg:#050505; }
  html,body{height:100%;margin:0;background:var(--bg);overflow:hidden;}
  body{display:flex;align-items:center;justify-content:center;}
  #c{display:block;border-radius:50%;overflow:hidden;box-shadow:0 0 0 1px var(--ring);}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
(function(){
  "use strict";
  var TAU=Math.PI*2;
  var cvs=document.getElementById('c');
  var ctx=cvs.getContext('2d',{alpha:false});
  var D=0,DPR=1,CX=0,CY=0,S=1;
  var calm=window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  var t0=0;

  /* ---------- patterns ---------- */
  var noiseTiles=[],scanPat=null;
  function buildNoise(){
    noiseTiles=[];
    for(var n=0;n<4;n++){
      var s=140,nc=document.createElement('canvas'); nc.width=nc.height=s;
      var nx=nc.getContext('2d'),id=nx.createImageData(s,s),d=id.data;
      for(var i=0;i<d.length;i+=4){
        var v=Math.random()*255;
        d[i]=v; d[i+1]=v; d[i+2]=v; d[i+3]=Math.random()*Math.random()*85;
      }
      nx.putImageData(id,0,0);
      noiseTiles.push(ctx.createPattern(nc,'repeat'));
    }
    var sc=document.createElement('canvas'); sc.width=1; sc.height=3;
    var sx=sc.getContext('2d');
    sx.fillStyle='rgba(0,0,0,0.5)'; sx.fillRect(0,0,1,1);
    scanPat=ctx.createPattern(sc,'repeat');
  }

  /* ---------- sizing ---------- */
  function measure(){
    var vw=window.innerWidth||800, vh=window.innerHeight||600;
    return Math.round(Math.max(150, Math.min(Math.min(vw,vh)*0.46, 420)));
  }
  function resize(){
    D=measure();
    DPR=Math.min(2, window.devicePixelRatio||1);
    cvs.style.width=D+'px'; cvs.style.height=D+'px';
    cvs.width=Math.round(D*DPR); cvs.height=Math.round(D*DPR);
    CX=D*0.5; CY=D*0.5; S=D/360;
    buildNoise();
  }
  window.addEventListener('resize', resize);

  /* how much horizontal room the disc allows at a given height */
  function fitAt(y){
    var v=(y-CY)/(D*0.5);
    return Math.sqrt(Math.max(0,1-Math.min(1,v*v)));
  }

  /* ---------- signal ---------- */
  var NB=56,bands=new Float32Array(NB),WV=256,wave=new Float32Array(WV);
  function envelope(t){
    var b=t*(calm?68:112)/60;
    var kick=Math.exp(-(b%1)*6);
    var snare=Math.exp(-((b+0.5)%2)*5)*0.5;
    var hat=Math.exp(-((b*2)%1)*14)*0.22;
    return Math.min(1.7, kick*0.85+snare+hat+0.14);
  }
  function updateSignal(t, amp){
    for(var k=0;k<NB;k++){
      var f=k/NB;
      var v=(Math.sin(t*(1.7+k*0.41)+k*2.1)*0.5+0.5);
      v*=Math.pow(1-f,1.15);
      v=v*0.65+Math.random()*0.12;
      v*=0.35+amp*0.8;
      bands[k]=bands[k]*0.74+v*0.26;
    }
    for(var i=0;i<WV;i++){
      var u=i/(WV-1),s=0;
      s+=Math.sin(u*TAU*3+t*4.6)*0.52;
      s+=Math.sin(u*TAU*7.3-t*3.1)*0.27;
      s+=Math.sin(u*TAU*17.7+t*9.4)*0.13;
      s+=(Math.random()-0.5)*0.18;
      wave[i]=s*amp;
    }
  }

  /* ---------- glitch ---------- */
  var g=0,gUntil=0,dropUntil=0,nextDrop=4;
  function updateGlitch(t, amp){
    if(t>gUntil && Math.random()<(calm?0.006:0.03)){
      gUntil=t+0.05+Math.random()*(calm?0.14:0.38);
      g=Math.max(g,(calm?0.25:0.35)+Math.random()*(calm?0.3:0.65));
    }
    if(t>gUntil) g*=0.86;
    if(g<0.004) g=0;
    if(!calm && t>nextDrop){
      dropUntil=t+0.05+Math.random()*0.16;
      nextDrop=t+5+Math.random()*7;
    }
  }

  /* ---------- colour flares ---------- */
  var FLARE_HUES=[188,312,96,36];
  var flare=0,flareHue=188,flareUntil=0,nextFlare=1.6;
  function hsl(h,s,l){ return 'hsl('+((h%360+360)%360)+','+Math.round(s)+'%,'+Math.round(l)+'%)'; }
  function updateFlare(t){
    if(t>nextFlare){
      flareHue=FLARE_HUES[(Math.random()*FLARE_HUES.length)|0];
      flare=Math.max(flare,0.6+Math.random()*0.4);
      flareUntil=t+0.16+Math.random()*(calm?0.3:0.55);
      nextFlare=t+(calm?4:2.2)+Math.random()*(calm?6:4.5);
    }
    if(g>0.6 && flare<0.25) flare=0.25+Math.random()*0.3;
    if(t>flareUntil) flare*=0.90;
    if(flare<0.005) flare=0;
  }

  /* ---------- bolt generation ---------- */
  function subdivide(pts, disp, iters){
    for(var it=0; it<iters; it++){
      var np=[];
      for(var i=0;i<pts.length-1;i++){
        var a=pts[i], b=pts[i+1];
        var mx=(a.x+b.x)*0.5, my=(a.y+b.y)*0.5;
        var f=0.25+0.75*fitAt(my);
        mx+=(Math.random()-0.5)*disp*f;
        my+=(Math.random()-0.5)*disp*0.22;
        np.push(a,{x:mx,y:my});
      }
      np.push(pts[pts.length-1]);
      pts=np;
      disp*=0.55;
    }
    return pts;
  }

  function makeBranch(pts, at, dir, len){
    var p=pts[at];
    var ang=(-Math.PI*0.5)+dir*(0.5+Math.random()*0.75);
    var end={ x:p.x+Math.cos(ang)*len*(Math.random()<0.5?-1:1),
              y:p.y+Math.abs(Math.sin(ang))*len };
    return subdivide([{x:p.x,y:p.y},end], len*0.42, 4);
  }

  var bolts=[], sparks=[], flash=0, nextStrike=0, prevAmp=0;

  function strike(t, power){
    var startX=CX+(Math.random()-0.5)*D*0.34;
    var endX=CX+(Math.random()-0.5)*D*0.5;
    var pts=subdivide([{x:startX,y:-D*0.08},{x:endX,y:D*1.08}], D*0.28, 7);

    var branches=[], nb=1+((Math.random()*3+power*2)|0);
    for(var i=0;i<nb;i++){
      var at=(0.15+Math.random()*0.65)*(pts.length-1)|0;
      branches.push(makeBranch(pts, at, Math.random()<0.5?-1:1, D*(0.12+Math.random()*0.26)));
    }

    bolts.push({ pts:pts, branches:branches, born:t,
                 dur:0.26+Math.random()*0.42, seed:Math.random()*10,
                 w:(1.0+power*1.1)*S });
    if(bolts.length>7) bolts.shift();

    flash=Math.max(flash, 0.35+power*0.45);
    g=Math.max(g, 0.2+power*0.4);
    if(Math.random()<0.28){
      flare=Math.max(flare,0.45+Math.random()*0.4);
      flareUntil=t+0.2+Math.random()*0.3;
      flareHue=FLARE_HUES[(Math.random()*FLARE_HUES.length)|0];
    }

    /* sparks off the branch tips */
    for(var b=0;b<branches.length;b++){
      var tip=branches[b][branches[b].length-1];
      var n=2+((Math.random()*4)|0);
      for(var s2=0;s2<n;s2++){
        sparks.push({ x:tip.x, y:tip.y,
                      vx:(Math.random()-0.5)*90*S, vy:(Math.random()-0.2)*90*S,
                      life:0.18+Math.random()*0.35, age:0 });
      }
    }
    if(sparks.length>90) sparks.splice(0, sparks.length-90);
  }

  function updateStrikes(t, dt, amp){
    /* strikes land on the beat, with extra ones in between */
    if(amp>1.15 && prevAmp<=1.15) strike(t, Math.min(1,(amp-1.05)/0.6));
    prevAmp=amp;
    if(t>nextStrike){
      strike(t, Math.random()*0.6);
      nextStrike=t+(calm?0.6:0.22)+Math.random()*(calm?1.2:0.75);
    }
    for(var i=sparks.length-1;i>=0;i--){
      var s=sparks[i];
      s.age+=dt; s.x+=s.vx*dt; s.y+=s.vy*dt; s.vy+=40*S*dt;
      if(s.age>=s.life) sparks.splice(i,1);
    }
    flash*=0.84;
    if(flash<0.004) flash=0;
  }

  /* ---------- drawing ---------- */
  function tracePath(pts){
    ctx.beginPath();
    ctx.moveTo(pts[0].x, pts[0].y);
    for(var i=1;i<pts.length;i++) ctx.lineTo(pts[i].x, pts[i].y);
  }

  function strokeBolt(pts, I, w){
    tracePath(pts);
    ctx.lineJoin='round'; ctx.lineCap='round';
    ctx.strokeStyle=hsl(flareHue, flare*95, 54);
    ctx.globalAlpha=I*0.15; ctx.lineWidth=w*8; ctx.stroke();
    ctx.strokeStyle=hsl(flareHue, flare*88, 70);
    ctx.globalAlpha=I*0.30; ctx.lineWidth=w*3.2; ctx.stroke();
    ctx.strokeStyle=hsl(flareHue, flare*35, 97);
    ctx.globalAlpha=I*0.95; ctx.lineWidth=w; ctx.stroke();
  }

  /* the ionised channel that never fully goes out */
  function coreChannel(t, amp){
    var M=44;
    ctx.globalCompositeOperation='lighter';
    ctx.beginPath();
    for(var i=0;i<=M;i++){
      var u=i/M;
      var y=-D*0.08+D*1.16*u;
      var f=0.2+0.8*fitAt(y);
      var x=CX+(Math.sin(u*7.1+t*1.6)*0.5+Math.sin(u*17.3-t*2.3)*0.3
              +wave[(i*6)%WV]*0.3)*D*0.17*f;
      if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
    }
    ctx.lineJoin='round'; ctx.lineCap='round';
    ctx.strokeStyle=hsl(flareHue, flare*90, 52);
    ctx.globalAlpha=0.10+amp*0.05; ctx.lineWidth=7*S; ctx.stroke();
    ctx.strokeStyle=hsl(flareHue, flare*40, 92);
    ctx.globalAlpha=0.16+amp*0.10; ctx.lineWidth=1.1*S; ctx.stroke();
    ctx.globalAlpha=1;
  }

  function drawBolts(t){
    ctx.globalCompositeOperation='lighter';
    for(var i=bolts.length-1;i>=0;i--){
      var b=bolts[i];
      var age=t-b.born, k=age/b.dur;
      if(k>=1){ bolts.splice(i,1); continue; }
      var env=Math.pow(1-k,2.3);
      var flick=0.62+0.38*Math.sin(age*70+b.seed);
      var I=env*flick;
      if(I<=0.01) continue;
      strokeBolt(b.pts, I, b.w);
      for(var j=0;j<b.branches.length;j++) strokeBolt(b.branches[j], I*0.55, b.w*0.55);
    }
    ctx.globalAlpha=1;
  }

  function drawSparks(){
    ctx.globalCompositeOperation='lighter';
    for(var i=0;i<sparks.length;i++){
      var s=sparks[i], k=1-s.age/s.life;
      var sz=(0.9+k*1.8)*S;
      ctx.globalAlpha=k*0.85;
      ctx.fillStyle=hsl(flareHue, flare*70, 95);
      ctx.fillRect(s.x-sz*0.5, s.y-sz*0.5, sz, sz);
    }
    ctx.globalAlpha=1;
  }

  /* ---------- background & overlays ---------- */
  var bgGrad=null,bgKey='';
  function backdrop(amp){
    ctx.globalCompositeOperation='source-over';
    ctx.fillStyle='#050505'; ctx.fillRect(0,0,D,D);

    if(bgKey!==String(D)){
      bgGrad=ctx.createRadialGradient(CX,CY,0,CX,CY,D*0.5);
      bgGrad.addColorStop(0,'rgba(255,255,255,0.10)');
      bgGrad.addColorStop(0.6,'rgba(255,255,255,0.03)');
      bgGrad.addColorStop(1,'rgba(255,255,255,0)');
      bgKey=String(D);
    }
    ctx.globalAlpha=0.55+amp*0.12;
    ctx.fillStyle=bgGrad; ctx.fillRect(0,0,D,D);

    if(flash>0.02 || flare>0.02){
      var fg=ctx.createRadialGradient(CX,CY,0,CX,CY,D*0.55);
      fg.addColorStop(0, hsl(flareHue, flare*90, 62));
      fg.addColorStop(1, hsl(flareHue, flare*90, 30));
      ctx.globalCompositeOperation='lighter';
      ctx.globalAlpha=flash*0.16+flare*0.05;
      ctx.fillStyle=fg; ctx.fillRect(0,0,D,D);
      ctx.globalCompositeOperation='source-over';
    }
    ctx.globalAlpha=1;
  }

  function glitchPost(){
    if(g<=0) return;
    var cw=cvs.width,ch=cvs.height;
    ctx.save(); ctx.setTransform(1,0,0,1,0,0);
    ctx.globalCompositeOperation='source-over';
    var n=1+((g*10)|0);
    for(var i=0;i<n;i++){
      var sy=Math.random()*ch;
      var sh=(2+Math.random()*26)*S*DPR;
      var dx=(Math.random()-0.5)*g*70*S*DPR;
      ctx.drawImage(cvs,0,sy,cw,sh,dx,sy,cw,sh);
    }
    var b=((g*6)|0);
    for(var j=0;j<b;j++){
      var bw=(20+Math.random()*90)*S*DPR, bh=(5+Math.random()*34)*S*DPR;
      var sx=Math.random()*Math.max(1,cw-bw), sy2=Math.random()*Math.max(1,ch-bh);
      var ox=(Math.random()-0.5)*g*50*S*DPR, oy=(Math.random()-0.5)*g*16*S*DPR;
      ctx.drawImage(cvs,sx,sy2,bw,bh,sx+ox,sy2+oy,bw,bh);
    }
    if(g>0.45){
      var sft=g*5*S*DPR;
      ctx.globalCompositeOperation='lighter';
      ctx.globalAlpha=0.18;
      ctx.drawImage(cvs,-sft,0);
      ctx.drawImage(cvs,sft,0);
      ctx.globalAlpha=1;
    }
    ctx.restore();
  }

  function signalDrop(){
    ctx.save(); ctx.setTransform(1,0,0,1,0,0);
    var cw=cvs.width,ch=cvs.height;
    ctx.globalCompositeOperation='source-over';
    ctx.fillStyle='rgba(5,5,5,0.72)'; ctx.fillRect(0,0,cw,ch);
    for(var i=0;i<46;i++){
      var y=Math.random()*ch,h=(1+Math.random()*7)*S*DPR;
      ctx.globalAlpha=0.08+Math.random()*0.38;
      ctx.fillStyle=Math.random()<0.5?'#2a2a2a':'#1a1a1a';
      ctx.fillRect(0,y,cw,h);
    }
    ctx.globalAlpha=1; ctx.restore();
  }

  var vgGrad=null,vgKey='';
  function overlays(){
    ctx.save(); ctx.setTransform(1,0,0,1,0,0);
    var cw=cvs.width,ch=cvs.height;

    ctx.globalCompositeOperation='screen';
    ctx.globalAlpha=0.22+g*0.3;
    ctx.fillStyle=noiseTiles[(Math.random()*noiseTiles.length)|0];
    ctx.save();
    ctx.translate((Math.random()*140)|0,(Math.random()*140)|0);
    ctx.fillRect(-160,-160,cw+320,ch+320);
    ctx.restore();

    ctx.globalCompositeOperation='multiply';
    ctx.globalAlpha=0.3;
    ctx.fillStyle=scanPat; ctx.fillRect(0,0,cw,ch);

    ctx.globalCompositeOperation='source-over';
    ctx.globalAlpha=1;
    if(vgKey!==cw+'x'+ch){
      vgGrad=ctx.createRadialGradient(cw*0.5,ch*0.5,cw*0.20,cw*0.5,ch*0.5,cw*0.5);
      vgGrad.addColorStop(0,'rgba(5,5,5,0)');
      vgGrad.addColorStop(0.78,'rgba(5,5,5,0.55)');
      vgGrad.addColorStop(1,'rgba(5,5,5,0.94)');
      vgKey=cw+'x'+ch;
    }
    ctx.fillStyle=vgGrad; ctx.fillRect(0,0,cw,ch);
    ctx.restore();
  }

  /* ---------- loop ---------- */
  var last=0;
  function frame(now){
    if(measure()!==D) resize();
    var t=(now-t0)/1000;
    var dt=Math.min(0.05,(now-last)/1000||0.016); last=now;

    var amp=envelope(t);
    updateSignal(t, amp);
    updateGlitch(t, amp);
    updateFlare(t);
    updateStrikes(t, dt, amp);

    ctx.setTransform(DPR,0,0,DPR,0,0);
    backdrop(amp);
    coreChannel(t, amp);
    drawBolts(t);
    drawSparks();
    glitchPost();
    if(t<dropUntil) signalDrop();
    overlays();

    requestAnimationFrame(frame);
  }

  resize();
  requestAnimationFrame(function(n){ t0=n; last=n; frame(n); });
})();
</script>
</body>
</html>