← GERMINAL v1
index.html
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>GERMINAL</title>
<style>
html,body{margin:0;height:100%;background:#000;overflow:hidden;color:#fff;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased}
canvas{position:fixed;inset:0;width:100%;height:100%;display:block}
#o{position:fixed;pointer-events:none;color:#fff;font-size:10px}
#o.i{color:#000}
#o div{position:absolute;white-space:nowrap}
#fr{inset:2.2em;border:1px solid;opacity:.28}
#ti{left:0;right:0;top:50%;margin-top:1.15em;text-align:center;font-weight:200;font-size:7.4em;letter-spacing:.46em;text-indent:.46em;opacity:0;transition:opacity 1.6s}
#su{left:0;right:0;top:50%;margin-top:18.5em;text-align:center;font-size:1.05em;letter-spacing:.62em;text-indent:.62em;text-transform:uppercase;opacity:0;transition:opacity 1.6s}
#pl{left:3.6em;bottom:3.4em;font-size:1.15em;letter-spacing:.4em;text-transform:uppercase}
#pl b{font-weight:400;color:#ff1c00}
#st{right:3.6em;top:3.4em;font:1.05em/1.5 ui-monospace,Menlo,Consolas,monospace;letter-spacing:.14em;text-align:right;opacity:.75}
#tc{right:3.6em;bottom:3.4em;font:1.05em ui-monospace,Menlo,Consolas,monospace;letter-spacing:.14em;opacity:.75}
#s{position:fixed;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#000;cursor:pointer;text-align:center}
#s h1{font-weight:200;font-size:min(9vw,84px);letter-spacing:.46em;text-indent:.46em;margin:0 0 .5em}
#s p{font-size:min(3.2vw,13px);letter-spacing:.5em;text-indent:.5em;text-transform:uppercase;margin:.5em 1em;opacity:.7}
#bd{position:absolute;right:14px;bottom:10px;font:10px ui-monospace,Menlo,monospace;letter-spacing:.12em;opacity:.35}
#s i{display:block;width:9px;height:9px;border-radius:9px;background:#ff1c00;margin:0 auto 2.6em;animation:p 1.25s infinite}
@keyframes p{0%,30%{transform:scale(1)}8%{transform:scale(1.9)}18%{transform:scale(1.4)}}
</style>
<canvas id="c"></canvas>
<div id="o"><div id="fr"></div><div id="ti">GERMINAL</div><div id="su">a growth in six plates</div><div id="pl"></div><div id="st"></div><div id="tc"></div></div>
<div id="s"><i></i><h1>GERMINAL</h1><p id="sm">click or press a key to plant the seed</p><p>sound on · F for fullscreen</p><div id="bd">build 4</div></div>
<script>
"use strict";
/*SYNTH*/
function synth(sr,dry,cb){
const BT=.625,ST=BT/4,BAR=2.5,NB=78,N=dry?1:Math.ceil(NB*BAR*sr)+1,TAU=6.283185307;
const F=()=>new Float32Array(N);
const L=F(),R=F(),DL=F(),DR=F(),VL=F(),VR=F(),PL=F(),PR=F(),SC=F();
let sd=20261;const rnd=()=>(sd=(sd*1664525+1013904223)>>>0)/4294967296;
const hz=n=>440*2**((n-69)/12);
const kicks=[],snares=[];
const kick=(t,a)=>{kicks.push(t);if(dry)return;const i0=t*sr|0;let ph=0;for(let i=0,n=sr*.5|0;i<n&&i0+i<N;i++){const x=i/sr;ph+=(44+120*Math.exp(-x*30))/sr;let v=Math.sin(TAU*ph)*Math.exp(-x*6.5)*a;if(i<80)v+=(rnd()-.5)*.6*a*(1-i/80);v=Math.tanh(v*1.5)*.8;L[i0+i]+=v;R[i0+i]+=v;const e=a*Math.exp(-x*8);if(e>SC[i0+i])SC[i0+i]=e}};
const snare=(t,a)=>{if(a>.5)snares.push(t);if(dry)return;const i0=t*sr|0;let lp=0;for(let i=0,n=sr*.3|0;i<n&&i0+i<N;i++){const x=i/sr,w=rnd()*2-1;lp+=(w-lp)*.25;const v=((w-lp)*Math.exp(-x*20)*.55+Math.sin(TAU*(180*x+8*Math.exp(-x*50)))*Math.exp(-x*28)*.5)*a;L[i0+i]+=v*.7;R[i0+i]+=v*.7;VL[i0+i]+=v*.3;VR[i0+i]+=v*.3}};
const hat=(t,a,dc,pan)=>{if(dry)return;const i0=t*sr|0;let p=0;for(let i=0,n=sr*6/dc|0;i<n&&i0+i<N;i++){const w=rnd()*2-1,v=(w-p)*.5*Math.exp(-i/sr*dc)*a;p=w;L[i0+i]+=v*(1-pan);R[i0+i]+=v*(1+pan)}};
const fm=(t,note,a,pan,dec,ratio,idx,dly,rev)=>{if(dry)return;const f=hz(note),i0=t*sr|0,n=Math.min(sr*7/dec|0,N-i0),gl=Math.sqrt(.5-.5*pan)*a,gr=Math.sqrt(.5+.5*pan)*a;for(let i=0;i<n;i++){const x=i/sr,v=Math.sin(TAU*f*x+idx*Math.exp(-x*dec*1.5)*Math.sin(TAU*f*ratio*x))*Math.exp(-x*dec)*Math.min(1,i/60),l=v*gl,r=v*gr,j=i0+i;L[j]+=l;R[j]+=r;DL[j]+=l*dly;DR[j]+=r*dly;VL[j]+=l*rev;VR[j]+=r*rev}};
const bass=(t,dur,note,a,gr)=>{if(dry)return;const f=hz(note),i0=t*sr|0,n=Math.min((dur+.08)*sr|0,N-i0);let p1=0,p2=0,lp=0;for(let i=0;i<n;i++){const x=i/sr,env=Math.min(1,x*200)*Math.min(1,(dur+.08-x)*14);let v=Math.sin(TAU*f*x)+.4*Math.sin(TAU*2*f*x);if(gr){p1+=f*2.014/sr;p2+=f*1.986/sr;p1-=p1|0;p2-=p2|0;lp+=(p1+p2-1-lp)*.1;v+=lp*gr*2}v=Math.tanh(v*1.3)*env*a;PL[i0+i]+=v;PR[i0+i]+=v}};
const pad=(t,dur,notes,a,c0,c1)=>{if(dry)return;const i0=t*sr|0,at=.9,rl=1.8,n=Math.min((dur+rl)*sr|0,N-i0),m=notes.length*3,ph=new Float64Array(m),dt=new Float64Array(m);notes.forEach((nt,j)=>{for(let k=0;k<3;k++){dt[j*3+k]=hz(nt+(k-1)*.1)/sr;ph[j*3+k]=rnd()}});let l1=0,b1=0,l2=0,b2=0;
for(let i=0;i<n;i++){const x=i/sr;let sl=0,sq=0;for(let j=0;j<m;j++){let p=ph[j]+dt[j];if(p>=1)p-=1;ph[j]=p;const d=dt[j];let v=2*p-1;if(p<d){const q=p/d;v-=q+q-q*q-1}else if(p>1-d){const q=(p-1)/d;v-=q*q+q+q+1}const k=j%3;if(k==0)sl+=v;else if(k==2)sq+=v;else{sl+=v*.7;sq+=v*.7}}
const env=Math.min(1,x/at)*Math.min(1,Math.max(0,(dur+rl-x)/rl)),fc=c0+(c1-c0)*Math.min(1,x/dur),f=2*Math.sin(Math.PI*fc/sr);l1+=f*b1;b1+=f*(sl-l1-.9*b1);l2+=f*b2;b2+=f*(sq-l2-.9*b2);const g=env*a/m*3,j=i0+i;PL[j]+=l1*g;PR[j]+=l2*g;VL[j]+=l1*g*.5;VR[j]+=l2*g*.5}};
const lead=(t,dur,note,a)=>{if(dry)return;const f=hz(note),i0=t*sr|0,n=Math.min((dur+.4)*sr|0,N-i0);let ph=0;for(let i=0;i<n;i++){const x=i/sr;ph+=f*(1+.006*Math.sin(TAU*5.2*x)*Math.min(1,x*2))/sr;const env=Math.min(1,x*40)*Math.min(1,Math.max(0,(dur+.4-x)/.4)),v=(Math.sin(TAU*ph+.9*Math.sin(TAU*ph*2))+.5*Math.sin(TAU*ph*1.006))*env*a,j=i0+i;L[j]+=v*.5;R[j]+=v*.5;DL[j]+=v*.35;DR[j]+=v*.35;VL[j]+=v*.3;VR[j]+=v*.3}};
const noiz=(t,dur,a,f0,f1,fall)=>{if(dry)return;const i0=t*sr|0,n=Math.min(dur*sr|0,N-i0);let l=0,b=0;for(let i=0;i<n;i++){const x=i/n,f=2*Math.sin(Math.PI*f0*(f1/f0)**x/sr),w=rnd()*2-1;l+=f*b;b+=f*(w-l-.4*b);const v=b*(fall?(1-x)*(1-x):x*x)*a,j=i0+i;L[j]+=v;R[j]+=v;VL[j]+=v*.5;VR[j]+=v*.5}};
const boom=(t,a)=>{if(dry)return;const i0=t*sr|0;for(let i=0,n=sr*2;i<n&&i0+i<N;i++){const x=i/sr,v=Math.sin(TAU*(38*x+25*(1-Math.exp(-x*6))/6))*Math.exp(-x*2.2)*a*Math.min(1,i/40);L[i0+i]+=v;R[i0+i]+=v}};
const hum=(t,dur,a)=>{if(dry)return;const i0=t*sr|0,n=Math.min(dur*sr|0,N-i0);let p1=0,p2=0,p3=0,lp=0;for(let i=0;i<n;i++){const x=i/sr;p1+=36.71/sr;p2+=73.9/sr;p3+=110.4/sr;p1-=p1|0;p2-=p2|0;p3-=p3|0;lp+=((p1+p2+p3)*2/3-1-lp)*.07;const v=lp*(.6+.4*Math.sin(TAU*23*x)*Math.sin(TAU*.31*x))*Math.min(1,x/2)*Math.min(1,(dur-x)/2)*a,j=i0+i;PL[j]+=v;PR[j]+=v;VL[j]+=v*.3;VR[j]+=v*.3}};
const crackle=(t,dur,a)=>{if(dry)return;for(let k=0,m=dur*9|0;k<m;k++){const i0=(t+rnd()*dur)*sr|0,g=a*rnd()*rnd(),pn=rnd();for(let i=0;i<30&&i0+i<N;i++){const v=(rnd()-.5)*g*(1-i/30);L[i0+i]+=v*pn;R[i0+i]+=v*(1-pn)}}};
const dd=Math.round(ST*3*sr),da=F(),db=F();let fe=0,fx_=0,fy=0,fd=0;
const mkrev=(X,Y,off)=>{const cl=[1423,1543,1657,1777,1301,1481].map(n=>Math.round((n+off)*sr/44100)),cb=cl.map(n=>new Float32Array(n)),ci=[0,0,0,0,0,0],lp=[0,0,0,0,0,0],al=[547,439,149].map(n=>Math.round((n+off)*sr/44100)),ab=al.map(n=>new Float32Array(n)),ai=[0,0,0];
return(i0,i1)=>{for(let i=i0;i<i1;i++){const x=X[i];let s=0;for(let k=0;k<6;k++){const b=cb[k],j=ci[k],y=b[j];lp[k]+=(y-lp[k])*.55;b[j]=x+lp[k]*.86;ci[k]=j+1==cl[k]?0:j+1;s+=y}s*=.16;for(let k=0;k<3;k++){const b=ab[k],j=ai[k],y=b[j];b[j]=s+y*.5;s=y-s*.5;ai[k]=j+1==al[k]?0:j+1}Y[i]+=s*.8}}};
const rvL=dry?0:mkrev(VL,L,0),rvR=dry?0:mkrev(VR,R,31);
// everything before sample `to` is final once the notes that start before it are written: run the effects up to there
const fx=to=>{if(dry)return;to=Math.min(N,Math.round(to));if(to<=fd)return;
for(let i=fd;i<to;i++){fe+=(SC[i]-fe)*.012;const g=1-.65*fe;L[i]+=PL[i]*g;R[i]+=PR[i]*g}
for(let i=fd;i<to;i++){const fl=i>=dd?db[i-dd]:0,fr=i>=dd?da[i-dd]:0;fx_+=(fl-fx_)*.5;fy+=(fr-fy)*.5;da[i]=DL[i]+fx_*.52;db[i]=DR[i]+fy*.52;const dl=i>=dd?da[i-dd]:0,dr=i>=dd?db[i-dd]:0;L[i]+=dl*.8;R[i]+=dr*.8;VL[i]+=dl*.25;VR[i]+=dr*.25}
rvL(fd,to);rvR(fd,to);
for(let i=fd;i<to;i++){L[i]=Math.tanh(L[i]*MG)*.96;R[i]=Math.tanh(R[i]*MG)*.96}
if(cb)cb(fd,to,L,R);fd=to};
const CH={d:[50,57,60,64,65],b:[46,53,57,60,62],g:[43,50,53,58,62],a:[45,52,55,60,64],f:[41,48,53,57,64],A:[45,52,55,61,64]},RT={d:38,b:34,g:31,a:33,f:41,A:33};
const PG=['ddbgda','ddbgda','dfbgdA','bbggfA','dfbagA','dfbagA'];
const KP=[0,['x.....x...x.....','x..x......xx....'],['x......x..x.....','x.....x..x....x.'],['x.........x.....','x.........x..x..'],['x.....x...x.....','x..x..x...x.....'],['x.....x...x..x..','x..x..x...x...x.']];
const SP=[0,['....x.......x...','....x.......x..o'],['....x.......x...','....x......ox...'],['........x.......','........x.....o.'],['....x.......x...','....x..o....x...'],['....x.......x..o','....x..o....x.oo']];
const AR=[0,2,4,1,3,2,4,3],CUT=[520,900,1400,800,1800,2500],PA=[.5,.5,.55,.6,.6,.65],DEN=[0,.45,.7,.3,.6,.85],RAT=[1,1,2,3.5,1,2];
const LD=[[0,2,69],[2,1.5,74],[3.5,.5,72],[4,3,76],[7,1,74],[8,2,72],[10,2,69],[12,1,67],[13,3,69],[16,2,74],[18,1.5,77],[19.5,.5,76],[20,3,74],[23,1,72],[24,2,76],[26,2,72],[28,1,67],[29,3,69],[32,2,70],[34,2,74],[36,3,79],[39,1,77],[40,2,76],[42,2,73],[44,4,69]];
for(let s=0;s<6;s++){
if(s){boom(s*12*BAR,.75);noiz(s*12*BAR,1.6,.22,5000,500,1)}
if(s==3)hum(36*BAR,12*BAR,.5);
if(s==0||s==3)crackle(s*12*BAR,(s?4:12)*BAR,.25);
if(s>3)for(const n of LD){lead((s*12)*BAR+n[0]*BT,n[1]*BT,n[2],.2);if(s==5)lead(60*BAR+n[0]*BT,n[1]*BT,n[2]+12,.07)}
for(let c=0;c<6;c++){const ch=PG[s][c],t=(s*12+c*2)*BAR,nt=CH[ch],rt=RT[ch];
pad(t,2*BAR,nt,PA[s],CUT[s]*.6,CUT[s]*(1+.3*(c%2)));
for(let b=0;b<2;b++){const tb=t+b*BAR,bar=s*12+c*2+b,last=bar%12==11,quiet=s==0||(s==3&&bar<40);
if(quiet){if(bar>=4){kick(tb,.42);kick(tb+3*ST,.3)}if(s==3)bass(tb,BAR-.1,rt+12,.28,0);
for(const st of[0,6,11])if(rnd()<.55)fm(tb+st*ST,nt[rnd()*5|0]+24,.2,rnd()-.5,1.6,3.5,1.5,.5,.5)}
else{const kp=KP[s][b],sp=SP[s][b];
for(let i=0;i<16;i++){const ti=tb+i*ST,fill=last&&i>=8;
if(kp[i]=='x'&&!fill){kick(ti,1);let j=i+1;while(j<16&&kp[j]!='x'&&j<i+4)j++;bass(ti,(j-i)*ST-.03,rt+(rt<36?12:0),.5,s==5?.5:0)}
if(fill){snare(ti,.35+.07*(i-8));if(i>11)snare(ti+ST/2,.3+.07*(i-8))}
else if(sp[i]=='x')snare(ti,.9);else if(sp[i]=='o')snare(ti,.3);
const hp=i%2?[0,.3,.5,.4,.5,.9][s]:.85;
if(rnd()<hp){const ha=.1+.12*rnd(),pn=(rnd()-.5)*.8;if(s>1&&rnd()<.14){hat(ti,ha,70,pn);hat(ti+ST/3,ha*.8,80,pn);hat(ti+ST*2/3,ha*.6,90,pn)}else if(i==14&&rnd()<.4)hat(ti,ha,11,pn);else hat(ti,ha,60,pn)}
if(s>1&&rnd()<.1)fm(ti,nt[rnd()*5|0]+36,.07,rnd()*2-1,28,2.7,4,.6,.2);
if(rnd()<DEN[s])fm(ti,nt[AR[(i+bar*3)%8]]+12+(rnd()<.2?12:0),.13*(i%4?1:1.35),Math.sin(i*1.7)*.7,s==3?3:7,RAT[s],2.5,.45,.25)}
if(i14(bar))bass(tb+14*ST,2*ST-.03,rt+12,.4,0)}
if(last)noiz(tb,BAR,.2,300,7000,0)}fx((t+2*BAR)*sr)}}
function i14(b){return b%4==3&&b%12!=11}
pad(180,3*BAR,CH.d,.6,400,1500);boom(180,.8);noiz(180,2,.2,6000,300,1);crackle(180,10,.25);
for(let b=0;b<4;b++){kick(180+b*BAR,.42);kick(180+b*BAR+3*ST,.3);fm(180+b*BAR+6*ST,[74,69,77,81][b],.2,b%2-.5,1.4,3.5,1.5,.5,.5)}
kick(190,.5);
kicks.sort((a,b)=>a-b);
if(dry)return{kicks,snares};
fx(N);
return{L,R,kicks,snares,N}}
const MG=.72;
/*END*/
const VS=`#version 300 es
void main(){gl_Position=vec4(vec2(gl_VertexID&1,gl_VertexID>>1)*4.-1.,0,1);}`;
const FS=`#version 300 es
precision highp float;
uniform vec2 uR;uniform float uT,uG,uK,uSym,uTilt,uTw,uMir,uKs,uCore,uW,uWa,uFr,uNeg,uFog,uFov,uRoll,uBr;
uniform int uM;uniform vec3 uRo,uTa,uUp,uL;
out vec4 o;
float gE,gH;
mat2 rot(float a){float c=cos(a),s=sin(a);return mat2(c,-s,s,c);}
float h3(vec3 p){return fract(sin(dot(p,vec3(127.1,311.7,74.7)))*43758.5453);}
float smin(float a,float b,float k){float h=max(k-abs(a-b),0.)/k;return min(a,b)-h*h*k*.25;}
vec2 toff(float z){return vec2(sin(z*.13),cos(z*.09))*1.1;}
float tree(vec3 p,float G,float sym,float tilt,float tw,float mir,float ks,float core){
float s=1.,d0=length(p)-.4-.03*uK*core,d=d0,em=0.;
if(mir>.5)p.y=abs(p.y);
float ct=cos(tilt),st=sin(tilt),w=6.2832/sym;
for(int i=0;i<7;i++){
float fi=float(i),g=clamp(G-fi,0.,1.);
if(g<=0.)break;
g=g*g*(3.-2.*g);
float a=atan(p.z,p.x);a=mod(a+.5*w,w)-.5*w;
p.xz=length(p.xz)*vec2(cos(a),sin(a));
vec3 tip=vec3(st,ct,0)*g;
float h=clamp(dot(p,tip)/dot(tip,tip),0.,1.);
float c=min(length(p-tip*h)-.13*(1.-.45*h),length(p-tip)-.15*g)/s;
float ei=uFr*(1.-smoothstep(.5,1.5,G-fi))*smoothstep(.15,.9,h)+uWa*exp(-(fi+h-uW)*(fi+h-uW)*7.);
if(c<d)em=ei;
d=smin(d,c,.14/s);
p-=tip;p.xy=vec2(p.x*ct-p.y*st,p.x*st+p.y*ct);p.xz*=rot(tw);p*=ks;s*=ks;
}
gE=clamp(em,0.,1.);gH=core*exp(-max(d0,0.)*5.)*2.4+gE*exp(-d*20.);
return d;}
float lattice(vec3 q){
vec3 p=mod(q+2.,4.)-2.;
float s=1.,d=1e9,em=0.;
for(int i=0;i<5;i++){
float fi=float(i),g=clamp(uG-fi,0.,1.);
if(g<=0.)break;
p=abs(p);
if(p.x<p.y)p.xy=p.yx;if(p.x<p.z)p.xz=p.zx;if(p.y<p.z)p.yz=p.zy;
float c=(length(vec3(p.x-clamp(p.x,0.,2.*g),p.yz))-.17)/s;
c=min(c,(length(p-vec3(2.*g,0,0))-.3*g)/s);
if(c<d)em=(1.-smoothstep(.2,1.,uG-fi))*smoothstep(2.*g-.7,2.*g,p.x);
d=smin(d,c,.2/s);
p.x-=2.;p.yz*=rot(uTw);p*=2.;s*=2.;
}
float pz=q.z-uW;
gE=clamp(em+uWa*exp(-pz*pz*3.),0.,1.);gH=gE*exp(-d*14.);
d=max(d,.3-length(q.xy-2.-vec2(.3*sin(q.z*.35),.25*cos(q.z*.27))));
return d*.85;}
float hive(vec3 p){
p.xy-=toff(p.z);
float r=length(p.xy),a=atan(p.y,p.x),n=14.,w=6.2832*3./n,h=3.-r,S=1.7320508;
vec2 q=vec2(a*3.,p.z)/w;
float d=h,s=1.,hot=0.,dl=1e9;
for(int i=0;i<3;i++){
vec2 A=q-vec2(floor(q.x+.5),floor(q.y/S+.5)*S),q2=q-vec2(.5,S*.5),B=q2-vec2(floor(q2.x+.5),floor(q2.y/S+.5)*S);
vec2 l=dot(A,A)<dot(B,B)?A:B,id=q-l;
float e=.5-max(abs(l.x),abs(l.x)*.5+abs(l.y)*S*.5);
float a1=e*w/s-.05/s,b1=h-.62/s;
d=min(d,length(max(vec2(a1,b1),0.))+min(max(a1,b1),0.));
if(i==0){
id.x=mod(id.x+.25,n);
float rr=h3(vec3(floor(id*4.+.5),3.));
float gr=clamp((uG*1.25-rr)*5.,0.,1.);
dl=length(vec3(l*w,h-.1-.15*gr))-(.07+.2*gr*(1.+.2*uK));
hot=gr;
}
q=l*3.;s*=3.;
}
d=min(d,dl);
gE=hot*exp(-max(dl,0.)*5.);gH=hot*exp(-max(dl,0.)*7.)*1.6;
return d*.75;}
float colony(vec3 p){
vec3 id=floor(p/7.);p=mod(p,7.)-3.5;
float r=h3(id),r2=fract(r*17.3);
float bd=3.5-max(abs(p.x),max(abs(p.y),abs(p.z)));
p.xz*=rot(r*6.28+uT*.3*(r2-.5));p.xy*=rot(r2*6.28+uT*.2*(r-.5));
float d=tree(p,2.6+1.9*r2-.4*uK,5.+floor(r*3.),.9+.4*r2,r*3.+uT*.1,1.,1.8,1.);
if(r<uG){gE=1.;gH=exp(-d*14.);}else{gE=0.;gH=0.;}
return min(d,bd+.6);}
float map(vec3 p){
if(uM==0)return tree(p,uG,uSym,uTilt,uTw,uMir,uKs,uCore);
if(uM==1)return lattice(p);
if(uM==2)return hive(p);
return colony(p);}
vec3 nrm(vec3 p,float e){vec2 k=vec2(1,-1);return normalize(k.xyy*map(p+k.xyy*e)+k.yyx*map(p+k.yyx*e)+k.yxy*map(p+k.yxy*e)+k.xxx*map(p+k.xxx*e));}
void main(){
vec2 uv=(gl_FragCoord.xy*2.-uR)/uR.y;uv*=rot(uRoll);
vec3 f=normalize(uTa-uRo),r=normalize(cross(f,uUp)),u=cross(r,f),rd=normalize(f*uFov+uv.x*r+uv.y*u);
float t=.02,far=uM==0?40.:uM==3?46.:30.,gl=0.;bool hit=false;
if(uM==0){float b=dot(uRo,rd),c=dot(uRo,uRo)-uBr*uBr,dd=b*b-c;
if(dd<0.)t=far+1.;else{t=max(-b-sqrt(dd),.02);far=-b+sqrt(dd);}
float m=dot(-uRo,rd);vec3 pc=uRo+rd*max(m,0.);gl+=uCore*.05/(.02+dot(pc,pc))*step(far+.5,t);}
for(int i=0;i<110;i++){
if(t>far)break;
vec3 p=uRo+rd*t;float d=map(p);
gl+=gH*.013;
if(d<(uM==0?.0008:.0022)*t+.0002){hit=true;break;}if(uM==3&&i>74)break;
t+=d;}
float lum=0.,acc=0.;
if(hit){
vec3 p=uRo+rd*t;float e0=gE,fg=exp(-t*uFog);
vec3 n=nrm(p,.0008*t+.0004);
vec3 l=uM==2?normalize(uRo-p):uL;
float sh=1.;
if(uM!=2){float st=.03;for(int j=0;j<18;j++){float d=map(p+n*.006+l*st);sh=min(sh,9.*d/st);st+=clamp(d,.03,.4);if(sh<.01||st>6.||(uM!=0&&j>9))break;}sh=clamp(sh,0.,1.);}
float ao=0.,sca=1.;for(int j=1;j<6;j++){float hh=.05*float(j);ao+=(hh-map(p+n*hh))*sca;sca*=.72;}
ao=clamp(1.-1.6*ao,0.,1.);
float dif=max(dot(n,l),0.),rim=pow(1.-max(dot(n,-rd),0.),3.);
lum=dif*sh*1.15+.1*ao+rim*.35*ao*(.4+.6*sh);
if(uM==2)lum=(dif*.9+rim*.3)*ao*ao*2.2/(1.+.12*t*t);
lum*=.35+.65*ao;lum=uNeg>.5?fg*(1.-.55*min(lum,1.)):lum*fg;
acc=e0*fg*(.55+.45*dif);
}
acc+=gl;
o=vec4(lum,acc,0,1);}`;
const PS=`#version 300 es
precision highp float;
uniform sampler2D uX;uniform vec4 uB;uniform float uT,uInv,uGl,uFl,uRf,uFade;
out vec4 o;
float h(vec2 p){return fract(sin(dot(p,vec2(12.9898,78.233)))*43758.5453);}
void main(){
vec2 uv=(gl_FragCoord.xy-uB.xy)/uB.zw;
if(uv.y<0.||uv.y>1.||uv.x<0.||uv.x>1.){o=vec4(0,0,0,1);return;}
float row=floor(uv.y*34.),tq=floor(uT*14.);
float g=uGl*step(.5,h(vec2(row,tq)));
uv.x=fract(uv.x+g*(h(vec2(row*3.,tq))-.5)*.4);
vec2 c=texture(uX,uv).rg,bl=vec2(0);float w=.5;
for(int i=2;i<7;i++){bl+=textureLod(uX,uv,float(i)).rg*w;w*=.8;}
float l=c.r+bl.r*.1,a=c.g*.9+bl.g*.55;
l=smoothstep(0.,1.,l);
vec2 q=uv-.5;l*=1.-.9*dot(q,q)*dot(q,q)*4.;
l+=(h(gl_FragCoord.xy+fract(uT)*91.)-.5)*.06;
l=clamp(l,0.,1.);
l=mix(l,1.-l,uInv);
a=clamp(a+uRf+g*.15,0.,1.);
vec3 col=mix(vec3(l),vec3(1.,.11,0.),a*a*(3.-2.*a));
col=mix(col,vec3(1),uFl);
o=vec4(col*uFade,1);}`;
const $=id=>document.getElementById(id),Q=new URLSearchParams(location.search);
const BAR=2.5,END=190,cv=$('c'),gl=cv.getContext('webgl2',{antialias:false,alpha:false,powerPreference:'high-performance'});
const D=window.DEMO={ft:[],fps:[],scale:1,done:false,err:null};
function prog(fs){const p=gl.createProgram();for(const[ty,src]of[[gl.VERTEX_SHADER,VS],[gl.FRAGMENT_SHADER,fs]]){const s=gl.createShader(ty);gl.shaderSource(s,src);gl.compileShader(s);if(!gl.getShaderParameter(s,gl.COMPILE_STATUS)){D.err=gl.getShaderInfoLog(s);throw new Error(D.err)}gl.attachShader(p,s)}gl.linkProgram(p);if(!gl.getProgramParameter(p,gl.LINK_STATUS)){D.err=gl.getProgramInfoLog(p);throw new Error(D.err)}const u={};for(let i=0,n=gl.getProgramParameter(p,gl.ACTIVE_UNIFORMS);i<n;i++){const nm=gl.getActiveUniform(p,i).name;u[nm]=gl.getUniformLocation(p,nm)}return{p,u}}
let P1,P2,fbo,tex,fw=0,fh=0,W,H,bx,by,bw,bh;
const HF=gl&&gl.getExtension('EXT_color_buffer_float');
if(gl){gl.getExtension('OES_texture_float_linear');P1=prog(FS);P2=prog(PS);tex=gl.createTexture();fbo=gl.createFramebuffer();gl.bindVertexArray(gl.createVertexArray())}
else{D.err='no webgl2';$('sm').textContent='this demo needs WebGL2'}
function size(){const dpr=Math.min(devicePixelRatio||1,2);W=Math.min(Math.round(innerWidth*dpr),+Q.get('w')||1920);H=Math.round(W*innerHeight/innerWidth);cv.width=W;cv.height=H;bw=W;bh=Math.min(H,Math.round(W/2.2));bx=0;by=(H-bh)>>1;
const o=$('o').style,k=innerWidth/W;o.left='0';o.top=by*k+'px';o.width=innerWidth+'px';o.height=bh*k+'px';o.fontSize=innerWidth/100+'px';fw=0}
function target(rs){const w=Math.max(8,Math.round(bw*D.scale*rs)),h=Math.max(8,Math.round(bh*D.scale*rs));if(w==fw&&h==fh)return;fw=w;fh=h;gl.bindTexture(gl.TEXTURE_2D,tex);
if(HF)gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA16F,w,h,0,gl.RGBA,gl.HALF_FLOAT,null);else gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA8,w,h,0,gl.RGBA,gl.UNSIGNED_BYTE,null);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR_MIPMAP_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);
gl.bindFramebuffer(gl.FRAMEBUFFER,fbo);gl.framebufferTexture2D(gl.FRAMEBUFFER,gl.COLOR_ATTACHMENT0,gl.TEXTURE_2D,tex,0)}
const ss=(a,b,x)=>{x=Math.min(1,Math.max(0,(x-a)/(b-a)));return x*x*(3-2*x)},mix=(a,b,x)=>a+(b-a)*x,fr=x=>x-Math.floor(x),ease=x=>1-(1-Math.min(1,x))**3;
const env=(A,t,r)=>{let lo=0,hi=A.length;while(lo<hi){const m=lo+hi>>1;A[m]<=t?lo=m+1:hi=m}return lo?Math.exp(-(t-A[lo-1])*r):0};
const EV=synth(44100,1),toff=z=>[Math.sin(z*.13)*1.1,Math.cos(z*.09)*1.1];
const NAMES=['SEED','GERMINATION','LATTICE','HIVE','ORGANISM','COLONY','SEED'],NUM=['I','II','III','IV','V','VI','I'];
function scene(t){
const bar=t/BAR,s=Math.min(6,Math.floor(bar/12)),b=bar-s*12,u=b/12,k=env(EV.kicks,t,5),sn=env(EV.snares,t,7),ts=t-s*30;
const o={s,b,m:0,G:0,sym:3,tilt:.62,tw:.9,mir:0,ks:1.55,core:0,W:-9,Wa:0,fr:1,inv:0,fog:0,br:3.4,fov:1.7,roll:0,gl:0,fl:0,rf:0,fade:1,rs:1,k,up:[0,1,0],st:''};
let ang=.6+t*.11,dist=3,ty=0,el=.5;
if(s==0){o.G=ss(7,12,bar)*.999;dist=mix(3.6,2.7,ss(0,12,b));ty=.1+.25*o.G;o.core=.35+1.1*k;o.fade=ss(0,2.5,t);o.st='depth 0<br>branches '+(o.G>.02?3:0)}
else if(s==1){const n=Math.floor(b/2);o.G=1+Math.min(6,n+ease(fr(b/2)*1.5));dist=2.7+.42*(o.G-1);ty=.35+.27*(o.G-1);el=.5+.12*(o.G-1);o.core=.4+.8*k;o.fl=Math.exp(-ts*4);o.st='depth '+Math.min(7,n+2)+'<br>branches '+(3**Math.min(7,n+2)*1.5-1.5)}
else if(s==6){dist=mix(2.7,4.4,ss(0,4,b));ty=.1;o.core=.35+1.1*k;o.fl=Math.exp(-ts*3);o.fade=1-ss(8,10,ts);o.st='depth 0<br>again'}
if(o.m==0&&s!=4){o.ro=[Math.cos(ang)*dist,ty+el,Math.sin(ang)*dist];o.ta=[0,ty,0]}
if(s==2){o.m=1;o.inv=1;o.G=2+Math.min(3,Math.floor(b/3)+ease(fr(b/3)*2));const z=ts*2.2;o.ro=[2+.3*Math.sin(z*.35),2+.25*Math.cos(z*.27),z];o.ta=[o.ro[0]+.7*Math.sin(t*.3),o.ro[1]+.45*Math.cos(t*.23),z+3];o.roll=.4*Math.sin(t*.37);o.fog=.075;o.fov=1.15;o.W=z+6+30*fr(bar);o.Wa=.8;o.rs=.78;o.tw=.6+.25*Math.sin(t*.21);o.fl=Math.exp(-ts*5);o.gl=.3*sn;o.st='fold 48<br>depth '+Math.floor(o.G)}
if(s==3){o.m=2;const z=b<4?ts*.9:9+(ts-10)*2.7,c=toff(z),c2=toff(z+3),a=t*.21;o.ro=[c[0]+.9*Math.cos(a),c[1]+.9*Math.sin(a),z];o.ta=[c2[0],c2[1],z+3];o.roll=t*.13;o.fog=.05;o.fov=1;o.G=.04+.96*u**1.4;o.rf=ss(11.2,12,b);o.st='cells hatched<br>'+Math.round(Math.min(1,o.G*1.25)*100)+'%'}
if(s==4){o.mir=1;o.sym=b<6?6:8;o.tilt=1.02+.22*Math.sin(t*.45);o.tw=t*.12;o.ks=1.8;o.G=6-.55*k;o.br=2.95;o.fr=0;o.core=1;o.W=7*fr(bar)-.5;o.Wa=1;o.rf=Math.exp(-ts*2.2);
if(b<6){const a=t*.22;dist=4.6-.1*b;o.ro=[Math.cos(a)*dist,1.7,Math.sin(a)*dist];o.ta=[0,0,0]}
else{o.ro=[0,5.2-.2*(b-6),.001];o.ta=[0,0,0];o.up=[0,0,1];o.roll=t*.17;o.fl=Math.exp(-(ts-15)*5)}
o.gl=.2*sn;o.st='symmetry '+o.sym+'<br>tips '+(2*o.sym**6)}
if(s==5){o.m=3;const z=ts*5.2;o.ro=[3.5+.5*Math.sin(z*.1),3.5+.5*Math.cos(z*.13),z];o.ta=[o.ro[0]+.8*Math.sin(t*.5),o.ro[1]+.6*Math.cos(t*.4),z+4];o.roll=t*.22;o.fog=.03;o.fov=1.2;o.rs=.78;o.G=.06+.3*u;const hit=sn>.6;o.inv=b<6?+hit:+!hit;o.fl=Math.exp(-ts*5);o.gl=.25*sn;o.st='colony<br>unbounded'}
if(b>11.5&&s<6)o.gl=1;
return o}
const LAST={};
function set(id,v){if(LAST[id]!==v){LAST[id]=v;$(id).innerHTML=v}}
function draw(t){
const o=scene(t);target(o.rs);
gl.bindFramebuffer(gl.FRAMEBUFFER,fbo);gl.viewport(0,0,fw,fh);gl.useProgram(P1.p);
let U=P1.u;
gl.uniform2f(U.uR,fw,fh);gl.uniform1f(U.uT,t);gl.uniform1f(U.uG,o.G);gl.uniform1f(U.uK,o.k);gl.uniform1f(U.uSym,o.sym);gl.uniform1f(U.uTilt,o.tilt);gl.uniform1f(U.uTw,o.tw);gl.uniform1f(U.uMir,o.mir);gl.uniform1f(U.uKs,o.ks);gl.uniform1f(U.uCore,o.core);gl.uniform1f(U.uW,o.W);gl.uniform1f(U.uWa,o.Wa);gl.uniform1f(U.uFr,o.fr);gl.uniform1f(U.uNeg,o.inv);gl.uniform1f(U.uFog,o.fog);gl.uniform1f(U.uFov,o.fov);gl.uniform1f(U.uRoll,o.roll);gl.uniform1f(U.uBr,o.br);gl.uniform1i(U.uM,o.m);
gl.uniform3fv(U.uRo,o.ro);gl.uniform3fv(U.uTa,o.ta);gl.uniform3fv(U.uUp,o.up);
{const f=[0,1,2].map(i=>o.ta[i]-o.ro[i]),fl=Math.hypot(...f);f.forEach((v,i)=>f[i]=v/fl);const up=o.up,r=[f[1]*up[2]-f[2]*up[1],f[2]*up[0]-f[0]*up[2],f[0]*up[1]-f[1]*up[0]],rl=Math.hypot(...r);r.forEach((v,i)=>r[i]=v/rl);const uu=[r[1]*f[2]-r[2]*f[1],r[2]*f[0]-r[0]*f[2],r[0]*f[1]-r[1]*f[0]],l=[0,1,2].map(i=>r[i]*.55+uu[i]*.7-f[i]*.45),ll=Math.hypot(...l);gl.uniform3f(U.uL,l[0]/ll,l[1]/ll,l[2]/ll)}
gl.drawArrays(gl.TRIANGLES,0,3);
gl.bindTexture(gl.TEXTURE_2D,tex);gl.generateMipmap(gl.TEXTURE_2D);
gl.bindFramebuffer(gl.FRAMEBUFFER,null);gl.viewport(0,0,W,H);gl.useProgram(P2.p);U=P2.u;
gl.uniform4f(U.uB,bx,by,bw,bh);gl.uniform1f(U.uT,t);gl.uniform1f(U.uInv,o.inv);gl.uniform1f(U.uGl,o.gl);gl.uniform1f(U.uFl,Math.min(1,o.fl));gl.uniform1f(U.uRf,o.rf);gl.uniform1f(U.uFade,o.fade);gl.uniform1i(U.uX,0);
gl.drawArrays(gl.TRIANGLES,0,3);
const tt=t<180?t>3.5&&t<13:t>181.5&&t<187.5;
$('ti').style.opacity=tt?1:0;$('su').style.opacity=tt?.7:0;
if(t>180)set('su','one file · webgl2 + webaudio · no assets');
set('pl',t<END?'<b>plate '+NUM[o.s]+'</b> '+NAMES[o.s]:'');set('st',t<END?o.st:'');
const m=Math.floor(t/60),sec=Math.floor(t%60);set('tc',t<END?'0'+m+':'+(sec<10?'0':'')+sec+' / 03:10':'');
const inv=o.inv>.5&&o.fl<.5;if(LAST.inv!==inv){LAST.inv=inv;$('o').className=inv?'i':''}
$('o').style.opacity=o.fade}
const LOG=Q.has('log');let ac,t0=0,playing=false,lt=0,fc=0,fa=0,ls=0,probe=0;
function loop(now){
if(!playing)return;
const t=ac?ac.currentTime-t0:0;
if(!stall&&!allIn&&nx>=CK.length&&sEnd-t<.3){stall=1;D.stalls++;ac.suspend()}else if(!stall&&ac.state!='running'&&now-rs>400){rs=now;ac.resume()}
if(LOG&&t>0){an.getFloatTimeDomainData(ab);let q=0;for(let i=0;i<2048;i++)q+=ab[i]*ab[i];D.rms.push(t,Math.sqrt(q/2048))}
if(t>=END+.6){playing=false;D.done=true;draw(END+1);$('o').style.opacity=0;$('s').style.display='flex';$('sm').textContent='click or press a key to grow it again';ac.suspend();return}
if(lt){const dt=now-lt;D.ft.push(dt);fa+=dt;fc++;
if(now-ls>=1000){const avg=fa/fc;D.fps.push(Math.round(1000/avg));
if(!Q.get('q')){if(avg>19.5&&D.scale>.45){D.scale=Math.max(.45,D.scale*.85);probe=now+9000}else if(avg<17.3&&D.scale<1&&now>probe){D.scale=Math.min(1,D.scale/.9);probe=now+9000}}
fa=fc=0;ls=now}}
lt=now;draw(Math.max(0,t));if(!D.tFrame)D.tFrame=Math.round(now-tc);if(!D.tSound&&t>=0)D.tSound=Math.round(performance.now()-tc);requestAnimationFrame(loop)}
// Soundtrack: a worker renders it in 5 s blocks from page load; each block is scheduled on the audio clock as it arrives.
// ?mute keeps the speakers silent for self-tests (the analyser still hears the mix), ?log records what it hears.
// If playback ever catches up with the rendering, the audio clock (the demo's only clock) is paused until the next block is in.
const AC_=window.AudioContext||window.webkitAudioContext,BUILD='4';let SR=44100,out,an,ab;
function mkac(){ac=new AC_();SR=ac.sampleRate;out=ac.createGain();const mg=ac.createGain();mg.gain.value=Q.has('mute')?0:1;out.connect(mg);mg.connect(ac.destination);an=ac.createAnalyser();an.fftSize=2048;out.connect(an);ab=new Float32Array(2048)}
try{mkac()}catch(e){ac=null}
const CK=[];let WK=null,wait=0,tc=0,allIn=0,nx=0,sEnd=0,stall=0,srcs=[],rs=0;
D.build=BUILD;D.stalls=0;D.rms=[];D.ck=CK;
const keep=(a,b,L,R)=>CK.push({a,b,L:L.slice(a,b),R:R.slice(a,b)});
try{WK=new Worker(URL.createObjectURL(new Blob(['const MG='+MG+';'+synth+';const g=performance.now();synth('+SR+',0,(a,b,L,R)=>{const l=L.slice(a,b),r=R.slice(a,b);postMessage({a,b,L:l,R:r,ms:performance.now()-g},[l.buffer,r.buffer])});postMessage({end:1,ms:performance.now()-g})'])));
WK.onmessage=e=>{const d=e.data;if(d.end){allIn=1;D.gen_full=Math.round(d.ms)}else{CK.push(d);if(CK.length==1)D.gen_first=Math.round(d.ms)}pump()};
WK.onerror=()=>{WK=null;if(wait)begin()}}catch(e){WK=null}
function pump(){
if(wait){begin();return}
if(!playing)return;
while(nx<CK.length){const c=CK[nx++],now=ac.currentTime,T=t0+c.a/SR;sEnd=c.b/SR;if(t0+sEnd<=now+.02)continue;
if(!c.buf){c.buf=ac.createBuffer(2,c.b-c.a,SR);c.buf.copyToChannel(c.L,0);c.buf.copyToChannel(c.R,1)}
const s=ac.createBufferSource(),w=Math.max(T,ac.currentTime+.03);s.buffer=c.buf;s.connect(out);if(w>T)s.start(w,w-T);else s.start(T);srcs.push(s)}
if(stall&&(allIn||sEnd-(ac.currentTime-t0)>1)){stall=0;ac.resume()}}
function begin(){
const off=Math.max(0,+Q.get('t')||0),k0=Math.min(36,Math.floor(off/5));
if(!WK&&!allIn){CK.length=0;synth(SR,0,keep);allIn=1}
if(CK.length<=k0){wait=1;$('sm').textContent='germinating…';return}
wait=0;for(const s of srcs)try{s.stop()}catch(e){}srcs=[];nx=k0;stall=0;sEnd=0;
t0=Math.ceil((ac.currentTime+.06)*SR)/SR-off;D.audio=ac;D.src=allIn?'full':'blocks';D.stalls=0;D.rms=[];
$('s').style.display='none';D.done=false;D.ft=[];D.fps=[];D.tFrame=D.tSound=0;lt=0;ls=performance.now();playing=true;pump();requestAnimationFrame(loop)}
function start(){
if(playing||!gl||wait)return;tc=performance.now();if(!ac)mkac();ac.resume();begin()}
addEventListener('resize',()=>{size();if(!playing&&gl)draw(shotT)});
addEventListener('keydown',e=>{if(e.key=='f'||e.key=='F'){document.fullscreenElement?document.exitFullscreen():document.documentElement.requestFullscreen&&document.documentElement.requestFullscreen()}else start()});
$('s').addEventListener('click',start);
let shotT=0;
D.shot=t=>{shotT=t;$('s').style.display='none';if(Q.get('q'))D.scale=+Q.get('q');draw(t);gl.finish();return t};
D.bench=(t,n)=>{const a=performance.now();for(let i=0;i<n;i++){draw(t+i/60);gl.finish()}const px=new Uint8Array(4);gl.readPixels(0,0,1,1,gl.RGBA,gl.UNSIGNED_BYTE,px);return(performance.now()-a)/n};
D.gaps=()=>{const r=D.rms,g=[];let n=0,bad=0,cur=null;for(let i=0;i<r.length;i+=2){const t=r[i],k=Math.min(CK.length-1,Math.floor(t/5)),c=CK[k];if(!c)continue;const j=Math.round(t*SR)-c.a;let q=0,m=0;for(let x=Math.max(0,j-2048);x<j&&x<c.L.length;x++){q+=(c.L[x]+c.R[x])**2/4;m++}const ex=m?Math.sqrt(q/m):0;if(ex<.03)continue;n++;if(r[i+1]<ex*.25){bad++;if(cur&&t-cur[1]<.2)cur[1]=t;else g.push(cur=[t,t])}}return{frames:r.length/2,loud:n,quiet:bad,gaps:g.filter(x=>x[1]-x[0]>.08).map(x=>x[0].toFixed(2)+'-'+x[1].toFixed(2))}};
if(gl){size();if(Q.get('q'))D.scale=+Q.get('q');if(Q.has('shot'))D.shot(+Q.get('shot'));else draw(0)}
</script>
</html>