← Mike Higgins Home
Custom Code Experiment •

DNA 2

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>DNA 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 ---------- */
  var N=110, TURNS=2.6, R=0;
  var px,pz,qx,qz,py;
  function alloc(){
    px=new Float32Array(N); pz=new Float32Array(N);
    qx=new Float32Array(N); qz=new Float32Array(N);
    py=new Float32Array(N);
  }
  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;
    N = D<240 ? 84 : 110;
    R = D*0.215;
    alloc();
    buildNoise();
  }
  window.addEventListener('resize', resize);

  /* ---------- 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 state ---------- */
  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(amp>1.25 && Math.random()<0.5) g=Math.max(g, calm?0.25:0.55);
    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;
  }

  /* ---------- geometry ---------- */
  function computeGeo(t, amp){
    var top=-D*0.10, bot=D*1.10, span=bot-top;
    for(var i=0;i<N;i++){
      var u=i/(N-1);
      var y=top+span*u;
      /* taper toward the top and bottom so the strand sits inside the disc */
      var v=(y-CY)/(D*0.5);
      var fit=Math.sqrt(Math.max(0,1-Math.min(1,v*v)));
      var ph=u*TURNS*TAU + t*(calm?0.6:1.25);
      var rr=R*(0.80+0.20*Math.sin(u*5.1-t*1.9))*(0.88+0.26*amp)*(0.34+0.66*fit);
      var jitter=Math.sin(u*23.7+t*7)*4.5*S*amp*(0.4+g*2.2);
      px[i]=CX+Math.sin(ph)*rr+jitter;
      pz[i]=Math.cos(ph);
      qx[i]=CX+Math.sin(ph+Math.PI)*rr-jitter;
      qz[i]=Math.cos(ph+Math.PI);
      py[i]=y;
    }
  }

  /* ---------- drawing ---------- */
  function strandPath(xs, dx){
    ctx.beginPath();
    ctx.moveTo(xs[0]+dx, py[0]);
    for(var i=1;i<N;i++){
      var mx=(xs[i-1]+xs[i])*0.5+dx, my=(py[i-1]+py[i])*0.5;
      ctx.quadraticCurveTo(xs[i-1]+dx, py[i-1], mx, my);
    }
    ctx.lineTo(xs[N-1]+dx, py[N-1]);
  }

  function ghostPass(dx, a, hue){
    ctx.globalCompositeOperation='lighter';
    ctx.strokeStyle=hsl(hue, flare*92, 70+flare*6);
    ctx.globalAlpha=a*(1+flare*1.1); ctx.lineWidth=1.4*S;
    strandPath(px, dx); ctx.stroke();
    strandPath(qx, dx); ctx.stroke();
    ctx.lineWidth=S;
    ctx.beginPath();
    for(var i=0;i<N;i+=3){
      ctx.moveTo(px[i]+dx, py[i]);
      ctx.lineTo(qx[i]+dx, py[i]);
    }
    ctx.stroke();
    ctx.globalAlpha=1;
  }

  function mainPass(t){
    ctx.globalCompositeOperation='lighter';

    if(flare>0.02){
      ctx.lineWidth=15*S; ctx.globalAlpha=flare*0.11;
      ctx.strokeStyle=hsl(flareHue,95,56); strandPath(px,0); ctx.stroke();
      ctx.strokeStyle=hsl(flareHue+150,95,56); strandPath(qx,0); ctx.stroke();
    }

    ctx.lineWidth=6*S; ctx.globalAlpha=0.07+flare*0.14;
    ctx.strokeStyle=hsl(flareHue, flare*90, 62+flare*10);
    strandPath(px,0); ctx.stroke();
    strandPath(qx,0); ctx.stroke();

    for(var i=1;i<N;i++){
      var za=(pz[i]+1)*0.5, zb=(qz[i]+1)*0.5;

      ctx.globalAlpha=0.16+za*0.78;
      ctx.lineWidth=(0.7+za*1.9)*S;
      ctx.strokeStyle=hsl(flareHue, flare*82, 92);
      ctx.beginPath(); ctx.moveTo(px[i-1],py[i-1]); ctx.lineTo(px[i],py[i]); ctx.stroke();

      ctx.globalAlpha=(0.16+zb*0.78)*0.72;
      ctx.lineWidth=(0.7+zb*1.9)*S;
      ctx.strokeStyle=hsl(flareHue+150, flare*82, 84);
      ctx.beginPath(); ctx.moveTo(qx[i-1],py[i-1]); ctx.lineTo(qx[i],py[i]); ctx.stroke();
    }

    for(var j=0;j<N;j+=3){
      var zz=((pz[j]+qz[j])*0.5+1)*0.5;
      var x1=px[j], x2=qx[j], y=py[j], w=x2-x1;
      var broken=((j*37+((t*7)|0)*13)%97)/97 < g*0.55;
      var ticks=8;
      var base=(4+bands[(j*3)%NB]*18)*S*(0.45+zz*0.9);

      ctx.globalAlpha=0.14+zz*0.6+flare*0.12;
      ctx.lineWidth=(0.9+zz)*S;
      ctx.strokeStyle= (j%9===0) ? hsl(flareHue+150, flare*95, 78) : hsl(flareHue, flare*88, 92);
      ctx.beginPath();
      for(var k=0;k<=ticks;k++){
        if(broken && (k%3===1)) continue;
        var f=k/ticks;
        var xx=x1+w*f;
        var s=wave[((k*29+j*7+((t*60)|0))%WV+WV)%WV];
        var hgt=base*(0.25+Math.abs(Math.sin(f*Math.PI)))*(0.5+Math.abs(s));
        ctx.moveTo(xx, y-hgt*0.5);
        ctx.lineTo(xx, y+hgt*0.5);
      }
      ctx.stroke();

      ctx.globalAlpha=(0.08+zz*0.26)*(broken?0.25:1);
      ctx.lineWidth=0.6*S;
      ctx.beginPath(); ctx.moveTo(x1,y); ctx.lineTo(x2,y); ctx.stroke();

      var ns=(1.2+zz*2.4)*S;
      ctx.globalAlpha=0.3+zz*0.6;
      ctx.fillStyle=hsl(flareHue, flare*70, 96);
      ctx.fillRect(px[j]-ns*0.5, y-ns*0.5, ns, ns);
      ctx.globalAlpha=(0.3+zz*0.6)*0.7;
      ctx.fillStyle=hsl(flareHue+150, flare*70, 92);
      ctx.fillRect(qx[j]-ns*0.5, y-ns*0.5, ns, ns);
    }
    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.11)');
      bgGrad.addColorStop(0.6,'rgba(255,255,255,0.035)');
      bgGrad.addColorStop(1,'rgba(255,255,255,0)');
      bgKey=String(D);
    }
    ctx.globalAlpha=0.6+amp*0.15;
    ctx.fillStyle=bgGrad; ctx.fillRect(0,0,D,D);

    if(flare>0.02){
      var fg=ctx.createRadialGradient(CX,CY,0,CX,CY,D*0.5);
      fg.addColorStop(0, hsl(flareHue,90,50));
      fg.addColorStop(1, hsl(flareHue,90,50));
      ctx.globalCompositeOperation='lighter';
      ctx.globalAlpha=flare*0.07;
      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.24+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);

    /* fade the content out toward the rim of the disc */
    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 ---------- */
  function frame(now){
    if(measure()!==D) resize();

    var t=(now-t0)/1000;
    var amp=envelope(t);
    updateSignal(t, amp);
    updateGlitch(t, amp);
    updateFlare(t);
    computeGeo(t, amp);

    ctx.setTransform(DPR,0,0,DPR,0,0);
    backdrop(amp);

    var split=(1.2+g*10+amp*1.6+flare*4)*S;
    ghostPass(-split, 0.22, flareHue);
    ghostPass( split, 0.22, flareHue+150);
    mainPass(t);

    glitchPost();
    if(t<dropUntil) signalDrop();
    overlays();

    requestAnimationFrame(frame);
  }

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