← UPLIFT v2
index.html
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>UPLIFT</title>
<style>
html,body{margin:0;height:100%;background:#000;overflow:hidden;color:#fff;font:300 13px/1.3 system-ui,-apple-system,"Helvetica Neue",Arial,sans-serif;-webkit-font-smoothing:antialiased}
canvas{position:fixed;inset:0;width:100%;height:100%;display:block}
.o{position:fixed;pointer-events:none;opacity:0;text-transform:uppercase;white-space:nowrap}
#ti{left:0;right:0;top:50%;transform:translateY(-50%);text-align:center;font-size:min(9vw,120px);font-weight:200;letter-spacing:.55em;text-indent:.55em}
#su{left:0;right:0;top:calc(50% + min(7vw,90px));text-align:center;letter-spacing:.4em;text-indent:.4em;font-size:min(2.6vw,13px)}
#sc{left:4vw;bottom:4vw;letter-spacing:.45em;font-size:min(3vw,15px)}
#dp{right:4vw;bottom:4vw;letter-spacing:.3em;font-size:min(2.6vw,12px);font-variant-numeric:tabular-nums}
#ui{position:fixed;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2.2em;text-align:center;letter-spacing:.4em;text-transform:uppercase;padding:0 6vw}
#ui b{font-size:min(10vw,72px);font-weight:200;letter-spacing:.55em;text-indent:.55em}
#ui button{font:inherit;letter-spacing:.4em;text-indent:.4em;text-transform:uppercase;color:#000;background:#fff;border:0;padding:1.1em 2.4em;cursor:pointer}
#ui button:disabled{background:none;color:#888;cursor:default}
#ui small{color:#777;font-size:11px;letter-spacing:.25em;line-height:2}
.play{cursor:none}
</style>
<canvas></canvas>
<div id="ti" class="o">Uplift</div>
<div id="su" class="o"></div>
<div id="sc" class="o"></div>
<div id="dp" class="o"></div>
<div id="ui"><b>Uplift</b><button id="go" disabled>calculating</button><small>one file · real time · sound on<span id="fsh"><br>F for full screen</span></small><script>document.fullscreenEnabled||document.getElementById("fsh").remove()</script></div>
<script>
'use strict';
/*SYNTH*/
// The whole soundtrack is computed here: 88 bars of melodic techno at 124 bpm. It is made bar by bar and each
// finished bar is handed to emit(left,right,firstSample,last), so playback can start while the rest is still being made.
// DR and GN are the master drive and gain; `node tools/synth-check.mjs` prints what they should be after a change to the music.
function synth(sr,emit,cal){
const DR=0.71359,GN=0.96015;
const BPM=124,bt=60/BPM,st=bt/4,BARS=88,N=((BARS*4*bt+3)*sr)|0;
const bus=()=>[new Float32Array(N),new Float32Array(N)];
const dry=bus(),duck=bus(),rev=bus(),del=bus(),sc=new Float32Array(N).fill(1);
const mtof=m=>440*2**((m-69)/12);
let seed=7,ps=99;const ph0=()=>(ps=ps*48271%2147483647)/2147483647,rnd=()=>(seed=seed*16807%2147483647)/1073741823.5-1;
const blep=(p,d)=>p<d?(p/=d,p+p-p*p-1):p>1-d?(p=(p-1)/d,p*p+p+p+1):0;
// one note: unison oscillators through a resonant low-pass with its own envelope
function note(o){
const i0=(o.t*sr)|0,rl=o.rl||.05,len=Math.min(N-i0,((o.d+rl*1.2)*sr)|0),n=o.n||1,w=o.w||0,at=o.at||.003;
const ph=[],inc=[];for(let k=0;k<n;k++){ph.push(ph0());inc.push(o.f*2**((n>1?(k/(n-1)-.5)*2*(o.det||.1):0)/12)/sr)}
const kq=1/(o.q||.8),c0=o.c0||8000,c1=o.c1||c0,cd=o.cd||.1,pan=o.pan||0,gl=Math.cos((pan+1)*Math.PI/4)*o.a/Math.sqrt(n),gr=Math.sin((pan+1)*Math.PI/4)*o.a/Math.sqrt(n);
const B=o.b,rs=o.rs||0,ds=o.ds||0;let ic1=0,ic2=0,a1=0,a2=0,a3=0,e=0,et=0;
for(let i=0;i<len;i++){
const tt=i/sr;
if(!(i&15)){const g=Math.tan(Math.PI*Math.min(c1+(c0-c1)*Math.exp(-tt/cd),sr*.45)/sr);a1=1/(1+g*(g+kq));a2=g*a1;a3=g*a2;
et=Math.min(1,(tt+.0004)/at)*(tt<o.d?1:Math.exp(-(tt-o.d)/rl*4))*(o.dec?Math.exp(-tt/o.dec):1)}
e+=(et-e)*.25;
let x=0;
for(let k=0;k<n;k++){let p=ph[k]+inc[k];if(p>=1)p-=1;ph[k]=p;
if(w==0)x+=2*p-1-blep(p,inc[k]);
else if(w==1){let p2=p+.5;if(p2>=1)p2-=1;x+=(p<.5?1:-1)+blep(p,inc[k])-blep(p2,inc[k])}
else if(w==2)x+=Math.sin(6.2831853*p);
else x+=4*Math.abs(p-.5)-1}
const v3=x-ic2,v1=a1*ic1+a2*v3,v2=ic2+a2*ic1+a3*v3;ic1=2*v1-ic1;ic2=2*v2-ic2;
const yl=v2*e*gl,yr=v2*e*gr,j=i0+i;
B[0][j]+=yl;B[1][j]+=yr;
if(rs){rev[0][j]+=yl*rs;rev[1][j]+=yr*rs}
if(ds){del[0][j]+=yl*ds;del[1][j]+=yr*ds}
}
}
function kick(t,a){
const i0=(t*sr)|0,len=Math.min(N-i0,(.42*sr)|0);
for(let i=0;i<len;i++){const tt=i/sr,y=(Math.sin(6.2831853*(46*tt+5.2*(1-Math.exp(-tt*30))))*Math.exp(-tt*8.5)+rnd()*Math.exp(-tt*300)*.25)*a,j=i0+i;
dry[0][j]+=y;dry[1][j]+=y;const g=1-.85*a*Math.exp(-tt*8);if(g<sc[j])sc[j]=g}
}
// noise voice: hp>0 high-passes, bp>0 sweeps a band-pass from bp to bp2
function noise(t,d,a,o){
const i0=(t*sr)|0,len=Math.min(N-i0,(d*sr)|0),pan=o.pan||0,rs=o.rs||0;let x1=0,x2=0,ic1=0,ic2=0,a1=0,a2=0,a3=0;
for(let i=0;i<len;i++){const tt=i/sr,u=tt/d;let x=rnd();
if(o.bp){if(!(i&31)){const g=Math.tan(Math.PI*Math.min(o.bp*(o.bp2/o.bp)**u,sr*.45)/sr),k=o.k||.4;a1=1/(1+g*(g+k));a2=g*a1;a3=g*a2}
const v3=x-ic2,v1=a1*ic1+a2*v3,v2=ic2+a2*ic1+a3*v3;ic1=2*v1-ic1;ic2=2*v2-ic2;x=v1}
if(o.hp){const y=x-x1;x1=x;x=y-x2*.5;x2=y}
const e=o.env(tt,u)*a,j=i0+i,yl=x*e*(1-pan*.5),yr=x*e*(1+pan*.5);
dry[0][j]+=yl;dry[1][j]+=yr;if(rs){rev[0][j]+=yl*rs;rev[1][j]+=yr*rs}}
}
const hat=(t,a,open)=>noise(t,open?.3:.06,a,{hp:1,pan:open?.3:-.2,env:tt=>Math.exp(-tt*(open?13:85))});
const clap=(t,a)=>noise(t,.3,a*1.6,{bp:2100,bp2:1700,k:1.4,rs:.5,env:tt=>(tt<.03?(1-(tt*100%1))*.9:Math.exp(-(tt-.03)*22))});
const riser=(t,d,a)=>noise(t,d,a,{bp:250,bp2:9000,k:.25,rs:.4,env:(tt,u)=>u*u});
const crash=(t,a)=>{noise(t,2.2,a,{hp:1,rs:.6,pan:.1,env:tt=>Math.exp(-tt*2.2)});
note({b:dry,t,d:.1,f:48,a:a*1.6,w:2,dec:.45,rl:1.2,at:.002})};
const snare=(t,a)=>{noise(t,.14,a,{bp:2200,bp2:1800,k:1.2,rs:.3,env:tt=>Math.exp(-tt*34)});note({b:dry,t,d:.02,f:190,a:a*.6,w:2,dec:.05,rl:.08})};
const CH=[[33,[57,60,64]],[29,[53,57,60]],[36,[55,60,64]],[31,[55,59,62]]];
const AP=[0,1,2,3,2,1,4,2,0,1,2,5,2,1,3,2];
const MEL=[[[0,6,76],[6,2,74],[8,4,76],[12,4,81],[16,6,79],[22,2,76],[24,8,72]],
[[0,6,77],[6,2,76],[8,4,77],[12,4,81],[16,6,79],[22,2,77],[24,8,72]],
[[0,6,76],[6,2,74],[8,4,76],[12,4,79],[16,6,84],[22,2,83],[24,8,79]],
[[0,6,74],[6,2,71],[8,4,74],[12,4,79],[16,8,83],[24,4,81],[28,4,79]]];
// ping-pong delay (dotted eighth), reverb (4 combs + 2 all-passes a side), duck, soft clip: run over each finished
// stretch of samples, with the filter state carried on so tails cross from one stretch into the next
const D=(3*st*sr)|0,fo=(BARS*4*bt+.5)*sr;let fin=0;
const fx=[0,1].map(c=>({cb:[1557,1617,1491,1422].map(a=>({b:new Float32Array(((a+c*23)*sr/44100)|0),p:0,lp:0})),ap:[556,441].map(a=>({b:new Float32Array(((a+c*13)*sr/44100)|0),p:0}))}));
function finish(e){
const a=fin,o=[new Float32Array(e-a),new Float32Array(e-a)];
for(let i=Math.max(a,D);i<e;i++){del[0][i]+=del[1][i-D]*.42;del[1][i]+=del[0][i-D]*.42}
for(let c=0;c<2;c++){
const inp=rev[c],dl=del[c],dk=duck[c],d=dry[c],w=o[c];
for(const s of fx[c].cb){const buf=s.b,n=buf.length;let p=s.p,lp=s.lp;
for(let i=a;i<e;i++){const y=buf[p];lp=y*.72+lp*.28;buf[p]=inp[i]+dl[i]*.25+dk[i]*sc[i]*.12+lp*.86;if(++p==n)p=0;w[i-a]+=y}
s.p=p;s.lp=lp}
for(const s of fx[c].ap){const buf=s.b,n=buf.length;let p=s.p;
for(let i=a;i<e;i++){const b=buf[p],x=w[i-a];buf[p]=x+b*.5;w[i-a]=b-x*.5;if(++p==n)p=0}
s.p=p}
for(let i=a;i<e;i++){const x=d[i]+dk[i]*sc[i]+dl[i]*.5*(.4+.6*sc[i])+w[i-a]*.11;
w[i-a]=cal?x:Math.tanh(x*DR)*GN*Math.min(1,Math.max(0,(N-i)/(N-fo)))}
}
fin=e;emit(o[0],o[1],a,e==N);
}
for(let bar=0;bar<BARS;bar++){
const t0=bar*4*bt,sec=bar<8?0:bar<24?1:bar<40?2:bar<48?3:bar<52?4:bar<68?5:bar<84?6:7,tp=bar>=68?2:0;
let ci=(bar>>1)&3;if(sec==4)ci=3;if(sec==7)ci=0;
const root=CH[ci][0]+tp,tri=CH[ci][1].map(m=>m+tp+(sec==7&&m==60?1:0)),groove=sec==1||sec==2||sec==5||sec==6;
// drums
if(groove)for(let b=0;b<4;b++){if(bar==39&&b>1)continue;kick(t0+b*bt,.9)}
if(sec==0){kick(t0,.55);kick(t0+bt*.75,.3)}
if(sec==4&&bar<51)for(let b=0;b<4;b++)kick(t0+b*bt,.8);
if(groove)for(let s=0;s<16;s++){
hat(t0+s*st,[.12,.06,.22,.08][s&3]*(sec>2?1.2:1),0);
if((bar>=16||sec>1)&&(s&3)==2)hat(t0+s*st,.16,1);
if((bar>=16||sec>1)&&(s==4||s==12))clap(t0+s*st,.5);
if(sec==6&&(s&1)==0)noise(t0+s*st,.05,.05,{hp:1,pan:.5,env:tt=>Math.exp(-tt*60)});
}
if(sec==4){const div=bar<50?2:bar<51?4:8;for(let k=0;k<4*div;k++)snare(t0+k*bt/div,.12+.4*((bar-48)*4*div+k)/(16*div))}
if(bar==6||bar==38||bar==66||bar==22)riser(t0,8*bt,bar==22?.2:.35);
if(bar==48)riser(t0,16*bt,.5);
if(bar==8||bar==24||bar==52||bar==68||bar==84)crash(t0,.5);
if(bar==16||bar==32||bar==60||bar==76||bar==40)crash(t0,.25);
// bass
if(groove)for(let s=0;s<16;s++)if(s&3){if(bar==39&&s>7)continue;
note({b:duck,t:t0+s*st,d:st*.7,f:mtof(root+((s&3)==2?12:0)),a:.27,w:s&1,n:1,c0:sec>2?2800:1700,c1:130,cd:.07,q:.6,rl:.03})}
if(sec==0||sec==3||sec==4||sec==7)note({b:duck,t:t0,d:4*bt,f:mtof(sec==0?33:root),a:sec==7?.3:.22,w:2,at:.3,rl:.4});
// arp
if((bar>=4&&sec==0)||groove)for(let s=0;s<16;s++){
const ext=[tri[0],tri[1],tri[2],tri[0]+12,tri[1]+12,tri[2]+12],m=ext[AP[s]]+(sec==6&&(s&2)?12:0);
const cut=sec==0?300+(bar-4+s/16)*300:sec==1?2400+1200*Math.sin(bar*.4):sec==2?4500:sec==5?6000:8000;
note({b:dry,t:t0+s*st,d:st*.5,f:mtof(m),a:(s%3==0?.24:.16)*(sec==0?.6:1),n:2,det:.12,c0:cut,c1:350,cd:.11,q:.5,rl:.12,pan:(s&1?.5:-.5),ds:.45,rs:.25})}
// pads
if(!(bar&1)&&sec!=4||bar==48){
const d=(sec==4||sec==7?4:2)*4*bt,u=(bar-40)/8;
const c0=sec==0?180:sec==1?800:sec==2?1800:sec==3?400+u*2500:sec==4?3000:sec==5?3500:sec==6?6000:9000;
const c1=sec==0?420:sec==1?1100:sec==2?2400:sec==3?400+(u+.25)*2500:sec==4?11000:sec==5?4200:sec==6?7000:300;
for(const m of [tri[0]-12,tri[0],tri[1],tri[2],tri[1]+12])for(const p of [-.8,.8])
note({b:duck,t:t0,d:d,f:mtof(m),a:sec==3||sec==4?.085:sec==7?.1:.07,n:3,det:.16,c0,c1,cd:sec==7?2.5:d*.5,q:1,at:sec==7?.01:.25,rl:sec==7?2.5:.5,pan:p,rs:.5})}
// stabs
if(sec==2||sec==6)for(const s of [3,6,11,14])for(const m of tri)
note({b:duck,t:t0+s*st,d:st*.6,f:mtof(m+12),a:.1,n:2,det:.2,c0:7000,c1:900,cd:.06,q:.7,rl:.08,pan:m%2?.4:-.4,ds:.5,rs:.3});
// melody: bells in the breakdown, lead after the drop
if(!(bar&1)&&(sec==3||sec==5||sec==6))for(const [s,l,m] of MEL[ci]){
if(sec==3)note({b:dry,t:t0+s*st,d:.05,f:mtof(m+tp),a:.12,w:3,n:2,det:.05,c0:5000,c1:1200,cd:.4,dec:.5,rl:1.2,rs:.9,ds:.5,pan:s%8?.3:-.3});
else{note({b:duck,t:t0+s*st,d:l*st*.92,f:mtof(m+tp),a:.14,n:3,det:.14,c0:sec==6?9000:6500,c1:2200,cd:.3,q:.7,at:.01,rl:.15,rs:.4,ds:.45});
note({b:duck,t:t0+s*st,d:l*st*.92,f:mtof(m+tp-12),a:.06,w:1,c0:2500,c1:900,cd:.3,rl:.15,rs:.2});
if(sec==6)note({b:dry,t:t0+s*st,d:l*st*.5,f:mtof(m+tp+12),a:.035,w:3,n:2,det:.08,c0:8000,dec:.4,rl:.4,rs:.6,ds:.5,pan:.4})}}
// nothing later can still write before the next bar line, so everything up to it is final
finish(bar==BARS-1?N:((bar+1)*4*bt*sr)|0);
}
}
/*END*/
const BT=60/124,BARS=88,END=BARS*4*BT,CUTS=[0,8,24,40,52,68,84,88];
const NAMES=['I Pressure','II Vein','III Lattice','IV Geode','V Vault','VI Daylight'];
const $=s=>document.querySelector(s),Q=new URLSearchParams(location.search),SHOT=Q.get('shot'),DEMO=window.DEMO={fps:[],frames:0,done:false,err:[]};
const cv=$('canvas'),gl=cv.getContext('webgl2',{antialias:false,alpha:false,powerPreference:'high-performance',preserveDrawingBuffer:SHOT!=null});
const VS='#version 300 es\nvoid main(){gl_Position=vec4(vec2(gl_VertexID&1,gl_VertexID>>1)*4.-1.,0,1);}';
// shared by every scene
const HEAD=`#version 300 es
precision highp float;
uniform vec2 R;uniform float T,B,L,K;uniform vec4 DBG;
out vec4 O;
#define PI 3.14159265
#define sat(x) clamp(x,0.,1.)
vec2 tr;
mat2 rot(float a){float c=cos(a),s=sin(a);return mat2(c,-s,s,c);}
float hash(vec3 p){p=fract(p*.3183099+.1);p*=17.;return fract(p.x*p.y*p.z*(p.x+p.y+p.z));}
float noise(vec3 x){vec3 i=floor(x),f=fract(x);f=f*f*(3.-2.*f);
return mix(mix(mix(hash(i),hash(i+vec3(1,0,0)),f.x),mix(hash(i+vec3(0,1,0)),hash(i+vec3(1,1,0)),f.x),f.y),
mix(mix(hash(i+vec3(0,0,1)),hash(i+vec3(1,0,1)),f.x),mix(hash(i+vec3(0,1,1)),hash(i+vec3(1,1,1)),f.x),f.y),f.z);}
vec3 pal(float t){return .5+.5*cos(6.2832*(t+vec3(0,.33,.67)));}
float box(vec3 p,vec3 b){vec3 q=abs(p)-b;return length(max(q,0.))+min(max(q.x,max(q.y,q.z)),0.);}
float hexd(vec2 p){p=abs(p);return max(p.x*.866025+p.y*.5,p.y);}
float tri(float x){return abs(fract(x)-.5);}
vec3 env(vec3 d,float h){float s=.5+.5*sin(d.y*5.+sin(d.x*3.+T*.2)*2.+d.z*2.);return pal(d.y*.35+d.x*.2+T*.03+h)*(.1+pow(s,6.)*1.6);}
// iridescent metal: the hue turns with the viewing angle, like a thin film
vec3 crystal(vec3 n,vec3 rd,float x,float h,vec3 ld){
vec3 ir=pal(x+dot(n,rd)*.9+h),r=reflect(rd,n);float fr=pow(1.-sat(dot(n,-rd)),4.);
return ir*(.1+.55*sat(dot(n,ld)))+env(r,h)*mix(ir,vec3(1),fr*.7)*.9+pow(sat(dot(r,ld)),48.)*2.;
}
`;
const TAIL=`
void main(){
vec2 uv=(gl_FragCoord.xy*2.-R)/R.y;
vec3 ro,ta;float fov=1.6,roll=0.;cam(ro,ta,fov,roll);
vec3 f=normalize(ta-ro),r=normalize(cross(f,vec3(sin(roll),cos(roll),0))),u=cross(r,f),rd=normalize(uv.x*r+uv.y*u+fov*f);
float t=0.,d=0.,gw=0.;
t=map(ro)*hash(vec3(gl_FragCoord.xy,T))*.7;
for(int i=0;i<STEPS;i++){vec3 p=ro+rd*t;d=map(p);if(d<.0004+.0008*t||t>FAR)break;t+=d*SS;gw+=GLOW;}
vec3 bgc=bg(rd),col=bgc;
if(t<FAR&&d<.002+.003*t){
vec3 p=ro+rd*t;vec2 e=vec2(max(.0008,.0012*t),0);
d=map(p);vec2 m=tr;
vec3 n=normalize(vec3(map(p+e.xyy),map(p+e.yxy),map(p+e.yyx))-d);
float a=0.,s=1.;for(int i=1;i<5;i++){float h=AOS*float(i);a+=(h-map(p+n*h))*s/AOS;s*=.65;}
a=sat(1.-.55*a);a*=a;
col=shade(p,n,rd,t,m,a);
#ifdef REFL
if(m.y>.5){vec3 q=p+n*.03,rr=reflect(rd,n);float tt=.02,dd;
for(int i=0;i<40;i++){dd=map(q+rr*tt);if(dd<.004*tt||tt>FAR*.5)break;tt+=dd*SS;}
vec3 rc=bg(rr);
if(tt<FAR*.5){vec3 q2=q+rr*tt;dd=map(q2);vec2 m2=tr;
vec3 n2=normalize(vec3(map(q2+e.xyy),map(q2+e.yxy),map(q2+e.yyx))-dd);rc=mix(shade(q2,n2,rr,tt,m2,1.),rc,1.-exp(-FOG*tt*tt));}
col+=rc*(.25+.75*pow(1.-sat(dot(n,-rd)),3.))*REFL*a;}
#endif
col=mix(col,bgc,1.-exp(-FOG*t*t));
}
col+=(GCOL)*gw;
O=vec4(max(col,0.),1);
}`;
const SCENES=[
// I. Pressure: a twinned cube crystal grows in a hollow of hot rock
`#define STEPS 90
#define FAR 12.
#define SS .8
#define AOS .07
#define FOG .012
#define GLOW .0015/(1.+d*d*30.)
#define GCOL vec3(1.,.45,.15)*smoothstep(0.,20.,L)
float map(vec3 p){
float g=smoothstep(0.,30.,L),s=.2+.8*g+.04*K;
float rock=(4.6-length(p)-(noise(p*1.3)-.5)*1.7-(noise(p*4.)-.5)*.35)*.55;
vec3 q=p;q.xz*=rot(.6);q.xy*=rot(.4);
float c=box(q,vec3(s*.8));
q=p-vec3(.55,.35,.2)*s;q.yz*=rot(1.1);q.xz*=rot(.3);c=min(c,box(q,vec3(s*.55)));
q=p+vec3(.5,.4,-.3)*s;q.xy*=rot(.9);q.yz*=rot(.5);c=min(c,box(q,vec3(s*.45)));
tr=vec2(length(p)*.3,c<rock?1.:0.);
return min(c,rock);
}
void cam(out vec3 ro,out vec3 ta,out float fov,out float roll){
float v=floor(L/16.),a=L*.045+v*2.3,r=mix(3.5,2.5,v)-fract(L/16.)*.5;
ro=vec3(sin(a)*r,.6-v*1.1+sin(L*.05)*.3,cos(a)*r);ta=vec3(0);fov=1.8;roll=.12*sin(L*.07+v);
}
vec3 bg(vec3 rd){return vec3(0);}
vec3 shade(vec3 p,vec3 n,vec3 rd,float t,vec2 m,float a){
vec3 ld=normalize(vec3(.5,.8,.3)),e=pal(.03+p.y*.03+.05*sin(T*.3))*vec3(1.8,.7,.3);
float vein=pow(sat(1.-abs(noise(p*2.2)-.5)*16.),3.)*smoothstep(.35,.7,noise(p*.7+3.));
if(m.y<.5)return vec3(.03,.024,.03)*(.2+sat(dot(n,ld)))*a+e*vein*(1.2+4.*K)*smoothstep(-2.,14.,L);
return crystal(n,rd,m.x+T*.02,0.,ld)*a*smoothstep(-1.,14.,L)+e*.25*a*(.5+K);
}`,
// II. Vein: flight through an Apollonian fractal, metal veins lit from inside
`#define STEPS 120
#define FAR 22.
#define SS .85
#define AOS .03
#define FOG .012
#define GLOW .0012/(1.+d*d*900.)
#define GCOL pal(.6+B*.01)*(.6+2.*K)
float map(vec3 p){
float s=1.,o=1e9,an=1.12+.4*smoothstep(-.5,.5,sin(B*PI/24.));
for(int i=0;i<7;i++){p=-1.+2.*fract(.5*p+.5);float r2=dot(p,p);o=min(o,r2);float k=an/r2;p*=k;s*=k;}
tr=vec2(o,1.);
return .25*abs(p.y)/s;
}
void cam(out vec3 ro,out vec3 ta,out float fov,out float roll){
float v=floor(L/16.),t=L*.1+v*9.7;
ro=vec3(2.8*cos(.1+.33*t),.4+.3*cos(.37*t),2.8*cos(.5+.35*t));
ta=vec3(1.9*cos(1.2+.41*t),.4+.1*cos(.27*t),1.9*cos(2.+.38*t));
roll=.3*cos(.1*t);fov=1.3;
}
vec3 bg(vec3 rd){return pal(.7+B*.006)*.05;}
vec3 shade(vec3 p,vec3 n,vec3 rd,float t,vec2 m,float a){
vec3 ld=normalize(vec3(.6,.7,-.4));
float vein=pow(sat(1.-m.x*1.5),3.);
return crystal(n,rd,m.x*2.2+p.y*.5+p.x*.2+T*.04,.55,ld)*(.3+.7*a)*(.6+1.6*sat(m.x*2.))+pal(m.x*2.5+B*.02+.1)*vein*(.6+3.*K)*a;
}`,
// III. Lattice: cubic crystals on an endless grid, bonds carrying light on the beat
`#define STEPS 110
#define FAR 46.
#define SS .85
#define AOS .06
#define FOG .0011
#define GLOW 0.
#define GCOL vec3(0)
#define REFL .8
float map(vec3 p){
float v=floor(L/16.);
if(v>0.)p.xy*=rot(p.z*.03*sin(L*.07));
vec3 c=floor(p/2.+.5),q=p-c*2.;
float h=hash(c),h2=hash(c+7.3),bd=1.1-max(abs(q.x),abs(q.y));
if(c.x==0.&&c.y==0.){tr=vec2(0);return bd;}
bd=min(bd,1.1-abs(q.z));
float on=smoothstep(0.,2.,L-h2*20.),sz=(.16+.4*h)*on*(1.+.3*K);
vec3 r=q;if(v>1.){r.xy*=rot(B*(h-.5)*.8);r.yz*=rot(B*(h2-.5)*.8);}
float d=box(r,vec3(sz))-.012,l=min(length(q.xy),min(length(q.yz),length(q.xz)))-.011;
tr=vec2(h,d<l?1.:0.);
return min(min(d,l),bd);
}
void cam(out vec3 ro,out vec3 ta,out float fov,out float roll){
float v=floor(L/16.);
ro=vec3(.22*sin(L*.2),.22*cos(L*.17),L*(v>1.?1.9:1.1)+v*13.);
vec3 d=v==1.?vec3(.95,.25*sin(L*.2),.55):v==3.?vec3(.3*sin(L*.1),.25,-1.):vec3(.2*sin(L*.13),.15*cos(L*.1),1.);
ta=ro+d;roll=L*(v>1.?.05:.012);fov=v==1.?1.1:1.3;
}
vec3 bg(vec3 rd){return pal(.62+rd.y*.15+B*.004)*.035;}
vec3 shade(vec3 p,vec3 n,vec3 rd,float t,vec2 m,float a){
vec3 ld=normalize(vec3(.4,.8,-.5));
if(m.y<.5)return pal(m.x*.4+p.z*.03+T*.04)*(.12+4.*pow(.5+.5*sin(dot(p,vec3(1))*1.2-B*PI*.5),14.));
return (crystal(n,rd,m.x*1.7+T*.03,.1,ld)*mix(vec3(1.,.8,.45),vec3(1),.55)+pal(m.x*1.7+.2)*K*.25)*a;
}`,
// IV. Geode: a star of crystal prisms (icosahedral symmetry) in a faceted hollow
`#define STEPS 100
#define FAR 18.
#define SS .8
#define AOS .06
#define FOG .0025
#define GLOW .002/(.08+dot(p,p))
#define GCOL pal(B*.03)*.5+.5
vec3 ifold(vec3 p){const vec3 nc=vec3(-.5,-.809017,.309017);for(int i=0;i<5;i++){p.xy=abs(p.xy);p-=2.*min(0.,dot(p,nc))*nc;}return p;}
float spike(vec3 p,vec3 a,float r1,float w){
float h=dot(p,a);vec3 u=normalize(cross(a,vec3(.31,.95,.2))),v=cross(a,u);
vec2 q=abs(vec2(dot(p,u),dot(p,v)));
return max(max(q.x,q.y)-w,(q.x+q.y)*.5+(h-r1)*.55);
}
float map(vec3 p){
float bu=smoothstep(32.,48.,L);
float wall=(7.-length(p)-1.5*(tri(p.x*.45+tri(p.y*.4))+tri(p.y*.45+tri(p.z*.4))+tri(p.z*.45+tri(p.x*.4))))*.4;
p.xz*=rot(L*.05+bu*bu*7.);p.xy*=rot(L*.03);
vec3 f=ifold(p);
float e=1.+.6*bu+.08*sin(L*PI*.5)+.15*K;
float d=spike(f,normalize(vec3(0,.309017,.809017)),2.3*e,.17);
d=min(d,spike(f,normalize(vec3(.309017,0,.5)),1.55*e,.24));
d=min(d,spike(f,vec3(0,0,1),1.1*e,.13));
tr=vec2(length(p)*.3,1.);
if(wall<d)tr=vec2(length(p)*.1,0.);
return min(d*.8,wall);
}
void cam(out vec3 ro,out vec3 ta,out float fov,out float roll){
float v=min(floor(L/16.),2.),a=L*.06+v*2.1,r=v==0.?5.4-L*.07:v==1.?3.3:3.6+(L-32.)*.1;
ro=vec3(sin(a)*r,(v==1.?-1.2:.8)+sin(L*.08),cos(a)*r);ta=vec3(0,v==1.?.5:0.,0);fov=v==1.?1.2:1.5;roll=.2*sin(L*.05)+(v==2.?(L-32.)*.04:0.);
}
vec3 bg(vec3 rd){return vec3(0);}
vec3 shade(vec3 p,vec3 n,vec3 rd,float t,vec2 m,float a){
float bu=smoothstep(32.,48.,L),l=length(p);vec3 ld=-p/l;
if(m.y<.5){float di=sat(dot(n,ld));vec3 r=reflect(rd,n);
return pal(dot(p/l,vec3(.5,.3,.7))*1.3+dot(n,rd)*.6+T*.02)*(di*(.3+1.1*bu)+.02)*a+pow(sat(dot(r,ld)),40.)*(.5+2.*bu);}
return crystal(n,rd,m.x*1.6+T*.04,.3,normalize(vec3(.3,.8,.5)))*a+pal(l*.4-B*.05)*exp(-l*1.6)*(1.5+3.*bu);
}`,
// V. Vault: inside a Mandelbox
`#define STEPS 130
#define FAR 14.
#define SS .9
#define AOS .025
#define FOG .02
#define GLOW .0005/(1.+d*d*1500.)
#define GCOL pal(.85+B*.012)*(.8+2.5*K)
#define MS 2.8
float map(vec3 p){
vec4 q=vec4(p,1.),q0=q;float o=1e9;
for(int i=0;i<10;i++){
q.xyz=clamp(q.xyz,-1.,1.)*2.-q.xyz;
float r2=dot(q.xyz,q.xyz);o=min(o,r2);
q*=clamp(max(.25/r2,.25),0.,1.);
q=q*vec4(vec3(MS),abs(MS))/.25+q0;
}
tr=vec2(sqrt(o),1.);
return (length(q.xyz)-abs(MS-1.))/q.w-pow(abs(MS),-9.);
}
void cam(out vec3 ro,out vec3 ta,out float fov,out float roll){
float v=floor(L/16.),u=fract(L/16.);
vec3 a,b;
CAMS
ro=mix(a,b,u);ta=b+(b-a)*2.;fov=1.4;roll=.15*sin(L*.1+v*2.);
}
vec3 bg(vec3 rd){return pal(.62+rd.y*.1+B*.004)*.05;}
vec3 shade(vec3 p,vec3 n,vec3 rd,float t,vec2 m,float a){
vec3 ld=normalize(vec3(.5,.8,-.3));
float vein=pow(sat(1.-m.x*1.1),4.);
return crystal(n,rd,m.x*1.6+length(p)*.35+T*.03,.8,ld)*(.25+.75*a)*(.7+.9*sat(m.x*1.5))+pal(m.x*2.+B*.03+.5)*vein*(.6+3.*K)*a;
}`,
// VI. Daylight: out through the surface, a field of crystal spires under the sun
`#define STEPS 120
#define FAR 110.
#define SS .75
#define AOS .25
#define FOG .0005
#define GLOW 0.
#define GCOL vec3(0)
#define REFL .55
const vec3 SUN=normalize(vec3(.25,.3,1.));
float map(vec3 p){
float gr=p.y+.7*noise(vec3(p.xz*.22,1.))+.07*noise(vec3(p.xz*2.,3.));
p.xz+=p.y*vec2(.1,.04);
vec2 c=floor(p.xz/4.),q=p.xz-c*4.-2.;
float h=hash(vec3(c,7.)),h2=hash(vec3(c,3.)),bd=2.15-max(abs(q.x),abs(q.y));
float up=smoothstep(0.,1.,(L-h2*5.)*.6);up*=1.+.3*sin(up*PI);
float tl=sat((h-.72)/.28),H=(.5+tl*tl*22.*smoothstep(.5,3.5,abs(c.x+.5))+1.3*h2)*up*(1.+.03*K)-.8,w=.5+.45*h2;
q+=(vec2(h2,h)-.5);
float hx=hexd(q),pr=max(hx-w,(p.y-H)*.5+hx*.62);
tr=vec2(h,pr<gr?1.:0.);
return min(min(gr*.8,pr*.8),bd);
}
void cam(out vec3 ro,out vec3 ta,out float fov,out float roll){
float v=floor(L/16.),l=L-v*16.;fov=1.3;roll=0.;
if(v==0.){ro=vec3(.8*sin(L*.15),2.6+.5*sin(L*.2),L*2.4);ta=ro+vec3(.25*sin(L*.1),.08+.1*smoothstep(0.,6.,L),1.);roll=.15*sin(L*.12);}
else if(v==1.){ro=vec3(7.*sin(L*.06),23.+2.*sin(L*.1),L*2.);ta=ro+vec3(-.5*sin(L*.06),-.5,1.);fov=1.1;}
else if(v==2.){ro=vec3(-.2+.5*sin(L*.2),3.+.6*sin(L*.3),L*3.2);ta=ro+vec3(.75,.12,.7);roll=-.25;fov=1.;}
else{float k=l/16.;ro=vec3(0,3.+40.*k*k,L*2.4);ta=ro+mix(vec3(0,-.15,1),SUN,smoothstep(0.,.8,k));roll=.4*(1.-k)*sin(l*.3);}
ro.x-=ro.y*.1;
}
vec3 bg(vec3 rd){
float s=sat(dot(rd,SUN));
vec3 c=mix(vec3(1.,.6,.38),vec3(.12,.3,.85),sat(rd.y*1.7+.12));
c=mix(c,vec3(1.,.5,.75),.35*exp(-abs(rd.y)*7.));
return c+vec3(1.,.8,.55)*(pow(s,8.)*.5+pow(s,200.)*3.+pow(s,3000.)*40.);
}
vec3 shade(vec3 p,vec3 n,vec3 rd,float t,vec2 m,float a){
float sh=1.,st=.15;for(int i=0;i<22;i++){float d=map(p+n*.03+SUN*st);sh=min(sh,10.*d/st);st+=clamp(d,.08,2.5);if(sh<.01||st>40.)break;}
sh=sat(sh);float di=sat(dot(n,SUN))*sh;vec3 sky=vec3(.25,.4,.8)*(.5+.5*n.y)*a;
if(m.y<.5){float ca=pow(noise(vec3(p.xz*1.3,T*.4)),3.)*sh;
return vec3(.07,.06,.065)*(di*vec3(3.,2.5,2.)+sky)+pal(noise(vec3(p.xz*.4,2.))*2.+T*.05)*ca*.5;}
vec3 ir=pal(m.x*2.+dot(n,rd)*1.1+p.y*.06+T*.03),r=reflect(rd,n);
return ir*(.12+.75*di)+ir*sky*.6+vec3(1.,.9,.7)*pow(sat(dot(r,SUN)),60.)*6.*sh+ir*pow(1.-sat(dot(n,-rd)),2.)*.35*a;
}`];
// camera moves inside the Mandelbox: a to b over four bars each, looking at ta
const CAMS=Q.get('cam')?(c=>`a=vec3(${c.slice(0,3)});b=vec3(${c.slice(3)});`)(Q.get('cam').split(',')):`if(v==0.){a=vec3(-2.272,-2.045,.336);b=vec3(-1.855,-2.335,.306);}else if(v==1.){a=vec3(-1.188,1.809,-.12);b=vec3(-1.003,1.34,.472);}else if(v==2.){a=vec3(1.277,.452,-.186);b=vec3(.954,.167,-.616);}else{a=vec3(-1.705,.986,-1.8);b=vec3(-1.351,1.072,-.553);}`;
// bloom: 4-tap blur used to shrink and to grow
const BLUR=`#version 300 es
precision highp float;uniform sampler2D A;uniform vec2 P;uniform float H,G;out vec4 O;
void main(){vec2 u=gl_FragCoord.xy*P*G;vec4 c=texture(A,u+P*vec2(H,H))+texture(A,u+P*vec2(-H,H))+texture(A,u+P*vec2(H,-H))+texture(A,u-P*vec2(H,H));
c*=.25;O=vec4(min(c.rgb,vec3(5.)),1);}`;
const COMP=`#version 300 es
precision highp float;uniform sampler2D A,M;uniform vec2 R;uniform float F,W,D,T,CA;out vec4 O;
float h(vec2 p){return fract(sin(dot(p,vec2(12.9898,78.233))+T)*43758.5453);}
void main(){
vec2 u=gl_FragCoord.xy/R,c=u-.5;float ca=CA*(.3+dot(c,c));
vec3 col=vec3(texture(A,.5+c*(1.-ca)).r,texture(A,u).g,texture(A,.5+c*(1.+ca)).b);
col+=texture(M,u).rgb*.06;
col*=1.-.9*dot(c,c);
col+=F*vec3(1.,.96,.9);
col=mix(col,vec3(3.),W);
col=(col*(2.51*col+.03))/(col*(2.43*col+.59)+.14);
col=pow(sat3(col),vec3(.4545));
col+=(h(gl_FragCoord.xy)-.5)*.035*(1.-W);
O=vec4(col*(1.-D),1);
}`.replace('sat3(col)','clamp(col,0.,1.)');
function sh(type,src){const s=gl.createShader(type);gl.shaderSource(s,src);gl.compileShader(s);return s}
function prog(fs){
const p=gl.createProgram(),f=sh(gl.FRAGMENT_SHADER,fs);gl.attachShader(p,sh(gl.VERTEX_SHADER,VS));gl.attachShader(p,f);gl.linkProgram(p);
if(!gl.getProgramParameter(p,gl.LINK_STATUS)){const e=gl.getShaderInfoLog(f)||gl.getProgramInfoLog(p);DEMO.err.push(e);console.error(e);return null}
const u={};for(let i=0,n=gl.getProgramParameter(p,gl.ACTIVE_UNIFORMS);i<n;i++){const a=gl.getActiveUniform(p,i);u[a.name]=gl.getUniformLocation(p,a.name)}
return {p,u};
}
let P=[],PB,PC,HDR,W=0,H=0,scale=1,maxScale=1,tgt=null;
function target(w,h){
const t=gl.createTexture();gl.bindTexture(gl.TEXTURE_2D,t);
gl.texImage2D(gl.TEXTURE_2D,0,HDR?gl.RGBA16F:gl.RGBA8,w,h,0,gl.RGBA,HDR?gl.HALF_FLOAT:gl.UNSIGNED_BYTE,null);
for(const [k,v] of [[gl.TEXTURE_MIN_FILTER,gl.LINEAR],[gl.TEXTURE_MAG_FILTER,gl.LINEAR],[gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE],[gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE]])gl.texParameteri(gl.TEXTURE_2D,k,v);
const f=gl.createFramebuffer();gl.bindFramebuffer(gl.FRAMEBUFFER,f);gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,t,0);
return {t,f,w,h};
}
function build(){
if(tgt)for(const t of tgt){gl.deleteTexture(t.t);gl.deleteFramebuffer(t.f)}
let w=Math.max(16,Math.round(W*scale)),h=Math.max(16,Math.round(H*scale));tgt=[target(w,h)];
for(let i=0;i<6;i++){w=Math.max(2,w>>1);h=Math.max(2,h>>1);tgt.push(target(w,h))}
}
function resize(){
const d=Math.min(devicePixelRatio||1,2),w=Math.round(innerWidth*d),h=Math.round(innerHeight*d);
if(w==W&&h==H)return;W=cv.width=w;H=cv.height=h;
maxScale=Math.min(1,Math.sqrt(+(Q.get('px')||1e6)/(w*h)));scale=maxScale;build();
}
function draw(p,t,un){
gl.bindFramebuffer(gl.FRAMEBUFFER,t?t.f:null);gl.viewport(0,0,t?t.w:W,t?t.h:H);gl.useProgram(p.p);
for(const k in un){const v=un[k],l=p.u[k];if(l==null)continue;
if(v.t!==undefined){gl.activeTexture(gl.TEXTURE0+v.i);gl.bindTexture(gl.TEXTURE_2D,v.t);gl.uniform1i(l,v.i)}
else if(v.length==2)gl.uniform2f(l,v[0],v[1]);else if(v.length==4)gl.uniform4f(l,v[0],v[1],v[2],v[3]);else gl.uniform1f(l,v)}
gl.drawArrays(gl.TRIANGLES,0,3);
}
const els={ti:$('#ti'),su:$('#su'),sc:$('#sc'),dp:$('#dp')},last={};
function set(k,op,html,col){
const e=els[k];op=Math.round(Math.max(0,Math.min(1,op))*50)/50;
if(last[k+'o']!==op){e.style.opacity=last[k+'o']=op}
if(html!=null&&last[k+'h']!==html){e.innerHTML=last[k+'h']=html}
if(col&&last[k+'c']!==col){e.style.color=last[k+'c']=col}
}
const ss=(a,b,x)=>{x=Math.max(0,Math.min(1,(x-a)/(b-a)));return x*x*(3-2*x)};
const DBG=(Q.get('cam')||'').split(',').map(Number);
function render(t){
const beat=t/BT,bar=beat/4;let S=0;while(S<6&&bar>=CUTS[S+1])S++;
const L=beat-CUTS[S]*4,fb=beat-Math.floor(beat);
let K=0;
if((bar>=8&&bar<39.5)||(bar>=48&&bar<51)||(bar>=52&&bar<84))K=Math.exp(-fb*4.5);
else if(bar<8){const b=beat%4;K=.6*Math.exp(-b*4.5)+(b>.75?.35*Math.exp(-(b-.75)*4.5):0)}
// flashes: big on a new scene, small on each four-bar cut inside it
let F=S>0&&S<6?1.1*Math.exp(-L*1.1):0;if(S<6&&L>=16)F+=.3*Math.exp(-(L%16)*2.5);
if(S==3&&L>=40)F+=.25*Math.max(0,1-((L*4)%1)*3)*(L-40)/8;
const Wt=S==5?ss(55,64,L):S==6?1:0;
const D=S==0?1-ss(0,6,L):S==6?ss(8,15.5,L):0;
if(S<6&&P[S]){
draw(P[S],tgt[0],{R:[tgt[0].w,tgt[0].h],T:t,B:beat,L,K,DBG:DBG.length>2?[DBG[0],DBG[1],DBG[2],1]:[0,0,0,0]});
draw(PB,tgt[1],{A:{t:tgt[0].t,i:0},P:[1/tgt[0].w,1/tgt[0].h],H:1,G:2});
for(let i=2;i<tgt.length;i++)draw(PB,tgt[i],{A:{t:tgt[i-1].t,i:0},P:[1/tgt[i-1].w,1/tgt[i-1].h],H:1,G:2});
gl.enable(gl.BLEND);gl.blendFunc(gl.ONE,gl.ONE);
for(let i=tgt.length-1;i>1;i--)draw(PB,tgt[i-1],{A:{t:tgt[i].t,i:0},P:[1/tgt[i].w,1/tgt[i].h],H:.75,G:.5});
gl.disable(gl.BLEND);
}
draw(PC,null,{A:{t:tgt[0].t,i:0},M:{t:tgt[1].t,i:1},R:[W,H],F,W:Wt,D,T:t%10,CA:.012+.02*K+.05*F});
// words
const end=S==6;
set('ti',end?ss(1,4,L)*(1-ss(9,14,L)):S==0?ss(14,18,L)*(1-ss(27,31,L)):0,null,end?'#111':'#fff');
set('su',end?ss(3,6,L)*(1-ss(9,13,L)):0,'from the deep to the light · thank you for watching','#333');
set('sc',S<6?ss(S?0:4,S?1:8,L)*(1-ss(7,9,L))*.9:0,NAMES[Math.min(S,5)]);
const dep=bar<68?2900*(1-Math.pow(bar/68,1.6)):-Math.pow(bar-68,2)*11;
set('dp',S<6?ss(2,8,beat)*.75*(1-Wt):0,(dep>=0?'depth ':'altitude ')+String(Math.round(Math.abs(dep))).padStart(4,'0')+' m');
}
let ctx,SR=44100,t0=0,off=0,playing=false,lastNow=0,acc=0,cnt=0,slow=0,lastS=-1;
// the soundtrack arrives in pieces while it is being made: chunks holds them, gen is how many seconds exist so far
let chunks=[],srcs=[],gen=0,genDone=false,genT=0;
function frame(now){
if(!playing)return;
let t=ctx.currentTime-t0;
if(!genDone){if(t>gen)t=gen;DEMO.lead=Math.min(DEMO.lead,gen-t)} // never show a moment that has no sound yet
if(t>=END+.3){stop();return}
resize();render(Math.max(0,t));DEMO.frames++;DEMO.t=t;
// keep 60 fps: shrink the picture when frames run slow, try growing it at each new scene
const dt=now-lastNow;lastNow=now;
if(dt<200){acc+=dt;cnt++;if(dt>21)slow++}
if(acc>=1000){const fps=cnt*1000/acc;DEMO.fps.push(Math.round(fps));
if(slow>cnt*.3&&scale>.3){scale*=.84;build()}acc=cnt=slow=0}
const S=CUTS.findIndex((c,i)=>t/BT/4<CUTS[i+1]);
if(S!=lastS){if(lastS>=0&&scale<maxScale){scale=Math.min(maxScale,scale*1.19);build()}lastS=S}
requestAnimationFrame(frame);
}
function stop(){
playing=false;DEMO.done=true;for(const s of srcs)try{s.stop()}catch(e){}
srcs=[];
for(const k in els)set(k,0);gl.bindFramebuffer(gl.FRAMEBUFFER,null);gl.clearColor(0,0,0,1);gl.clear(gl.COLOR_BUFFER_BIT);
$('#go').textContent='again';$('#ui').style.display='';document.body.classList.remove('play');
}
// One piece of the soundtrack is queued at its exact sample position, so pieces join with no gap or overlap.
function sched(c){
const o=Math.round(off*SR);if(c.e<=o)return;
const n=ctx.createBufferSource();n.buffer=c.b;n.connect(ctx.destination);
let when=t0+Math.max(c.a,o)/SR;
if(when<ctx.currentTime+.03){const d=Math.ceil((ctx.currentTime+.08-when)*SR)/SR;t0+=d;when+=d;DEMO.late++} // the making fell behind: hold, then go on in sync
n.start(when,Math.max(0,o-c.a)/SR);srcs.push(n);
}
function piece(m){
const b=ctx.createBuffer(2,m.l.length,SR);b.copyToChannel(m.l,0);b.copyToChannel(m.r,1);
const c={b,a:m.a,e:m.a+m.l.length};chunks.push(c);gen=c.e/SR;genDone=m.last;DEMO.gen=gen;
if(playing)sched(c);
// Start comes up once 8 seconds exist, or more on a machine too slow to stay ahead (the later bars cost about twice the first)
const r=gen/((performance.now()-genT)/1000)*.5;
if(!DEMO.ready&&(genDone||gen>=Math.max(off+8,END*(1-.8*r)))){const g=$('#go');g.disabled=false;g.textContent='start';g.onclick=start;DEMO.ready=true;DEMO.readyAt=performance.now();DEMO.rate=r*2}
if(genDone)DEMO.genMs=performance.now()-genT;
}
function start(){
if(playing)return;
ctx.resume();
t0=Math.ceil((ctx.currentTime+.1)*SR)/SR-off;srcs=[];for(const c of chunks)sched(c);
$('#ui').style.display='none';document.body.classList.add('play');
playing=true;DEMO.done=false;DEMO.lead=1e9;DEMO.late=0;lastS=-1;lastNow=performance.now();requestAnimationFrame(frame);
}
function init(){
if(!gl){$('#go').textContent='needs WebGL 2';return}
HDR=!!gl.getExtension('EXT_color_buffer_float');gl.getExtension('OES_texture_float_linear');
PB=prog(BLUR);PC=prog(COMP);
for(let i=0;i<6;i++)P.push(prog(HEAD+SCENES[i].replace('CAMS',CAMS)+TAIL));
resize();
if(SHOT!=null){DEMO.shot=t=>{resize();render(+t);gl.finish();return true};$('#ui').style.display='none';DEMO.shot(SHOT);DEMO.ready=true;return}
// warm every shader so no scene stutters on its first frame
const s=scale;scale=.05;build();for(const c of CUTS.slice(0,6))render(c*4*BT+1);scale=s;build();
for(const k in els)set(k,0);gl.bindFramebuffer(gl.FRAMEBUFFER,null);gl.clearColor(0,0,0,1);gl.clear(gl.COLOR_BUFFER_BIT);
// the soundtrack is made off the main thread, at the sample rate the sound card runs at
ctx=new (window.AudioContext||window.webkitAudioContext)({sampleRate:44100});SR=ctx.sampleRate;
off=Math.round(Math.max(0,Math.min(END-1,+Q.get('t')||0))*SR)/SR;genT=performance.now();
const here=()=>setTimeout(()=>synth(SR,(l,r,a,last)=>piece({l,r,a,last})),60);
try{const w=new Worker(URL.createObjectURL(new Blob(['onmessage=e=>('+synth+')(e.data,(l,r,a,last)=>{for(const t=Date.now()+'+(+Q.get('slow')||0)+';Date.now()<t;);postMessage({l,r,a,last},[l.buffer,r.buffer])})'])));
w.onmessage=e=>piece(e.data);w.onerror=()=>chunks.length||here();w.postMessage(SR)}
catch(e){here()}
}
addEventListener('keydown',e=>{if(e.key=='f'||e.key=='F'){document.fullscreenElement?document.exitFullscreen():document.documentElement.requestFullscreen&&document.documentElement.requestFullscreen()}
else if((e.key==' '||e.key=='Enter')&&!playing&&DEMO.ready)start()});
init();
</script>
</html>