Custom Code Experiment •
Van de Graaff
Source Code View HTML / CSS / JavaScript
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Van de Graaff Loader</title>
<style>
:root{ --bg:#050505; --ring:rgba(255,255,255,0.08); }
:root:not([data-theme="light"]){ --bg:#050505; }
html,body{height:100%;margin:0;background:var(--bg);overflow:hidden;}
body{display:flex;align-items:center;justify-content:center;}
#c{display:block;border-radius:50%;overflow:hidden;box-shadow:0 0 0 1px var(--ring);}
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
(function(){
"use strict";
var TAU=Math.PI*2;
var cvs=document.getElementById('c');
var ctx=cvs.getContext('2d',{alpha:false});
var D=0,DPR=1,CX=0,CY=0,S=1;
var calm=window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var t0=0,last=0;
/* rig geometry, recomputed on resize */
var SX=0,SY=0,RS=0,COLW=0,GX=0,GY=0,GR=0;
/* ---------- patterns ---------- */
var noiseTiles=[],scanPat=null;
function buildNoise(){
noiseTiles=[];
for(var n=0;n<4;n++){
var s=140,nc=document.createElement('canvas'); nc.width=nc.height=s;
var nx=nc.getContext('2d'),id=nx.createImageData(s,s),d=id.data;
for(var i=0;i<d.length;i+=4){
var v=Math.random()*255;
d[i]=v; d[i+1]=v; d[i+2]=v; d[i+3]=Math.random()*Math.random()*85;
}
nx.putImageData(id,0,0);
noiseTiles.push(ctx.createPattern(nc,'repeat'));
}
var sc=document.createElement('canvas'); sc.width=1; sc.height=3;
var sx=sc.getContext('2d');
sx.fillStyle='rgba(0,0,0,0.5)'; sx.fillRect(0,0,1,1);
scanPat=ctx.createPattern(sc,'repeat');
}
/* ---------- sizing ---------- */
function measure(){
var vw=window.innerWidth||800, vh=window.innerHeight||600;
return Math.round(Math.max(150, Math.min(Math.min(vw,vh)*0.46, 420)));
}
function resize(){
D=measure();
DPR=Math.min(2, window.devicePixelRatio||1);
cvs.style.width=D+'px'; cvs.style.height=D+'px';
cvs.width=Math.round(D*DPR); cvs.height=Math.round(D*DPR);
CX=D*0.5; CY=D*0.5; S=D/360;
SX=CX-D*0.045; SY=CY-D*0.10; RS=D*0.163;
COLW=D*0.072;
GX=CX+D*0.295; GY=SY+D*0.035; GR=D*0.050;
buildNoise();
}
window.addEventListener('resize', resize);
/* ---------- signal ---------- */
var NB=56,bands=new Float32Array(NB),WV=256,wave=new Float32Array(WV);
function envelope(t){
var b=t*(calm?68:112)/60;
var kick=Math.exp(-(b%1)*6);
var snare=Math.exp(-((b+0.5)%2)*5)*0.5;
var hat=Math.exp(-((b*2)%1)*14)*0.22;
return Math.min(1.7, kick*0.85+snare+hat+0.14);
}
function updateSignal(t, amp){
for(var k=0;k<NB;k++){
var f=k/NB;
var v=(Math.sin(t*(1.7+k*0.41)+k*2.1)*0.5+0.5);
v*=Math.pow(1-f,1.15); v=v*0.65+Math.random()*0.12;
v*=0.35+amp*0.8;
bands[k]=bands[k]*0.74+v*0.26;
}
for(var i=0;i<WV;i++){
var u=i/(WV-1),s=0;
s+=Math.sin(u*TAU*3+t*4.6)*0.52;
s+=Math.sin(u*TAU*7.3-t*3.1)*0.27;
s+=Math.sin(u*TAU*17.7+t*9.4)*0.13;
s+=(Math.random()-0.5)*0.18;
wave[i]=s*amp;
}
}
/* ---------- glitch ---------- */
var g=0,gUntil=0,dropUntil=0,nextDrop=4;
function updateGlitch(t){
if(t>gUntil && Math.random()<(calm?0.006:0.03)){
gUntil=t+0.05+Math.random()*(calm?0.14:0.38);
g=Math.max(g,(calm?0.25:0.35)+Math.random()*(calm?0.3:0.65));
}
if(t>gUntil) g*=0.86;
if(g<0.004) g=0;
if(!calm && t>nextDrop){ dropUntil=t+0.05+Math.random()*0.16; nextDrop=t+5+Math.random()*7; }
}
/* ---------- colour flares ---------- */
var FLARE_HUES=[188,312,96,36];
var flare=0,flareHue=188,flareUntil=0,nextFlare=1.6;
function hsl(h,s,l){ return 'hsl('+((h%360+360)%360)+','+Math.round(s)+'%,'+Math.round(l)+'%)'; }
function updateFlare(t){
if(t>nextFlare){
flareHue=FLARE_HUES[(Math.random()*FLARE_HUES.length)|0];
flare=Math.max(flare,0.6+Math.random()*0.4);
flareUntil=t+0.16+Math.random()*(calm?0.3:0.55);
nextFlare=t+(calm?4:2.2)+Math.random()*(calm?6:4.5);
}
if(g>0.6 && flare<0.25) flare=0.25+Math.random()*0.3;
if(t>flareUntil) flare*=0.90;
if(flare<0.005) flare=0;
}
/* ---------- arc generation ---------- */
function subdivide(pts, disp, iters){
for(var it=0;it<iters;it++){
var np=[];
for(var i=0;i<pts.length-1;i++){
var a=pts[i],b=pts[i+1];
var dx=b.x-a.x, dy=b.y-a.y, L=Math.sqrt(dx*dx+dy*dy)||1;
var nx=-dy/L, ny=dx/L;
var o=(Math.random()-0.5)*disp;
np.push(a,{x:(a.x+b.x)*0.5+nx*o, y:(a.y+b.y)*0.5+ny*o});
}
np.push(pts[pts.length-1]);
pts=np; disp*=0.56;
}
return pts;
}
function arcBetween(x0,y0,x1,y1,iters){
var dx=x1-x0,dy=y1-y0,L=Math.sqrt(dx*dx+dy*dy);
return subdivide([{x:x0,y:y0},{x:x1,y:y1}], L*0.34, iters||5);
}
var arcs=[],sparks=[],streamers=[],flash=0,nextStrike=0,nextStream=0,prevAmp=0,charge=0;
function domePoint(ang){ return {x:SX+Math.cos(ang)*RS, y:SY+Math.sin(ang)*RS}; }
function discharge(t, power){
var toGround=Math.random()<0.6;
var pts, len;
if(toGround){
var a0=Math.atan2(GY-SY, GX-SX)+(Math.random()-0.5)*0.9;
var a1=Math.atan2(SY-GY, SX-GX)+(Math.random()-0.5)*0.9;
var p=domePoint(a0);
var q={x:GX+Math.cos(a1)*GR, y:GY+Math.sin(a1)*GR};
pts=arcBetween(p.x,p.y,q.x,q.y,6);
len=1;
}else{
var ang=-Math.PI*(0.08+Math.random()*0.84);
if(Math.random()<0.2) ang=(Math.random()<0.5?-0.05:Math.PI+0.05);
var s0=domePoint(ang);
len=D*(0.10+Math.random()*0.24);
var ex=s0.x+Math.cos(ang)*len, ey=s0.y+Math.sin(ang)*len;
/* keep it inside the disc */
var vx=ex-CX, vy=ey-CY, r=Math.sqrt(vx*vx+vy*vy), lim=D*0.45;
if(r>lim){ ex=CX+vx/r*lim; ey=CY+vy/r*lim; }
pts=arcBetween(s0.x,s0.y,ex,ey,5);
}
var branches=[],nb=(toGround?1:0)+((Math.random()*2+power)|0);
for(var i=0;i<nb;i++){
var at=(0.25+Math.random()*0.5)*(pts.length-1)|0;
var bp=pts[at];
var bl=D*(0.05+Math.random()*0.13);
var ba=Math.random()*TAU;
branches.push(arcBetween(bp.x,bp.y,bp.x+Math.cos(ba)*bl,bp.y+Math.sin(ba)*bl,4));
}
arcs.push({pts:pts,branches:branches,born:t,
dur:0.18+Math.random()*0.34,seed:Math.random()*10,
w:(0.9+power*1.0)*S,ground:toGround});
if(arcs.length>8) arcs.shift();
flash=Math.max(flash,0.3+power*0.5);
g=Math.max(g,0.18+power*0.35);
charge=Math.max(0,charge-0.55-power*0.3);
if(Math.random()<0.3){
flare=Math.max(flare,0.45+Math.random()*0.4);
flareUntil=t+0.2+Math.random()*0.3;
flareHue=FLARE_HUES[(Math.random()*FLARE_HUES.length)|0];
}
var tipSrc=branches.length?branches:[pts];
for(var b=0;b<tipSrc.length;b++){
var tip=tipSrc[b][tipSrc[b].length-1];
var n=1+((Math.random()*4)|0);
for(var s2=0;s2<n;s2++){
sparks.push({x:tip.x,y:tip.y,
vx:(Math.random()-0.5)*80*S, vy:(Math.random()-0.6)*80*S,
life:0.16+Math.random()*0.32, age:0});
}
}
if(sparks.length>80) sparks.splice(0,sparks.length-80);
}
function refreshStreamers(t){
streamers.length=0;
var n=4+((Math.random()*5+charge*3)|0);
for(var i=0;i<n;i++){
var ang=-Math.PI*(0.02+Math.random()*0.96);
var p=domePoint(ang);
var l=D*(0.015+Math.random()*0.055)*(0.6+charge*0.8);
streamers.push(arcBetween(p.x,p.y,p.x+Math.cos(ang)*l,p.y+Math.sin(ang)*l,3));
}
nextStream=t+0.045+Math.random()*0.055;
}
function updateRig(t, dt, amp){
charge=Math.min(1.6, charge+dt*(calm?0.55:1.15));
if(amp>1.15 && prevAmp<=1.15 && charge>0.5) discharge(t, Math.min(1,charge*0.7));
prevAmp=amp;
if(t>nextStrike && charge>0.35){
discharge(t, Math.random()*0.5+charge*0.3);
nextStrike=t+(calm?0.7:0.28)+Math.random()*(calm?1.3:0.8);
}
if(t>nextStream) refreshStreamers(t);
for(var i=sparks.length-1;i>=0;i--){
var s=sparks[i];
s.age+=dt; s.x+=s.vx*dt; s.y+=s.vy*dt; s.vy+=55*S*dt;
if(s.age>=s.life) sparks.splice(i,1);
}
flash*=0.84; if(flash<0.004) flash=0;
}
/* ---------- drawing ---------- */
function tracePath(pts){
ctx.beginPath();
ctx.moveTo(pts[0].x,pts[0].y);
for(var i=1;i<pts.length;i++) ctx.lineTo(pts[i].x,pts[i].y);
}
function strokeArc(pts, I, w){
tracePath(pts);
ctx.lineJoin='round'; ctx.lineCap='round';
ctx.strokeStyle=hsl(flareHue, flare*95, 54);
ctx.globalAlpha=I*0.15; ctx.lineWidth=w*8; ctx.stroke();
ctx.strokeStyle=hsl(flareHue, flare*88, 70);
ctx.globalAlpha=I*0.30; ctx.lineWidth=w*3.2; ctx.stroke();
ctx.strokeStyle=hsl(flareHue, flare*35, 97);
ctx.globalAlpha=I*0.95; ctx.lineWidth=w; ctx.stroke();
}
/* the machine itself */
function drawRig(t){
ctx.globalCompositeOperation='source-over';
var colTop=SY+RS*0.55, colBot=D*1.05;
var lx=SX-COLW*0.5, rx=SX+COLW*0.5;
/* base plate */
ctx.fillStyle='#0d0d0d';
ctx.fillRect(SX-D*0.20, D*0.865, D*0.40, D*0.045);
ctx.fillStyle='rgba(255,255,255,0.10)';
ctx.fillRect(SX-D*0.20, D*0.865, D*0.40, 1*S);
/* column */
var cg=ctx.createLinearGradient(lx,0,rx,0);
cg.addColorStop(0,'#000000');
cg.addColorStop(0.42,'#1b1b1b');
cg.addColorStop(0.62,'#0a0a0a');
cg.addColorStop(1,'#000000');
ctx.fillStyle=cg;
ctx.fillRect(lx, colTop, COLW, colBot-colTop);
ctx.fillStyle='rgba(255,255,255,0.14)';
ctx.fillRect(lx, colTop, 1*S, colBot-colTop);
ctx.fillStyle='rgba(255,255,255,0.06)';
ctx.fillRect(rx-1*S, colTop, 1*S, colBot-colTop);
/* charge belt running up inside the column */
ctx.save();
ctx.beginPath(); ctx.rect(lx+2*S, colTop, COLW-4*S, colBot-colTop); ctx.clip();
ctx.globalCompositeOperation='lighter';
var step=D*0.052, off=(t*(calm?60:150)*S)%step;
for(var y=colBot+step; y>colTop-step; y-=step){
var yy=y-off;
var k=1-Math.max(0,Math.min(1,(yy-colTop)/(colBot-colTop)));
ctx.globalAlpha=(0.10+charge*0.22)*(0.35+k*0.65);
ctx.fillStyle=hsl(flareHue, flare*60, 88);
ctx.fillRect(lx+3*S, yy, COLW-6*S, 1.6*S);
}
ctx.globalAlpha=1;
ctx.restore();
/* grounded electrode */
ctx.globalCompositeOperation='source-over';
ctx.fillStyle='#101010';
ctx.fillRect(GX-D*0.011, GY, D*0.022, D*0.62);
ctx.fillStyle='rgba(255,255,255,0.10)';
ctx.fillRect(GX-D*0.011, GY, 1*S, D*0.62);
var ggr=ctx.createRadialGradient(GX-GR*0.4,GY-GR*0.5,GR*0.1,GX,GY,GR);
ggr.addColorStop(0,'#2a2a2a'); ggr.addColorStop(0.65,'#111111'); ggr.addColorStop(1,'#050505');
ctx.beginPath(); ctx.arc(GX,GY,GR,0,TAU); ctx.fillStyle=ggr; ctx.fill();
ctx.lineWidth=1*S; ctx.strokeStyle='rgba(255,255,255,'+(0.22+flash*0.5)+')'; ctx.stroke();
/* dome */
var dgr=ctx.createRadialGradient(SX-RS*0.42,SY-RS*0.5,RS*0.08,SX,SY,RS);
dgr.addColorStop(0,'#343434'); dgr.addColorStop(0.55,'#141414'); dgr.addColorStop(1,'#050505');
ctx.beginPath(); ctx.arc(SX,SY,RS,0,TAU); ctx.fillStyle=dgr; ctx.fill();
/* dome banding */
ctx.save();
ctx.beginPath(); ctx.arc(SX,SY,RS,0,TAU); ctx.clip();
ctx.globalCompositeOperation='lighter';
for(var i=-3;i<=3;i++){
ctx.globalAlpha=0.035;
ctx.fillStyle='#ffffff';
ctx.fillRect(SX-RS, SY+i*RS*0.26, RS*2, 1.2*S);
}
ctx.globalAlpha=1;
ctx.restore();
/* charged rim */
ctx.globalCompositeOperation='lighter';
ctx.beginPath(); ctx.arc(SX,SY,RS,0,TAU);
ctx.lineWidth=1.3*S;
ctx.strokeStyle=hsl(flareHue, flare*60, 92);
ctx.globalAlpha=0.30+charge*0.28+flash*0.4; ctx.stroke();
/* corona halo */
var hg=ctx.createRadialGradient(SX,SY,RS*0.9,SX,SY,RS*(1.9+charge*0.5));
hg.addColorStop(0, hsl(flareHue, flare*95, 60));
hg.addColorStop(1, hsl(flareHue, flare*95, 4));
ctx.globalAlpha=0.07+charge*0.10+flash*0.22;
ctx.fillStyle=hg;
ctx.beginPath(); ctx.arc(SX,SY,RS*(1.9+charge*0.5),0,TAU); ctx.fill();
ctx.globalAlpha=1;
/* charge crawling on the surface */
var nd=3+((charge*6)|0);
for(var d2=0;d2<nd;d2++){
var a=(t*1.7+d2*2.399)%TAU;
var wob=wave[(d2*31+((t*40)|0))%WV]*0.15;
var p={x:SX+Math.cos(a)*RS*(1+wob), y:SY+Math.sin(a)*RS*(1+wob)};
var sz=(1+Math.random()*1.6)*S;
ctx.globalAlpha=0.25+Math.random()*0.5;
ctx.fillStyle=hsl(flareHue, flare*70, 95);
ctx.fillRect(p.x-sz*0.5,p.y-sz*0.5,sz,sz);
}
ctx.globalAlpha=1;
}
function drawStreamers(){
ctx.globalCompositeOperation='lighter';
for(var i=0;i<streamers.length;i++){
strokeArc(streamers[i], 0.22+charge*0.3+Math.random()*0.18, 0.6*S);
}
ctx.globalAlpha=1;
}
function drawArcs(t){
ctx.globalCompositeOperation='lighter';
for(var i=arcs.length-1;i>=0;i--){
var a=arcs[i], age=t-a.born, k=age/a.dur;
if(k>=1){ arcs.splice(i,1); continue; }
var I=Math.pow(1-k,2.2)*(0.62+0.38*Math.sin(age*72+a.seed));
if(I<=0.01) continue;
strokeArc(a.pts, I, a.w);
for(var j=0;j<a.branches.length;j++) strokeArc(a.branches[j], I*0.5, a.w*0.5);
}
ctx.globalAlpha=1;
}
function drawSparks(){
ctx.globalCompositeOperation='lighter';
for(var i=0;i<sparks.length;i++){
var s=sparks[i],k=1-s.age/s.life,sz=(0.8+k*1.6)*S;
ctx.globalAlpha=k*0.85;
ctx.fillStyle=hsl(flareHue, flare*70, 95);
ctx.fillRect(s.x-sz*0.5,s.y-sz*0.5,sz,sz);
}
ctx.globalAlpha=1;
}
/* ---------- background & overlays ---------- */
var bgGrad=null,bgKey='';
function backdrop(amp){
ctx.globalCompositeOperation='source-over';
ctx.fillStyle='#050505'; ctx.fillRect(0,0,D,D);
if(bgKey!==String(D)){
bgGrad=ctx.createRadialGradient(CX,CY,0,CX,CY,D*0.5);
bgGrad.addColorStop(0,'rgba(255,255,255,0.09)');
bgGrad.addColorStop(0.6,'rgba(255,255,255,0.028)');
bgGrad.addColorStop(1,'rgba(255,255,255,0)');
bgKey=String(D);
}
ctx.globalAlpha=0.5+amp*0.1;
ctx.fillStyle=bgGrad; ctx.fillRect(0,0,D,D);
if(flash>0.02||flare>0.02){
var fg=ctx.createRadialGradient(SX,SY,0,SX,SY,D*0.6);
fg.addColorStop(0,hsl(flareHue,flare*90,62));
fg.addColorStop(1,hsl(flareHue,flare*90,26));
ctx.globalCompositeOperation='lighter';
ctx.globalAlpha=flash*0.15+flare*0.05;
ctx.fillStyle=fg; ctx.fillRect(0,0,D,D);
ctx.globalCompositeOperation='source-over';
}
ctx.globalAlpha=1;
}
function glitchPost(){
if(g<=0) return;
var cw=cvs.width,ch=cvs.height;
ctx.save(); ctx.setTransform(1,0,0,1,0,0);
ctx.globalCompositeOperation='source-over';
var n=1+((g*10)|0);
for(var i=0;i<n;i++){
var sy=Math.random()*ch,sh=(2+Math.random()*26)*S*DPR;
var dx=(Math.random()-0.5)*g*70*S*DPR;
ctx.drawImage(cvs,0,sy,cw,sh,dx,sy,cw,sh);
}
var b=((g*6)|0);
for(var j=0;j<b;j++){
var bw=(20+Math.random()*90)*S*DPR,bh=(5+Math.random()*34)*S*DPR;
var sx=Math.random()*Math.max(1,cw-bw),sy2=Math.random()*Math.max(1,ch-bh);
var ox=(Math.random()-0.5)*g*50*S*DPR,oy=(Math.random()-0.5)*g*16*S*DPR;
ctx.drawImage(cvs,sx,sy2,bw,bh,sx+ox,sy2+oy,bw,bh);
}
if(g>0.45){
var sft=g*5*S*DPR;
ctx.globalCompositeOperation='lighter'; ctx.globalAlpha=0.18;
ctx.drawImage(cvs,-sft,0); ctx.drawImage(cvs,sft,0);
ctx.globalAlpha=1;
}
ctx.restore();
}
function signalDrop(){
ctx.save(); ctx.setTransform(1,0,0,1,0,0);
var cw=cvs.width,ch=cvs.height;
ctx.globalCompositeOperation='source-over';
ctx.fillStyle='rgba(5,5,5,0.72)'; ctx.fillRect(0,0,cw,ch);
for(var i=0;i<46;i++){
var y=Math.random()*ch,h=(1+Math.random()*7)*S*DPR;
ctx.globalAlpha=0.08+Math.random()*0.38;
ctx.fillStyle=Math.random()<0.5?'#2a2a2a':'#1a1a1a';
ctx.fillRect(0,y,cw,h);
}
ctx.globalAlpha=1; ctx.restore();
}
var vgGrad=null,vgKey='';
function overlays(){
ctx.save(); ctx.setTransform(1,0,0,1,0,0);
var cw=cvs.width,ch=cvs.height;
ctx.globalCompositeOperation='screen';
ctx.globalAlpha=0.22+g*0.3;
ctx.fillStyle=noiseTiles[(Math.random()*noiseTiles.length)|0];
ctx.save();
ctx.translate((Math.random()*140)|0,(Math.random()*140)|0);
ctx.fillRect(-160,-160,cw+320,ch+320);
ctx.restore();
ctx.globalCompositeOperation='multiply';
ctx.globalAlpha=0.3; ctx.fillStyle=scanPat; ctx.fillRect(0,0,cw,ch);
ctx.globalCompositeOperation='source-over'; ctx.globalAlpha=1;
if(vgKey!==cw+'x'+ch){
vgGrad=ctx.createRadialGradient(cw*0.5,ch*0.5,cw*0.20,cw*0.5,ch*0.5,cw*0.5);
vgGrad.addColorStop(0,'rgba(5,5,5,0)');
vgGrad.addColorStop(0.78,'rgba(5,5,5,0.55)');
vgGrad.addColorStop(1,'rgba(5,5,5,0.94)');
vgKey=cw+'x'+ch;
}
ctx.fillStyle=vgGrad; ctx.fillRect(0,0,cw,ch);
ctx.restore();
}
/* ---------- loop ---------- */
function frame(now){
if(measure()!==D) resize();
var t=(now-t0)/1000;
var dt=Math.min(0.05,(now-last)/1000||0.016); last=now;
var amp=envelope(t);
updateSignal(t, amp);
updateGlitch(t);
updateFlare(t);
updateRig(t, dt, amp);
ctx.setTransform(DPR,0,0,DPR,0,0);
backdrop(amp);
drawRig(t);
drawStreamers();
drawArcs(t);
drawSparks();
glitchPost();
if(t<dropUntil) signalDrop();
overlays();
requestAnimationFrame(frame);
}
resize();
requestAnimationFrame(function(n){ t0=n; last=n; frame(n); });
})();
</script>
</body>
</html>