Show code
<!doctype html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Fourier Epicycles</title><style>
html,body{margin:0;height:100%;background:#07080e;overflow:hidden}
canvas{display:block}
#i{position:fixed;left:16px;top:14px;font:12px/1.6 ui-monospace,Menlo,monospace;color:#9fdcff;letter-spacing:.14em;text-transform:uppercase;text-shadow:0 0 10px rgba(60,170,255,.6);pointer-events:none}
#i b{color:#fff}
</style></head><body><canvas id=c></canvas><div id=i></div><script>
const T=6.283185307,c=document.getElementById('c'),g=c.getContext('2d'),I=document.getElementById('i'),D=Math.min(2,devicePixelRatio||1);
let W,H,CO,M,NRM,tt,hold,path,hue,last=0;
function rs(){W=c.width=innerWidth*D;H=c.height=innerHeight*D;c.style.width=innerWidth+'px';c.style.height=innerHeight+'px'}
function poly(V){return t=>{const n=V.length,s=t/T*n,i=Math.floor(s),f=s-i,a=V[i%n],b=V[(i+1)%n];return[a[0]+(b[0]-a[0])*f,a[1]+(b[1]-a[1])*f]}}
function ring(n,r1,r2){const V=[];for(let i=0;i<n;i++){const A=T*i/n-1.5708;V.push([(i%2?r2:r1)*Math.cos(A),(i%2?r2:r1)*Math.sin(A)])}return V}
function shape(){const R=Math.random,y=R()*6|0;
if(y==0){const k=3+(R()*5|0),a=.22+R()*.3;return t=>{const r=1+a*Math.cos(k*t);return[r*Math.cos(t),r*Math.sin(t)]}}
if(y==1)return poly(ring(3+(R()*6|0),1,1));
if(y==2){const p=5+(R()*4|0);return poly(ring(2*p,1.15,.42+R()*.16))}
if(y==3)return t=>{const s=Math.sin(t);return[1.2*s*s*s,-.075*(13*Math.cos(t)-5*Math.cos(2*t)-2*Math.cos(3*t)-Math.cos(4*t))]};
if(y==4){const k=6+(R()*8|0);return t=>{const r=1+.18*Math.tanh(5*Math.sin(k*t));return[r*Math.cos(t),r*Math.sin(t)]}}
const e=2.4+R()*4;return t=>{const u=Math.cos(t),v=Math.sin(t),q=Math.pow(Math.pow(Math.abs(u),e)+Math.pow(Math.abs(v),e),-1/e);return[q*u,q*v]}}
function init(){const f=shape(),N=400,P=[];let sx=0,sy=0;
for(let n=0;n<N;n++){const p=f(T*n/N);P.push(p);sx+=p[0];sy+=p[1]}
sx/=N;sy/=N;NRM=.1;
for(const p of P){p[0]-=sx;p[1]-=sy;NRM=Math.max(NRM,Math.hypot(p[0],p[1]))}
const C=[];
for(let k=0;k<N;k++){let re=0,im=0;
for(let n=0;n<N;n++){const A=-T*k*n/N,u=Math.cos(A),v=Math.sin(A);re+=P[n][0]*u-P[n][1]*v;im+=P[n][0]*v+P[n][1]*u}
C.push({f:k>N/2?k-N:k,a:Math.hypot(re,im)/N,p:Math.atan2(im,re)})}
C.sort((u,v)=>v.a-u.a);
M=26+(Math.random()*54|0);CO=C.slice(0,M);
tt=0;hold=0;path=[];hue=Math.random()*360|0;
I.innerHTML='Fourier series · epicycles: <b>'+M+'</b>'}
function frame(now){requestAnimationFrame(frame);
const dt=Math.min(.05,(now-last)/1000);last=now;
g.fillStyle='#07080e';g.fillRect(0,0,W,H);
const S=Math.min(W,H)*.31/NRM,p1=new Path2D(),p2=new Path2D();
g.save();g.translate(W/2,H/2);g.lineJoin='round';g.lineCap='round';
let px=0,py=0;p2.moveTo(0,0);
for(let i=0;i<M;i++){const o=CO[i],r=o.a*S,A=o.f*tt+o.p;
if(r>1.5){p1.moveTo(px+r,py);p1.arc(px,py,r,0,T)}
px+=r*Math.cos(A);py+=r*Math.sin(A);p2.lineTo(px,py)}
g.lineWidth=D*.7;g.strokeStyle='rgba(120,180,255,.17)';g.stroke(p1);
g.lineWidth=D;g.strokeStyle='rgba(200,230,255,.55)';g.stroke(p2);
if(!hold)path.push([px/S,py/S]);
const p3=new Path2D();
for(let i=0;i<path.length;i++){const p=path[i];i?p3.lineTo(p[0]*S,p[1]*S):p3.moveTo(p[0]*S,p[1]*S)}
g.lineWidth=D*1.7;g.shadowBlur=16;g.shadowColor='hsl('+hue+',100%,58%)';g.strokeStyle='hsl('+hue+',100%,68%)';g.stroke(p3);
g.shadowBlur=0;g.fillStyle='#fff';g.beginPath();g.arc(px,py,D*2.2,0,T);g.fill();
g.restore();
if(hold){hold+=dt;if(hold>1.7)init()}else{tt+=dt*.7;if(tt>=T){tt=T;hold=1e-4}}}
rs();addEventListener('resize',rs);init();requestAnimationFrame(frame);
</script></body></html>