this logo: 245 b

← ASHLAR v1

index.html

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>ASHLAR</title>
<meta name="build" content="r3-splash">
<style>
html,body{margin:0;height:100%;background:#000;overflow:hidden}
canvas{position:fixed;inset:0;width:100%;height:100%;display:block}
#h{position:fixed;left:0;right:0;bottom:max(18px,env(safe-area-inset-bottom));text-align:center;font:11px/1 ui-monospace,Menlo,monospace;letter-spacing:.35em;color:#e9c98a;opacity:0;transition:opacity 1s;pointer-events:none;text-transform:uppercase}
#s{position:fixed;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:7vh;background:radial-gradient(ellipse at 50% 58%,#1a1407 0,#03050a 45%,#000 75%);font-family:ui-monospace,Menlo,Consolas,monospace;transition:opacity .25s}
#s h1{margin:0;font-weight:400;font-size:clamp(30px,10vw,104px);letter-spacing:.42em;padding-left:.42em;color:#b9cdea;text-shadow:0 0 28px #3d5f9a}
#go{font:inherit;font-size:clamp(14px,2.2vw,20px);letter-spacing:.5em;padding:1.05em 2.2em 1.05em 2.7em;color:#ffd58a;background:#0000;border:1px solid #e9c98a;border-radius:2px;cursor:pointer;box-shadow:0 0 26px #e9c98a55,inset 0 0 18px #e9c98a22;-webkit-tap-highlight-color:transparent}
#go:hover,#go:focus-visible{background:#e9c98a;color:#000;outline:0}
</style>
<canvas id="c"></canvas>
<div id="h">tap for sound</div>
<div id="s"><h1>ASHLAR</h1><button id="go" autofocus>START</button></div>
<script>
"use strict";
/*SYNTH*/
// The song: pure functions of bar and step. The synth and the visuals both read it.
function SONG(){
  const BPM=130,BEAT=60/BPM,BAR=BEAT*4,ST=BEAT/4,BARS=88;
  const sec=b=>b<8?0:b<24?1:b<40?2:b<56?3:b<72?4:5;
  const kick=(b,s)=>((b>=8&&b<40)||(b>=52&&b<80))&&(s%4==0||(b==55&&s%2==0))&&!((b==23||b==39||b==71)&&s>=8);
  // acid notes: semitones above A1; +100 accent, +1000 slide into next; -1 rest
  const PA=[0,-1,0,112,-1,1000,103,-1,0,0,-1,110,0,-1,1001,112],
        PB=[0,12,1000,100,115,0,12,-1,0,1013,100,7,0,112,1,1000],
        PC=[12,0,1012,124,0,12,115,1012,0,124,13,1012,100,12,119,1022],
        PD=[0,0,112,0,1003,105,0,12,0,0,110,0,1008,107,0,113];
  const acid=(b,s)=>{const c=sec(b);
    if(c==0)return b<2?-1:(s%4==0||s==6||(b>5&&s==11))?PA[s]%100+(s==6?100:0):-1;
    if(c==1)return(b%4==3?PD:PA)[s];
    if(c==2)return(b%8==7?PC:b%2?PD:PB)[s];
    if(c==3)return b<48?(s%2==0||b>43?PB[s]:-1):PB[s];
    if(c==4)return(b%4==3?PD:b%2?PB:PC)[s];
    return b<84?PA[s]:-1};
  // filter opening 0..1 over the song
  const cut=x=>{const c=sec(x|0),m=Math.sin(x*Math.PI/4);
    return c==0?.08+.3*x/8:c==1?.42+.14*m:c==2?.58+.2*m:c==3?(x<48?.12+.5*(x-40)/8:.62+.38*(x-48)/8):c==4?.82+.16*m:Math.max(.05,.6-.55*(x-72)/12)};
  return{BPM,BEAT,BAR,ST,BARS,sec,kick,acid,cut,STARTS:[0,8,24,40,56,72,88]};
}
function SYNTH(){
  const T=SONG(),sr=sampleRate,TAU=Math.PI*2,{sin,exp,tanh,pow,min,max,floor,random}=Math;
  const TOTAL=Math.round(T.BARS*T.BAR*sr);
  const hz=n=>55*pow(2,n/12);
  const CH=[[12,15,19],[8,12,15],[12,15,19],[13,17,20]];// Am F Am Bb, semitones above A1
  class P extends AudioWorkletProcessor{
    constructor(){super();this.n=-1;this.go=0;this.port.onmessage=e=>{this.n=floor(e.data*sr)%TOTAL;this.go=0;this.ls=-1};
      const z=this;z.ls=-1;
      z.kt=9;z.kp=0;z.ht=9;z.hd=.03;z.hv=0;z.hl=0;z.ct=9;z.cd=18;z.cv=0;z.cb=0;z.cl=0;
      z.af=55;z.at=55;z.ap=0;z.fe=0;z.ae=0;z.ag=0;z.ac=0;z.gl=0;z.gs=0;z.y=[0,0,0,0];
      z.se=0;z.sp=[0,0,0];z.sf=[0,0,0];z.sl=0;z.pp=new Float64Array(12);z.pf=new Float64Array(12);z.pl=[0,0,0,0];z.pa=0;
      z.rv=[0,0];z.ut=9;z.xt=9;z.xl=0;z.rb=0;z.rl=0;z.bp=0;
      const k=sr/44100;z.dn=floor(T.ST*3*sr);z.dl=new Float32Array(z.dn);z.dr=new Float32Array(z.dn);z.di=0;
      z.cm=[1557,1617,1491,1422,1277,1356,1188,1116,1580,1640,1514,1445,1300,1379,1211,1139].map(x=>({b:new Float32Array(floor(x*k)),i:0,l:0}));
      z.al=[225,556,441,341,248,579,464,364].map(x=>({b:new Float32Array(floor(x*k)),i:0}));
    }
    trig(st){const z=this,b=floor(st/16)%T.BARS,s=st%16,c=T.sec(b);
      if(T.kick(b,s)){z.kt=0;z.kp=0}
      // hats
      if(c==0?(b>=4&&s%4==2):c==3?(b<48?s%4==2:1):c==5?(b<80||s%4==2)&&b<86:1){
        const op=s%4==2&&c>0;z.ht=0;z.hd=op?.11:.022+.01*(s%2);z.hv=op?.8:c<2&&s%2?.25:s%4==0?.35:.55;if(c==0)z.hv=.3}
      // clap, and the roll in the build
      if((s==4||s==12)&&((b>=16&&b<40)||(b>=56&&b<80))){z.ct=0;z.cd=16;z.cv=1}
      if(b>=48&&b<56&&(b<52?s%4==0:b<54?s%2==0:1)){z.ct=0;z.cd=30;z.cv=.35+.5*(b-48+s/16)/8}
      // acid
      const a=T.acid(b,s);
      if(a>=0){const n=a%100;z.at=hz(n);if(!z.gs||!z.ag){z.fe=1;z.af=z.at}z.ac=(a/100|0)%10?1:0;z.ag=1;z.gl=z.gs;z.gs=a>=1000;z.go2=floor(T.ST*sr*(z.gs?1.1:.55))}
      else if(!z.gs)z.ag=0;
      // stab
      if((c==2||c==4)&&(s==3||s==10||(b%4==3&&s==14))){z.se=1;const h=CH[(b>>2)%4];for(let i=0;i<3;i++)z.sf[i]=hz(h[i]+12)}
      // sub
      if(c>0&&c!=3&&b<80&&s%4==2)z.ut=0; if(c==3&&b>=52&&s%4==2)z.ut=0;
      // impact on each scene start
      if(s==0&&(T.STARTS.includes(b)&&b>0))z.xt=0;
    }
    process(_,o){const z=this,L=o[0][0],R=o[0][1]||L;if(z.n<0)return true;
      if(!z.go){z.go=1;z.port.postMessage(currentTime)}
      const y=z.y,cm=z.cm,al=z.al;
      for(let i=0;i<L.length;i++){
        const n=z.n,t=n/sr,stf=t/T.ST,st=floor(stf),bf=t/T.BAR,b=floor(bf),c=T.sec(b);
        if(st!=z.ls){z.ls=st;z.trig(st)}
        // kick
        z.kt+=1/sr;let k=0;if(z.kt<.5){z.kp+=TAU*(44+190*exp(-z.kt*32))/sr;k=tanh((sin(z.kp)*exp(-z.kt*6.5)+(z.kt<.004?.6*(random()-.5):0))*2.4)}
        const duck=1-.65*exp(-z.kt*9);
        // sub bass
        z.ut+=1/sr;z.bp+=TAU*55/sr;const sub=z.ut<.3?sin(z.bp)*min(1,z.ut*400)*exp(-z.ut*9):0;
        // hat
        z.ht+=1/sr;const nz=random()*2-1;z.hl+=.72*(nz-z.hl);const hat=(nz-z.hl)*exp(-z.ht/z.hd)*z.hv;
        // clap
        z.ct+=1/sr;const ce=z.ct<.03?.9*(1-(z.ct%.01)*70):exp(-(z.ct-.03)*z.cd);
        z.cl+=.22*(nz-z.cb-z.cl*.9);z.cb+=.22*z.cl;const clap=z.cl*ce*z.cv*1.6;
        // acid
        z.af+=(z.at-z.af)*(z.gl?.0012*44100/sr:1);
        if(z.ag&&--z.go2<0&&!z.gs)z.ag=0;
        z.ae+=((z.ag?1:0)-z.ae)*(z.ag?.02:.004);
        z.fe*=exp(-1/(sr*(z.ac?.09:.2)));
        z.ap+=z.af/sr;if(z.ap>=1)z.ap-=1;const dt=z.af/sr;let saw=2*z.ap-1;
        if(z.ap<dt){const q=z.ap/dt;saw-=q+q-q*q-1}else if(z.ap>1-dt){const q=(z.ap-1)/dt;saw-=q*q+q+q+1}
        const cu=T.cut(bf),fc=min(sr*.42,70*pow(2,cu*5.2+z.fe*(1.6+cu*1.4+z.ac*1.1))),g=1-exp(-TAU*fc/(sr*2)),res=(c==3?3.75:3.45)+z.ac*.15;
        for(let q=0;q<2;q++){const x=tanh(saw*.9-res*y[3]);y[0]+=g*(x-y[0]);y[1]+=g*(y[0]-y[1]);y[2]+=g*(y[1]-y[2]);y[3]+=g*(y[2]-y[3])}
        const acid=tanh(y[3]*z.ae*(2.2+z.ac*1.6)*(1+cu))*.55;
        // stab
        z.se*=exp(-1/(sr*.11));let sb=0;for(let j=0;j<3;j++){z.sp[j]+=z.sf[j]/sr;if(z.sp[j]>=1)z.sp[j]-=1;sb+=2*z.sp[j]-1}
        z.sl+=(1-exp(-TAU*(250+5000*z.se*z.se)/sr))*(sb*z.se-z.sl);const stab=z.sl*.3;
        // pad
        const h=CH[(b>>2)%4],pg=c==0?min(1,bf/2):c==3?(bf<48?1:.7):c==5?max(0,min(1,(87-bf)/6)):.28;z.pa+=(pg-z.pa)*.0002;
        let pL=0,pR=0;for(let j=0;j<12;j++){const f=hz(h[j%3]+(j>5?12:0))*(1+(j%2?.004:-.004)*(1+(j>>1)%3));z.pf[j]+=(f-z.pf[j])*.0005;z.pp[j]+=z.pf[j]/sr;if(z.pp[j]>=1)z.pp[j]-=1;const v=2*z.pp[j]-1;if(j%2)pL+=v;else pR+=v}
        const pc=1-exp(-TAU*(300+2600*cu)/sr),pl=z.pl;pl[0]+=pc*(pL-pl[0]);pl[1]+=pc*(pl[0]-pl[1]);pl[2]+=pc*(pR-pl[2]);pl[3]+=pc*(pl[2]-pl[3]);
        const padL=pl[1]*z.pa*.11,padR=pl[3]*z.pa*.11;
        // riser into each handover (the last two bars of a scene), impact after it
        let ris=0;const nb=T.STARTS[c+1],u=(bf-(nb-2))/2;
        if(u>0&&c<5){const f=1-exp(-TAU*(200+7000*u*u)/sr);z.rl+=f*(nz-z.rb-z.rl*.35);z.rb+=f*z.rl;ris=z.rl*u*u*.35}
        z.xt+=1/sr;z.xl+=.25*(nz-z.xl);const imp=z.xt<4?(nz-z.xl)*exp(-z.xt*1.6)*.45+sin(TAU*38*z.xt)*exp(-z.xt*2.2)*.7:0;
        // delay (dotted eighth, ping-pong)
        const ds=acid*.3+stab*.7+clap*.12,di=z.di,dL=z.dl[di],dR=z.dr[di];z.dl[di]=ds+dR*.48;z.dr[di]=dL*.48;z.di=(di+1)%z.dn;
        // reverb
        const rs=(padL+padR)*.5+clap*.5+stab*.5+acid*.1+imp*.5+ris*.8+(dL+dR)*.25+hat*.08;const rv=z.rv;
        for(let q=0;q<2;q++){let a=0;for(let j=0;j<8;j++){const m=cm[q*8+j],v=m.b[m.i];m.l+=.28*(v-m.l);m.b[m.i]=rs+m.l*.86;if(++m.i>=m.b.length)m.i=0;a+=v}
          a*=.12;for(let j=0;j<4;j++){const m=al[q*4+j],v=m.b[m.i],w=a+v*.5;m.b[m.i]=w;if(++m.i>=m.b.length)m.i=0;a=v-w*.5}rv[q]=a}
        const dry=k*.95+sub*.5+(acid*.9)*(.5+.5*duck)+clap*.34+imp;
        const fd=bf>86?max(0,(88-bf)/2):1,wet=duck*.6+.4;
        L[i]=tanh((dry+hat*.2+padL*duck+dL*.55+rv[0]*.5*wet+ris*.7+stab*.5)*.78)*.95*fd;
        R[i]=tanh((dry+hat*.16+padR*duck+dR*.55+rv[1]*.5*wet+ris*.6+stab*.42)*.78)*.95*fd;
        if(++z.n>=TOTAL)z.n=0;
      }
      return true}
  }
  registerProcessor("s",P);
}
/*END*/

// ---------- maths ----------
const {sin,cos,PI,min,max,abs,floor,exp,pow,sqrt,tan,hypot}=Math,TAU=PI*2;
const mix=(a,b,t)=>a+(b-a)*t,cl=(x,a=0,b=1)=>min(b,max(a,x)),ss=(a,b,x)=>{x=cl((x-a)/(b-a));return x*x*(3-2*x)};
const sub=(a,b)=>[a[0]-b[0],a[1]-b[1],a[2]-b[2]],add=(a,b)=>[a[0]+b[0],a[1]+b[1],a[2]+b[2]],mul3=(a,s)=>[a[0]*s,a[1]*s,a[2]*s];
const dot=(a,b)=>a[0]*b[0]+a[1]*b[1]+a[2]*b[2],cross=(a,b)=>[a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]];
const len=a=>hypot(a[0],a[1],a[2]),norm=a=>mul3(a,1/(len(a)||1)),mix3=(a,b,t)=>[mix(a[0],b[0],t),mix(a[1],b[1],t),mix(a[2],b[2],t)];
const I4=()=>[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1];
function mm(a,b){const o=new Array(16);for(let c=0;c<4;c++)for(let r=0;r<4;r++)o[c*4+r]=a[r]*b[c*4]+a[4+r]*b[c*4+1]+a[8+r]*b[c*4+2]+a[12+r]*b[c*4+3];return o}
const rotX=a=>{const c=cos(a),s=sin(a);return[1,0,0,0,0,c,s,0,0,-s,c,0,0,0,0,1]};
const rotY=a=>{const c=cos(a),s=sin(a);return[c,0,-s,0,0,1,0,0,s,0,c,0,0,0,0,1]};
const rotZ=a=>{const c=cos(a),s=sin(a);return[c,s,0,0,-s,c,0,0,0,0,1,0,0,0,0,1]};
const scaleAt=(p,s)=>[s,0,0,0,0,s,0,0,0,0,s,0,p[0]*(1-s),p[1]*(1-s),p[2]*(1-s),1];
const persp=(f,a,n,r)=>[f/a,0,0,0,0,f,0,0,0,0,(r+n)/(n-r),-1,0,0,2*r*n/(n-r),0];
const ortho=(r,n,f)=>[1/r,0,0,0,0,1/r,0,0,0,0,-2/(f-n),0,0,0,-(f+n)/(f-n),1];
function look(e,t,roll){let z=norm(sub(e,t)),x=norm(cross(abs(z[1])>.999?[0,0,1]:[0,1,0],z)),y=cross(z,x);
  if(roll){const c=cos(roll),s=sin(roll),x2=add(mul3(x,c),mul3(y,s));y=add(mul3(y,c),mul3(x,-s));x=x2}
  return[x[0],y[0],z[0],0,x[1],y[1],z[1],0,x[2],y[2],z[2],0,-dot(x,e),-dot(y,e),-dot(z,e),1]}
let seed=7;const rnd=()=>{seed|=0;seed=seed+0x6D2B79F5|0;let t=Math.imul(seed^seed>>>15,1|seed);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296};
const rr=(a,b)=>a+(b-a)*rnd();
const qAxis=(ax,a)=>{const s=sin(a/2);return[ax[0]*s,ax[1]*s,ax[2]*s,cos(a/2)]};
const qFromY=d=>{const c=cross([0,1,0],d),w=1+d[1];if(w<1e-5)return[1,0,0,0];const l=hypot(c[0],c[1],c[2],w);return[c[0]/l,c[1]/l,c[2]/l,w/l]};
const hash2=(x,y)=>{const s=sin(x*127.1+y*311.7)*43758.5453;return s-floor(s)};
const vnoise=(x,y)=>{const i=floor(x),j=floor(y),f=x-i,g=y-j,u=f*f*(3-2*f),v=g*g*(3-2*g);return mix(mix(hash2(i,j),hash2(i+1,j),u),mix(hash2(i,j+1),hash2(i+1,j+1),u),v)};
// camera keys: [bar, px,py,pz, tx,ty,tz, roll, fov]; Catmull-Rom between them
function path(K,x){let i=0;while(i<K.length-2&&x>=K[i+1][0])i++;const a=K[max(i-1,0)],b=K[i],c=K[i+1],d=K[min(i+2,K.length-1)],u=cl((x-b[0])/(c[0]-b[0])),o=[];
  for(let j=1;j<9;j++){const m1=(c[j]-a[j])*.5,m2=(d[j]-b[j])*.5,u2=u*u,u3=u2*u;o.push((2*u3-3*u2+1)*b[j]+(u3-2*u2+u)*m1+(-2*u3+3*u2)*c[j]+(u3-u2)*m2)}
  return{p:o.slice(0,3),t:o.slice(3,6),r:o[6],f:o[7]}}

// ---------- GL ----------
const T=SONG(),cv=document.getElementById("c"),gl=cv.getContext("webgl2",{antialias:false,alpha:false,powerPreference:"high-performance"});
if(!gl){document.getElementById("h").textContent="WebGL2 needed";document.getElementById("h").style.opacity=1;throw 0}
const HDR=!!gl.getExtension("EXT_color_buffer_float")||!!gl.getExtension("EXT_color_buffer_half_float");
const CF=HDR?gl.RGBA16F:gl.RGBA8,CT=HDR?gl.HALF_FLOAT:gl.UNSIGNED_BYTE;
function prog(vs,fs){const p=gl.createProgram();for(const[t,s]of[[gl.VERTEX_SHADER,vs],[gl.FRAGMENT_SHADER,fs]]){const sh=gl.createShader(t);gl.shaderSource(sh,"#version 300 es\nprecision highp float;precision highp sampler2DShadow;\n"+s);gl.compileShader(sh);
    if(!gl.getShaderParameter(sh,gl.COMPILE_STATUS))console.error(gl.getShaderInfoLog(sh));gl.attachShader(p,sh)}
  gl.linkProgram(p);if(!gl.getProgramParameter(p,gl.LINK_STATUS))console.error(gl.getProgramInfoLog(p));
  const u={};p.u=n=>n in u?u[n]:u[n]=gl.getUniformLocation(p,n);return p}
const VS=`layout(location=0)in vec3 aC;layout(location=1)in vec3 aO;layout(location=2)in vec3 aN;
layout(location=3)in vec3 iP;layout(location=4)in vec3 iS;layout(location=5)in vec4 iQ;layout(location=6)in vec4 iM;
uniform mat4 uVP,uLV,uG[12];uniform vec4 uK;uniform float uNS;
out vec3 vW,vN,vL,vLN;out vec4 vM,vSh;
vec3 qr(vec3 v,vec4 q){return v+2.*cross(q.xyz,cross(q.xyz,v)+q.w*v);}
void main(){
 vec3 s=iS;float ph=iM.z;
 float e=iS.y*uK.y*(.5+.5*sin(uK.z+ph*40.));s.y+=e;
 float b=min(uK.w,min(s.x,min(s.y,s.z))*.35);
 vec3 l=aC*(s-b)+aO*b;l.y+=e;
 vec3 n=uNS>0.?normalize(aN/s):aN;
 vec3 p=iP+normalize(iP+1e-4)*uK.x*ph;
 mat4 G=uG[int(iM.y)];
 vec4 w=G*vec4(qr(l,iQ)+p,1.);
 vW=w.xyz;vN=mat3(G)*qr(n,iQ);vL=l;vLN=n;vM=iM;vSh=uLV*w;
 gl_Position=uVP*w;
}`;
const FS=`in vec3 vW,vN,vL,vLN;in vec4 vM,vSh;
uniform vec3 uCam,uLd,uLc;uniform vec4 uPL[3],uF,uA;uniform sampler2DShadow uSM;
out vec4 o;
float h(vec2 p){vec3 q=fract(vec3(p.xyx)*.1031);q+=dot(q,q.yzx+33.33);return fract((q.x+q.y)*q.z);}
const vec3 GOLD=vec3(1.,.72,.34),F0=vec3(.22,.3,.42);
void main(){
 vec3 N=normalize(vN),V=uCam-vW;float d=length(V);V/=d;
 vec3 an=abs(vLN);vec2 uv;float fid;
 if(an.x>an.y&&an.x>an.z){uv=vL.zy;fid=1.;}else if(an.y>an.z){uv=vL.xz;fid=2.;}else{uv=vL.xy;fid=3.;}
 float fl=step(.9,max(an.x,max(an.y,an.z)));
 vec2 g=uv/uA.w+fract(vM.z*13.7)*8.+fid*5.3;vec2 id=floor(g),f=fract(g),fw=fwidth(g);
 float lod=clamp(1.2-max(fw.x,fw.y)*2.,0.,1.)*fl,r=h(id+fid*7.);
 vec2 e=smoothstep(vec2(0.),fw*1.5+.012,min(f,1.-f));
 float line=mix(1.,e.x*e.y,lod);
 vec3 alb=vec3(.065,.1,.165)*(.45+r)*mix(.2,1.,line);
 float ro=mix(.2,.62,h(id+19.));
 vec2 w=g*vec2(8.,4.),wi=floor(w),wf=fract(w);
 float lw=clamp(1.2-max(fw.x*8.,fw.y*4.)*1.2,0.,1.)*fl;
 float win=step(.2,wf.x)*step(wf.x,.8)*step(.3,wf.y)*step(wf.y,.7)*step(1.-vM.w*.3,h(wi+fid*3.))*step(.66,h(id*.5+fid))*(.25+1.3*pow(h(wi+floor(uA.y)*.37),2.))*lw;
 float nl=max(dot(N,uLd),0.),sh=1.;
 vec3 sc=vSh.xyz/vSh.w*.5+.5;
 if(nl>0.&&abs(sc.x-.5)<.5&&abs(sc.y-.5)<.5&&sc.z<1.){sh=0.;float z=sc.z-.0012;
  for(int i=0;i<4;i++){vec2 q=vec2(float(i&1)-.5,float(i>>1)-.5)*uA.x;sh+=texture(uSM,vec3(sc.xy+q,z));}sh*=.25;}
 vec3 H=normalize(uLd+V);float a=ro*ro,a2=a*a,nh=max(dot(N,H),0.),D=a2/(3.1416*pow(nh*nh*(a2-1.)+1.,2.));
 float fr=pow(1.-max(dot(N,V),0.),4.);
 vec3 c=uLc*nl*sh*(alb+min(D,40.)*(F0+(1.-F0)*pow(1.-max(dot(V,H),0.),5.))*.25);
 c+=alb*mix(vec3(.3,.35,.5),vec3(.5,.8,1.4),N.y*.5+.5)*uA.z;
 vec3 R=reflect(-V,N);
 c+=(vec3(.02,.045,.09)*(.3+.7*smoothstep(-.5,.8,R.y))+vec3(.2,.34,.6)*pow(max(1.-abs(R.y-.2)*3.,0.),3.)*.35)*(F0+fr)*(1.-ro)*uA.z*4.5;
 for(int i=0;i<3;i++){vec3 L=uPL[i].xyz-vW;float d2=dot(L,L);L*=inversesqrt(d2);float at=uPL[i].w/(1.+d2);vec3 H2=normalize(L+V);float n2=max(dot(N,H2),0.);
  c+=GOLD*at*max(dot(N,L),0.)*(alb+min(a2/(3.1416*pow(n2*n2*(a2-1.)+1.,2.)),40.)*F0*.25);}
 c+=GOLD*(win*5.+vM.x)*uF.z;
 o=vec4(c*exp(-d*uF.x)*uF.y,1.);
}`;
const FSD=`void main(){}`;
const VQ=`out vec2 v;void main(){vec2 p=vec2(gl_VertexID&1,gl_VertexID>>1)*2.;v=p;gl_Position=vec4(p*2.-1.,0,1);}`;
const FDN=`in vec2 v;uniform sampler2D uT;uniform vec3 uP;out vec4 o;
void main(){vec3 c=(texture(uT,v+uP.xy).rgb+texture(uT,v-uP.xy).rgb+texture(uT,v+uP.xy*vec2(1,-1)).rgb+texture(uT,v-uP.xy*vec2(1,-1)).rgb)*.25;
 if(uP.z>0.){c=min(c,vec3(40.));float l=max(c.r,max(c.g,c.b));c*=max(l-uP.z,0.)/max(l,1e-4);}
 o=vec4(c,1.);}`;
const FRAY=`in vec2 v;uniform sampler2D uT;uniform vec2 uC;out vec4 o;
void main(){vec2 p=v,dl=(v-uC)/32.*.92;vec3 a=vec3(0.);float w=1.;for(int i=0;i<32;i++){a+=texture(uT,p).rgb*w;p-=dl;w*=.94;}o=vec4(min(a*.018,vec3(3.)),1.);}`;
const FOUT=`in vec2 v;uniform sampler2D uS,uB,uR;uniform vec4 uP,uX;out vec4 o;
float h(vec2 p){vec3 q=fract(vec3(p.xyx)*.1031);q+=dot(q,q.yzx+33.33);return fract((q.x+q.y)*q.z);}
void main(){vec2 q=v-.5;float r2=dot(q,q);vec2 k=q*r2*uP.x;
 vec3 c=vec3(texture(uS,v-k).r,texture(uS,v).g,texture(uS,v+k).b);
 c+=texture(uB,v).rgb*uP.y+texture(uR,v).rgb*uP.z;
 c+=vec3(1.,.8,.5)*uX.x;
 c*=uP.w*(1.-r2*1.15);
 c=clamp((c*(2.51*c+.03))/(c*(2.43*c+.59)+.14),0.,1.);
 c=pow(c,vec3(.4545));
 c+=(h(gl_FragCoord.xy+uX.y*61.)-.5)*.035;
 o=vec4(c,1.);}`;
const pS=prog(VS,FS),pD=prog(VS,FSD),pDn=prog(VQ,FDN),pRay=prog(VQ,FRAY),pOut=prog(VQ,FOUT);

// ---------- meshes made in code: each vertex is corner part, bevel part, normal ----------
function mesh(v,ns){const b=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,b);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(v),gl.STATIC_DRAW);return{b,n:v.length/9,ns}}
function boxMesh(){const v=[],P=(c,k)=>{const o=[0,0,0];o[k]=c[k];return[c,o]},
  tri=(A,B,C,n)=>{const pos=q=>add(mul3(q[0],.8),mul3(q[1],.2));if(dot(cross(sub(pos(B),pos(A)),sub(pos(C),pos(A))),n)<0){const t=B;B=C;C=t}for(const q of[A,B,C])v.push(...q[0],...q[1],...n)},
  quad=(a,b,c,d,n)=>{tri(a,b,c,n);tri(a,c,d,n)};
  for(let k=0;k<3;k++)for(const s of[-1,1]){const a=(k+1)%3,b=(k+2)%3,C=(x,y)=>{const c=[0,0,0];c[k]=s;c[a]=x;c[b]=y;return P(c,k)},n=[0,0,0];n[k]=s;quad(C(-1,-1),C(1,-1),C(1,1),C(-1,1),n)}
  for(let k=0;k<3;k++)for(let j=k+1;j<3;j++)for(const s1 of[-1,1])for(const s2 of[-1,1]){const m=3-k-j,c1=[0,0,0],c2=[0,0,0];c1[k]=c2[k]=s1;c1[j]=c2[j]=s2;c1[m]=-1;c2[m]=1;const n=[0,0,0];n[k]=s1*.7071;n[j]=s2*.7071;quad(P(c1,k),P(c2,k),P(c2,j),P(c1,j),n)}
  for(let i=0;i<8;i++){const c=[i&1?1:-1,i&2?1:-1,i&4?1:-1];tri(P(c,0),P(c,1),P(c,2),mul3(c,.57735))}
  return mesh(v,0)}
function plain(tris,smooth){const v=[];for(let i=0;i<tris.length;i+=3){const a=tris[i],b=tris[i+1],c=tris[i+2],n=norm(cross(sub(b,a),sub(c,a)));for(const q of[a,b,c])v.push(...q,0,0,0,...(smooth?smooth(q):n))}return mesh(v,1)}
function prismMesh(n){const t=[],p=(i,y)=>[cos(i/n*TAU),y,sin(i/n*TAU)];
  for(let i=0;i<n;i++){t.push(p(i,-1),p(i,1),p(i+1,1),p(i,-1),p(i+1,1),p(i+1,-1),[0,1,0],p(i+1,1),p(i,1),[0,-1,0],p(i,-1),p(i+1,-1))}
  return plain(t)}
function octaMesh(){const t=[];for(let i=0;i<8;i++){const x=[i&1?1:-1,0,0],y=[0,i&2?1:-1,0],z=[0,0,i&4?1:-1];(x[0]*y[1]*z[2]>0)?t.push(x,y,z):t.push(x,z,y)}return plain(t)}
function sphereMesh(){const t=[],A=24,B=14,p=(i,j)=>{const a=i/A*TAU,b=j/B*PI;return[sin(b)*cos(a),cos(b),sin(b)*sin(a)]};
  for(let j=0;j<B;j++)for(let i=0;i<A;i++){t.push(p(i,j),p(i+1,j),p(i+1,j+1),p(i,j),p(i+1,j+1),p(i,j+1))}return plain(t,q=>q)}
const MESH=[boxMesh(),prismMesh(6),octaMesh(),sphereMesh()],BOX=0,HEX=1,OCT=2,SPH=3;

// ---------- scenes ----------
// A(mesh, pos, halfsize, {e: glow, g: group, ph: phase, w: windows, q: quaternion})
function makeScene(def){const data=MESH.map(()=>[]);
  def.build((m,p,s,o={})=>{const q=o.q||[0,0,0,1];data[m].push(p[0],p[1],p[2],s[0],s[1],s[2],q[0],q[1],q[2],q[3],o.e||0,o.g||0,o.ph||0,o.w||0)});
  def.draw=MESH.map((M,i)=>{if(!data[i].length)return 0;const vao=gl.createVertexArray();gl.bindVertexArray(vao);
    gl.bindBuffer(gl.ARRAY_BUFFER,M.b);for(let k=0;k<3;k++){gl.enableVertexAttribArray(k);gl.vertexAttribPointer(k,3,gl.FLOAT,false,36,k*12)}
    const ib=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,ib);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(data[i]),gl.STATIC_DRAW);
    [[3,3,0],[4,3,12],[5,4,24],[6,4,40]].forEach(([l,n,o])=>{gl.enableVertexAttribArray(l);gl.vertexAttribPointer(l,n,gl.FLOAT,false,56,o);gl.vertexAttribDivisor(l,1)});
    return{vao,n:M.n,c:data[i].length/14,ns:M.ns}});
  gl.bindVertexArray(null);return def}
const LAMP=11; // group 11 of every scene holds the one lamp the camera dives into
function lamps(A,n,R0,R1,P,keep){for(let i=0;i<n;i++){let p;do{p=mul3(norm([rr(-1,1),rr(-.7,.7),rr(-1,1)]),rr(R0,R1))}while(keep&&!keep(p));A(OCT,p,[.5,.8,.5],{e:rr(10,22)})}
  A(OCT,P,[.6,.95,.6],{e:26,g:LAMP})}
function ring(A,R,n,hr,hy,fill,g,o={}){for(let i=0;i<n;i++){const a=i/n*TAU;if(o.skip&&o.skip(i))continue;
    A(BOX,[R*cos(a),0,R*sin(a)],[hr,hy,PI*R/n*fill],{q:qAxis([0,1,0],-a),g,w:o.w||0,ph:i/n});
    if(o.strip)A(BOX,[(R-hr-.05)*cos(a),0,(R-hr-.05)*sin(a)],[.06,hy*.12,PI*R/n*fill*.8],{q:qAxis([0,1,0],-a),g,e:o.strip})}}
const base=(o)=>Object.assign({G:Array.from({length:12},I4),K:[0,0,0,.12],Ld:norm([.5,.7,.4]),Lc:[2.4,2.9,3.6],PL:[0,0,0,0,0,0,0,0,0,0,0,0],fog:.004,shc:[0,0,0],shr:120,pan:3,amb:.3,bloom:.16,ray:0,core:[0,0,0],ca:.5,em:1},o);
const FONT={A:[14,17,17,31,17,17,17],S:[15,16,16,14,1,1,30],H:[17,17,17,31,17,17,17],L:[16,16,16,16,16,16,31],R:[30,17,17,30,20,18,17]};
let kk=0,beatF=0; // kick envelope and beat position, set every frame

const SC=[
// 0 SLAB: one monolith wakes in the dark
{P:[55,24,-80],fogc:.0035,build(A){seed=11;
  A(BOX,[0,0,0],[9,30,3.2],{w:1});
  A(BOX,[0,0,3.25],[.1,30,.06],{e:6});A(BOX,[0,0,-3.25],[.1,30,.06],{e:6});
  ring(A,24,28,.7,1.3,.72,1,{w:.6,strip:8});ring(A,33,36,.5,2.2,.5,2,{w:.5});ring(A,45,48,1.1,.8,.82,3,{w:.4,strip:6,skip:i=>i%6==5});
  for(let i=0;i<14;i++){const a=rr(0,TAU),r=rr(70,150),h=rr(8,26);A(BOX,[r*cos(a),rr(-50,50),r*sin(a)],[h*.3,h,h*.11],{w:1,ph:rnd(),q:qAxis(norm([rr(-1,1),0,rr(-1,1)]),rr(-.3,.3))})}
  lamps(A,34,40,110,this.P,p=>p[2]>-40||p[0]<20)},
 cam:[[0,-3,-16,4.6,-3.5,6,3.3,.5,.9],[2.5,-4,-6,7,-1,10,3.2,.2,.9],[4.6,-58,4,44,0,4,0,0,.8],[6,-78,16,-62,0,8,0,-.1,.78],[7.2,30,22,-150,30,15,-20,0,.75],[8,55,24,-215,50,22,-60,0,.75]],
 frame(x){const c=path(this.cam,x),li=ss(.4,3,x);
  const F=base({p:c.p,t:c.t,r:c.r,f:c.f,Ld:norm([sin(.6+x*.12),.55,cos(.6+x*.12)]),Lc:[2.6*li,3.1*li,3.9*li],fog:.0032,shr:75,pan:2.5,amb:.12+.2*li});
  F.G[1]=mm(rotX(.5),rotY(x*.25));F.G[2]=mm(rotZ(-.7),rotY(-x*.18+1));F.G[3]=mm(rotX(-.25),rotY(x*.1));return F}},
// 1 LATTICE: a frame without end, flown down its corridors
{P:[40,104,-30],build(A){seed=23;const D=30,ok=(i,j,k)=>abs(i)<5&&abs(j)<3&&abs(k)<5&&hash2(i*7+k*13.1,j*5+k*3.7)>.16;
  for(let i=-4;i<5;i++)for(let j=-2;j<3;j++)for(let k=-4;k<5;k++){if(!ok(i,j,k))continue;const p=[i*D,j*D,k*D],r=rnd();
    A(BOX,p,[4,4,4],{w:1,ph:rnd()});if(r<.3)A(BOX,p,[4.6,.5,4.6],{ph:rnd()});if(r>.75)A(BOX,p,[1.2,6.5,1.2],{w:.5,ph:rnd()});
    for(let ax=0;ax<3;ax++){const q=[i,j,k];q[ax]++;if(!ok(...q)||rnd()<.12)continue;const c=[i*D,j*D,k*D],h=[1,1,1];c[ax]+=D/2;h[ax]=11;const t=rnd();
      A(BOX,c,h,{w:.25,ph:rnd()});
      if(t<.3){const h2=[.07,.07,.07],c2=c.slice();h2[ax]=9;c2[ax==1?0:1]+=1.05;A(BOX,c2,h2,{e:9})}
      if(t>.8&&ax!=1){const h3=[1.4,1.4,1.4];h3[ax]=2.5;A(BOX,c,h3,{w:1,ph:rnd()})}}
    if(rnd()<.07&&j<2)A(BOX,[i*D+15,j*D,k*D+15],[13.6,.4,13.6],{w:.6,ph:rnd()})}
  lamps(A,44,30,160,this.P,p=>!(abs(p[0]-15)<6&&abs(p[1]-15)<6)&&!(abs(p[2]-15)<6&&abs(p[1]-15)<6)&&!(abs(p[0]-105)<6&&abs(p[2]-15)<6))},
 cam:[[0,15,15,-150,15,15,0,0,.8],[3.5,15,15,-60,15,15,40,.5,.95],[6,15,15,8,50,15,15,-.15,.95],[9.5,95,15,15,105,45,15,.4,.95],[11.5,105,80,12,95,20,-20,0,.85],[14,150,125,-110,30,60,-10,-.1,.75],[16,75,150,-170,40,104,-30,0,.75]],
 frame(x){const c=path(this.cam,x);return base({p:c.p,t:c.t,r:c.r,f:c.f,Ld:norm([.45,.6,-.5]),fog:.0048,shc:[c.p[0]*.6,0,c.p[2]*.6],shr:170,pan:2,amb:.28,bloom:.17})}},
// 2 GYRE: nested gimbals that tick with the kick
{P:[76,64,-76],build(A){seed=37;
  A(OCT,[0,0,0],[4.5,7,4.5],{e:30});
  [[13,14,1,1.8,.8],[21,22,1.2,3.4,.62],[31,30,1.8,1.6,.86],[43,40,1.4,5,.5],[57,52,2.6,2.2,.78],[73,64,1.6,7,.66]].forEach((r,i)=>{
    ring(A,r[0],r[1],r[2],r[3],r[4],i+1,{w:.9,strip:i%2?7:0,skip:i%3==2?(k=>k%8>5):0});
    for(let k=0;k<4;k++){const a=k*PI/2+i;A(BOX,[(r[0]+r[2]+1.2)*cos(a),0,(r[0]+r[2]+1.2)*sin(a)],[1.6,.5,.5],{q:qAxis([0,1,0],-a),g:i+1,w:.3})}});
  lamps(A,40,95,170,this.P)},
 cam:[[0,30,40,-130,0,0,0,0,.75],[4,-90,25,-70,0,0,0,.25,.75],[7.5,-48,-8,26,0,0,0,.6,.95],[10,-9,3,25,0,0,0,.2,1.05],[12.5,50,-22,60,0,0,0,-.3,.9],[14.5,105,50,-40,30,25,-30,-.1,.8],[16,76,100,-166,76,64,-76,0,.75]],
 frame(x){const c=path(this.cam,x),F=base({p:c.p,t:c.t,r:c.r,f:c.f,Ld:norm([-.5,.65,-.35]),fog:.0035,shr:90,pan:2.2,amb:.26,bloom:.18});
  const b=x*4,tick=floor(b)+ss(0,.45,b-floor(b));let M=I4();
  for(let i=6;i>0;i--){const a=tick*(i%2?1:-1)*PI/(4+i*2)+i;M=mm(M,i%2?rotX(a):rotZ(a));F.G[i]=M}
  F.PL=[0,0,0,900+500*kk,0,0,0,0,0,0,0,0];return F}},
// 3 STRATA: two fields of towers, floor and ceiling, closing on each other
{P:[0,0,100],build(A){seed=41;
  for(const g of[0,1])for(let i=-22;i<23;i++)for(let k=-22;k<23;k++){const x=i*5+(k&1)*2.5,z=k*4.33,d=hypot(x,z);if(d>108)continue;
    const n=vnoise(x*.035+g*9,z*.035),m=vnoise(x*.11+3,z*.11+g*5),h=min(10.5,.8+10*pow(n,1.8)*(.5+.9*m));
    A(HEX,[x,-22+h/2,z],[2.4,h/2,2.4],{g,w:h>3?1:.3,ph:d/108+g*.08})}
  for(let i=0;i<50;i++){const a=rr(0,TAU),r=rr(20,105),p=[r*cos(a),rr(-.4,.4),r*sin(a)];if(abs(p[0])>3)A(OCT,p,[.2,.32,.2],{e:rr(10,20)})}
  A(OCT,this.P,[.22,.35,.22],{e:26,g:LAMP})},
 cam:[[0,0,20,-150,0,0,-60,0,.75],[3,26,7,-96,10,-4,-40,.25,.85],[6.5,-40,-6,-30,-10,2,20,-.3,.9],[10,-28,3,44,0,-2,70,.2,.95],[13,-4,0,40,0,0,100,0,.9],[16,0,0,60,0,0,100,0,.8]],
 frame(x){const c=path(this.cam,x),gr=x<8?.12*x/8:.12+.78*pow((x-8)/8,1.5);
  const F=base({p:c.p,t:c.t,r:c.r,f:c.f,Ld:norm([.35,.25,.6]),Lc:[1.6,2,2.6],fog:.0085,shr:125,pan:1.6,amb:.34,bloom:.2,K:[0,gr,-x*PI,.1]});
  F.G[1]=rotZ(PI);F.PL=[0,0,100,500+2500*ss(8,16,x),0,0,0,0,0,0,0,0];return F}},
// 4 SHELL: plates around a white-gold core, thrown outward on every kick
{P:[64,92,44],build(A){seed=53;
  A(SPH,[0,0,0],[9,9,9],{e:34});
  [[22,260,2.1,.35,1],[34,520,2.6,.5,2],[49,900,3.3,.7,3]].forEach(([R,n,s,t,g])=>{for(let i=0;i<n;i++){const y=1-2*(i+.5)/n,r=sqrt(1-y*y),a=i*2.39996,d=[r*cos(a),y,r*sin(a)];
      if(hash2(i*.37,g)<.26)continue;const u=rnd();A(BOX,mul3(d,R),[s*rr(.6,1.15),t,s*rr(.6,1.15)],{q:qFromY(d),g,w:u<.5?1:.2,ph:.3+u*u*1.4})}});
  for(let i=0;i<70;i++){const d=norm([rr(-1,1),rr(-1,1),rr(-1,1)]),l=rr(4,16);A(BOX,mul3(d,62+l),[.35,l,.35],{q:qFromY(d),g:4,w:.3,ph:.2});if(i%3==0)A(OCT,mul3(d,64+l*2),[.5,.8,.5],{e:16,g:4})}
  lamps(A,30,105,180,this.P)},
 cam:[[0,0,0,-120,0,0,0,0,.75],[3,70,30,-80,0,0,0,.2,.8],[6,62,-12,20,0,0,0,.5,1],[8.5,20,10,38,0,0,0,.1,1.1],[11,-40,35,30,0,0,0,-.3,.95],[13.5,-60,90,150,10,30,20,-.1,.8],[16,64,82,174,64,92,44,0,.75]],
 frame(x){const c=path(this.cam,x),F=base({p:c.p,t:c.t,r:c.r,f:c.f,Ld:norm([.3,.5,-.8]),Lc:[1.2,1.5,2],fog:.003,shr:85,pan:1.8,amb:.2,bloom:.2,ray:(.5+.35*kk)*ss(22,85,len(c.p)),K:[kk*5+1.5*sin(x*PI/2)**2,0,0,.1],ca:.8+kk});
  F.G[1]=mm(rotY(x*.5),rotX(x*.3));F.G[2]=mm(rotZ(-x*.3),rotY(-x*.22));F.G[3]=mm(rotX(x*.14),rotZ(x*.1));F.G[4]=rotY(-x*.08);
  F.PL=[0,0,0,2600+1800*kk,0,0,0,0,0,0,0,0];return F}},
// 5 ASHLAR: the name, cut from the same stone
{P:[0,0,0],build(A){seed=67;
  [..."ASHLAR"].forEach((ch,li)=>FONT[ch].forEach((row,ry)=>{for(let cx=0;cx<5;cx++)if(row>>(4-cx)&1){const dp=rr(2.5,5);
    A(BOX,[(li-2.5)*27+(cx-2)*4.5,(3-ry)*4.5,0],[2.15,2.15,dp],{w:rnd()<.6?1:0,ph:rnd()});if(rnd()<.12)A(BOX,[(li-2.5)*27+(cx-2)*4.5,(3-ry)*4.5,dp+.1],[.9,.9,.08],{e:9})}}));
  A(BOX,[0,-19,0],[84,.5,7],{w:.3});A(BOX,[0,-19.6,7.1],[84,.07,.07],{e:10});A(BOX,[0,19,0],[84,.5,7],{w:.3});
  
  lamps(A,40,60,200,[0,400,0],p=>abs(p[2])>25)},
 cam:[[0,0,-8,125,0,0,0,0,.75],[4,-30,-4,150,-4,0,0,-.05,.7],[9,30,6,175,0,0,0,.04,.62],[16,0,12,230,0,0,0,0,.55]],
 frame(x){const c=path(this.cam,x),a=W/H;c.p=add(c.t,mul3(sub(c.p,c.t),1.6*max(a/1.6,.5)/min(a,1.6)));return base({p:c.p,t:c.t,r:c.r,f:c.f,Ld:norm([sin(x*.2-.6),.5,cos(x*.2-.6)]),fog:.0026,shr:110,pan:2.2,amb:.26,bloom:.18,em:1+.5*kk})}}
].map(makeScene);
const SCALE=300; // every next scene sits inside a lamp of the one before it, this many times smaller

// ---------- targets ----------
let W=0,H=0,RS=1,fbS,fbR,texR,texSM,fbSM,bl=[],texRay,fbRay,MS=0;const SMS=2048;
function tex(w,h,f,t,i){const x=gl.createTexture();gl.bindTexture(gl.TEXTURE_2D,x);gl.texStorage2D(gl.TEXTURE_2D,1,i,w,h);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);return x}
function fbo(t,d){const f=gl.createFramebuffer();gl.bindFramebuffer(gl.FRAMEBUFFER,f);if(t)gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,t,0);if(d)gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.DEPTH_ATTACHMENT,gl.TEXTURE_2D,d,0);return f}
texSM=tex(SMS,SMS,0,0,gl.DEPTH_COMPONENT24);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_COMPARE_MODE,gl.COMPARE_REF_TO_TEXTURE);fbSM=fbo(0,texSM);gl.drawBuffers([gl.NONE]);gl.readBuffer(gl.NONE);
let old=[];
function resize(){const dpr=min(devicePixelRatio||1,2),cw=innerWidth,ch=innerHeight;let w=cw*dpr*RS,h=ch*dpr*RS;const k=min(1,sqrt(2.6e6/(w*h)));w=max(2,w*k|0);h=max(2,h*k|0);
  if(w==W&&h==H)return;W=w;H=h;cv.width=w;cv.height=h;
  for(const o of old)o.del(o.x);old=[];const keep=(x,del)=>(old.push({x,del:del.bind(gl)}),x);
  const T2=(w,h)=>keep(tex(w,h,0,0,CF),gl.deleteTexture),FB=(t,d)=>keep(fbo(t,d),gl.deleteFramebuffer);
  texR=T2(w,h);fbR=FB(texR);
  // multisampled scene target, resolved into texR
  const ms=min(4,gl.getParameter(gl.MAX_SAMPLES));fbS=keep(gl.createFramebuffer(),gl.deleteFramebuffer);gl.bindFramebuffer(gl.FRAMEBUFFER,fbS);
  const rb=n=>{const c=keep(gl.createRenderbuffer(),gl.deleteRenderbuffer);gl.bindRenderbuffer(gl.RENDERBUFFER,c);gl.renderbufferStorageMultisample(gl.RENDERBUFFER,n,CF,w,h);gl.framebufferRenderbuffer(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.RENDERBUFFER,c);
    const d=keep(gl.createRenderbuffer(),gl.deleteRenderbuffer);gl.bindRenderbuffer(gl.RENDERBUFFER,d);gl.renderbufferStorageMultisample(gl.RENDERBUFFER,n,gl.DEPTH_COMPONENT24,w,h);gl.framebufferRenderbuffer(gl.FRAMEBUFFER,gl.DEPTH_ATTACHMENT,gl.RENDERBUFFER,d)};
  rb(ms);MS=ms;if(gl.checkFramebufferStatus(gl.FRAMEBUFFER)!=gl.FRAMEBUFFER_COMPLETE){rb(0);MS=0}
  bl=[];let bw=w,bh=h;for(let i=0;i<6;i++){bw=max(1,bw>>1);bh=max(1,bh>>1);const t=T2(bw,bh);bl.push({t,f:FB(t),w:bw,h:bh})}
  texRay=T2(bl[1].w,bl[1].h);fbRay=FB(texRay)}

// ---------- drawing ----------
function drawScene(S,F,p,VP,LV,cam,fogK,fade,emis){gl.useProgram(p);
  gl.uniformMatrix4fv(p.u("uVP"),false,VP);gl.uniformMatrix4fv(p.u("uLV"),false,LV);gl.uniformMatrix4fv(p.u("uG"),false,F.G.flat());gl.uniform4fv(p.u("uK"),F.K);
  if(p==pS){gl.uniform3fv(p.u("uCam"),cam);gl.uniform3fv(p.u("uLd"),F.Ld);gl.uniform3fv(p.u("uLc"),F.Lc);gl.uniform4fv(p.u("uPL"),F.PL);
    gl.uniform4f(p.u("uF"),F.fog*fogK,fade,emis*F.em*(1+.35*kk),0);gl.uniform4f(p.u("uA"),1.4/SMS,floor(beatF*4),F.amb,F.pan);
    gl.activeTexture(gl.TEXTURE0);gl.bindTexture(gl.TEXTURE_2D,texSM);gl.uniform1i(p.u("uSM"),0)}
  for(const d of S.draw)if(d){gl.uniform1f(p.u("uNS"),d.ns);gl.bindVertexArray(d.vao);gl.drawArraysInstanced(gl.TRIANGLES,0,d.n,d.c)}}
function pass(S,F,cam,tg,roll,fov,near,far,fogK,fade,emis,clear){
  // shadow map from the key light
  const lv=mm(ortho(F.shr,1,F.shr*4),look(add(F.shc,mul3(F.Ld,F.shr*2)),F.shc,0));
  gl.bindFramebuffer(gl.FRAMEBUFFER,fbSM);gl.viewport(0,0,SMS,SMS);gl.clear(gl.DEPTH_BUFFER_BIT);gl.enable(gl.POLYGON_OFFSET_FILL);gl.polygonOffset(2,4);
  drawScene(S,F,pD,lv,lv,cam,0,0,0);gl.disable(gl.POLYGON_OFFSET_FILL);
  gl.bindFramebuffer(gl.FRAMEBUFFER,fbS);gl.viewport(0,0,W,H);if(clear){gl.clearColor(0,0,0,1);gl.clear(gl.COLOR_BUFFER_BIT)}gl.clear(gl.DEPTH_BUFFER_BIT);
  const a=W/H,f=1/tan(fov/2)*cl(a/1.6,.5,1),vp=mm(persp(f,a,near,far),look(cam,tg,roll));
  drawScene(S,F,pS,vp,lv,cam,fogK,fade,emis);return vp}
function quad(p,f,w,h){gl.bindFramebuffer(gl.FRAMEBUFFER,f);gl.viewport(0,0,w,h);gl.drawArrays(gl.TRIANGLES,0,3)}
function render(t){
  const bar=t/T.BAR;let i=0;while(bar>=T.STARTS[i+1])i++;const A=SC[i],x=bar-T.STARTS[i],FA=A.frame(x);
  let u=i<5?(bar-(T.STARTS[i+1]-2))/2:-1,cam=FA.p,tg=FA.t,roll=FA.r,fov=FA.f,coreS=A,coreF=FA,vp;
  gl.enable(gl.DEPTH_TEST);gl.enable(gl.CULL_FACE);gl.disable(gl.BLEND);
  if(u>0){ // the handover: the camera falls into one lamp, and the lamp is the next scene
    const B=SC[i+1],FB=B.frame(0),e=1-pow(1-u,1.6),k=pow(SCALE,1-e),P=A.P;
    const pB=add(FB.t,mul3(sub(FB.p,FB.t),k)),w=ss(0,.42,u);
    cam=mix3(FA.p,add(P,mul3(pB,1/SCALE)),w);tg=mix3(FA.t,add(P,mul3(FB.t,1/SCALE)),ss(0,.3,u));roll=mix(FA.r,FB.r,w);fov=mix(FA.f,FB.f,w);
    FA.G[LAMP]=scaleAt(P,1-ss(.15,.5,u));
    pass(A,FA,cam,tg,roll,fov,.05,900,1,1-ss(.5,.92,u),1,1);
    const cB=mul3(sub(cam,P),SCALE),tB=mul3(sub(tg,P),SCALE),dB=len(sub(cB,FB.t)),d0=len(sub(FB.p,FB.t));
    vp=pass(B,FB,cB,tB,roll,fov,max(.3,dB-500),dB+900,min(1,1.4*d0/dB),ss(.02,.3,u),1+7*(1-ss(.25,.8,u)),0);coreF=FB;
  }else vp=pass(A,FA,cam,tg,roll,fov,.3,900,1,1,1,1);
  // resolve, bloom, rays, grade
  gl.bindFramebuffer(gl.READ_FRAMEBUFFER,fbS);gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER,fbR);gl.blitFramebuffer(0,0,W,H,0,0,W,H,gl.COLOR_BUFFER_BIT,gl.NEAREST);
  gl.disable(gl.DEPTH_TEST);gl.disable(gl.CULL_FACE);gl.bindVertexArray(null);gl.activeTexture(gl.TEXTURE0);
  gl.useProgram(pDn);gl.uniform1i(pDn.u("uT"),0);let src=texR,sw=W,sh=H;
  bl.forEach((b,j)=>{gl.bindTexture(gl.TEXTURE_2D,src);gl.uniform3f(pDn.u("uP"),1/sw,1/sh,j?0:1.1);quad(pDn,b.f,b.w,b.h);src=b.t;sw=b.w;sh=b.h});
  const ray=coreF.ray;
  if(ray>0){const c=coreF.core,q=[vp[0]*c[0]+vp[4]*c[1]+vp[8]*c[2]+vp[12],vp[1]*c[0]+vp[5]*c[1]+vp[9]*c[2]+vp[13],vp[3]*c[0]+vp[7]*c[1]+vp[11]*c[2]+vp[15]];
    gl.useProgram(pRay);gl.bindTexture(gl.TEXTURE_2D,bl[1].t);gl.uniform1i(pRay.u("uT"),0);gl.uniform2f(pRay.u("uC"),q[0]/q[2]*.5+.5,q[1]/q[2]*.5+.5);quad(pRay,fbRay,bl[1].w,bl[1].h)}
  gl.useProgram(pDn);gl.enable(gl.BLEND);gl.blendFunc(gl.ONE,gl.ONE);
  for(let j=5;j>0;j--){gl.bindTexture(gl.TEXTURE_2D,bl[j].t);gl.uniform3f(pDn.u("uP"),.5/bl[j].w,.5/bl[j].h,0);quad(pDn,bl[j-1].f,bl[j-1].w,bl[j-1].h)}
  gl.disable(gl.BLEND);
  const hit=i>0?exp(-x*5):0,fade=min(ss(0,1,bar),ss(88,85.5,bar));
  gl.useProgram(pOut);gl.bindTexture(gl.TEXTURE_2D,texR);gl.uniform1i(pOut.u("uS"),0);
  gl.activeTexture(gl.TEXTURE1);gl.bindTexture(gl.TEXTURE_2D,bl[0].t);gl.uniform1i(pOut.u("uB"),1);
  gl.activeTexture(gl.TEXTURE2);gl.bindTexture(gl.TEXTURE_2D,texRay);gl.uniform1i(pOut.u("uR"),2);
  gl.uniform4f(pOut.u("uP"),(FA.ca+kk*.35+hit*3+max(u,0)*1.5)*.014,FA.bloom*(u>0?mix(FA.bloom,coreF.bloom,u)/FA.bloom:1)*(1+.3*kk+hit),ray>0?ray*(u>0?ss(.6,1,u):1):0,fade*(1+hit*.8));
  gl.uniform4f(pOut.u("uX"),hit*.12+(u>0?pow(u,6)*.1:0),t%10,0,0);
  quad(pOut,null,W,H);
}

// ---------- clock, sound, loop ----------
const LEN=T.BARS*T.BAR,q=new URLSearchParams(location.hash.slice(1)),FREEZE=q.has("f");
let t0=0,tPress=0,tFrame=null,tSound=null,ac=null,aBase=null,stat={n:0,sum:0,max:0,slow:0,last:0};
const now=()=>{let t=performance.now()/1e3-t0;if(ac&&aBase){const ta=aBase[0]+ac.currentTime-aBase[1]-(ac.outputLatency||ac.baseLatency||0);if(abs(ta-t)>.06)t0-=ta-t;else t0-=(ta-t)*.04;t=performance.now()/1e3-t0}return FREEZE?(+q.get("t")||0):((t%LEN)+LEN)%LEN};
const hint=document.getElementById("h");
// Sound. The synth runs in a plain Worker (works on any origin, unlike AudioWorklet, which needs https or localhost)
// and hands over half-second blocks that are queued sample-exact on the audio clock. Nothing is rendered up front.
let wk,an,gen=0,endT=0,busy=0,first=0,live=0;
const WK="let sampleRate,currentTime=0,Q,p,g;class AudioWorkletProcessor{constructor(){this.port={postMessage(){}}}}const registerProcessor=(n,c)=>Q=c;"+SONG+SYNTH+
 ";onmessage=e=>{const d=e.data;let n=24064;if(d.sr){sampleRate=d.sr;if(!p){SYNTH();p=new Q}g=d.g;p.port.onmessage({data:d.pos});n=12032}else if(d.g!=g)return;"+
 "const l=new Float32Array(n),r=new Float32Array(n);for(let i=0;i<n;i+=128)p.process([],[[l.subarray(i,i+128),r.subarray(i,i+128)]]);postMessage({g,l,r,pos:d.pos},[l.buffer,r.buffer])}";
function seek(){first=1;busy=1;aBase=null;const pos=(now()+.08)%LEN;wk.postMessage({g:++gen,sr:ac.sampleRate,pos})}
function pump(){if(!ac||ac.state!="running")return;if(!live){live=1;return seek()}
  if(!first&&endT<ac.currentTime+.02)return seek(); // ran dry (a hidden tab, a stall): rejoin at the picture's time
  if(!busy&&endT-ac.currentTime<3){busy=1;wk.postMessage({g:gen})}}
function chunk(e){const d=e.data;if(d.g!=gen)return;busy=0;const sr=ac.sampleRate,n=d.l.length,b=ac.createBuffer(2,n,sr);b.getChannelData(0).set(d.l);b.getChannelData(1).set(d.r);
  if(first){first=0;endT=Math.ceil((ac.currentTime+.04)*sr)/sr;aBase=[d.pos,endT];hint.style.opacity=0;if(tSound==null)tSound=Math.round(performance.now()-tPress+(endT-ac.currentTime)*1e3)}
  const s=ac.createBufferSource();s.buffer=b;s.connect(an);s.start(endT);endT+=n/sr;pump()}
// Must run inside the tap itself: Safari only unlocks audio when resume() and a start() happen in the gesture handler.
function sound(){try{
  if(!ac){ac=new(window.AudioContext||window.webkitAudioContext)({latencyHint:"interactive"});
    an=ac.createAnalyser();const g=ac.createGain();g.gain.value=q.has("m")?0:1;an.connect(g);g.connect(ac.destination);
    wk.onmessage=chunk;ac.onstatechange=pump;setInterval(pump,200)}
  if(ac.state!="running"){ac.resume();const s=ac.createBufferSource();s.buffer=ac.createBuffer(1,1,ac.sampleRate);s.connect(ac.destination);s.start()}
  pump();if(ac.state!="running")hint.style.opacity=.8}catch(e){console.error(e)}}
// The splash: nothing plays until START is pressed. The press itself unlocks sound and starts picture and music together.
try{wk=new Worker(URL.createObjectURL(new Blob([WK],{type:"text/javascript"})))}catch(e){}
const splash=document.getElementById("s");
function start(){if(tPress)return;tPress=performance.now();t0=tPress/1e3-(+q.get("t")||0);sound();
  splash.style.opacity=0;splash.style.pointerEvents="none";setTimeout(()=>splash.remove(),300);adaptAt=tPress+3000;requestAnimationFrame(loop);
  for(const ev of["pointerdown","pointerup","mousedown","click","touchend","keydown"])addEventListener(ev,sound,{passive:true})}
document.getElementById("go").onclick=start;
addEventListener("dblclick",()=>document.fullscreenElement?document.exitFullscreen():document.documentElement.requestFullscreen&&document.documentElement.requestFullscreen());
addEventListener("resize",resize);resize();
function kickEnv(t){const sf=t/T.ST;for(let s=floor(sf);s>sf-16&&s>=0;s--)if(T.kick(floor(s/16),s%16))return exp(-(t-s*T.ST)*7);return 0}
let adaptAt=3000;
function loop(ms){if(tFrame==null&&tPress)tFrame=Math.round(performance.now()-tPress);const t=now();kk=kickEnv(t);beatF=t/T.BEAT;render(t);
  if(stat.last){const d=ms-stat.last;stat.n++;stat.sum+=d;stat.max=max(stat.max,d);if(d>25)stat.slow++;
    // if frames run long, render fewer pixels
    stat.ema=stat.ema?stat.ema*.95+d*.05:d;if(ms>adaptAt&&stat.ema>21&&RS>.5){RS*=.85;adaptAt=ms+1500;stat.ema=16;resize()}}
  stat.last=ms;if(!FREEZE||stat.n<3)requestAnimationFrame(loop);else stat.n++}
window.__st=stat;window.__t=now;window.__a=()=>{let rms=0;if(an){const f=new Float32Array(2048);an.getFloatTimeDomainData(f);for(const v of f)rms+=v*v;rms=Math.sqrt(rms/2048)}return{build:"r3-splash",tFrame,tSound,secure:isSecureContext,state:ac&&ac.state,ctxTime:ac&&+ac.currentTime.toFixed(2),queuedAhead:ac&&+(endT-ac.currentTime).toFixed(2),rms:+rms.toFixed(4),clock:aBase,scale:RS,W,H,MS,HDR}};
render(0); // warm the GPU behind the splash: shaders compile and buffers upload now, the frame itself is black
if(FREEZE){splash.remove();requestAnimationFrame(loop)}
</script>