Show code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Falling Sand — Cellular Automaton</title>
<style>
*{box-sizing:border-box}
body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1.2rem;
background:radial-gradient(1200px 700px at 50% -10%,#141a26,#080a10 70%);color:#e7ebf3;
font:16px/1.5 ui-sans-serif,system-ui,"Segoe UI",Roboto,sans-serif}
.wrap{width:min(980px,100%)}
h1{margin:0 0 .25rem;font-size:clamp(1.05rem,2.6vw,1.5rem);font-weight:650;letter-spacing:.01em}
p.sub{margin:0 0 1rem;color:#8b95a8;font-size:.85rem}
.stats{display:flex;flex-wrap:wrap;gap:.6rem;margin-bottom:.75rem}
.stat{flex:1 1 120px;background:#121722;border:1px solid #232a39;border-radius:12px;padding:.5rem .75rem}
.k{display:block;font-size:.66rem;letter-spacing:.14em;text-transform:uppercase;color:#7d879b}
.v{font:600 1.2rem/1.25 ui-monospace,SFMono-Regular,Menlo,monospace;color:#f3f6fb;font-variant-numeric:tabular-nums}
canvas{display:block;width:100%;aspect-ratio:3/2;background:#05070b;border:1px solid #232a39;
border-radius:12px;image-rendering:pixelated;box-shadow:0 18px 50px -22px #000}
</style>
</head>
<body>
<div class="wrap">
<h1>Falling Sand · 150 × 100 cell automaton</h1>
<p class="sub">Every step each grain moves one cell down, else down-left or down-right, else it rests.</p>
<div class="stats">
<div class="stat"><span class="k">Step</span><span class="v" id="eS">0</span></div>
<div class="stat"><span class="k">Resting cells</span><span class="v" id="eR">0</span></div>
<div class="stat"><span class="k">Grains</span><span class="v" id="eG">0</span></div>
<div class="stat"><span class="k">Run</span><span class="v" id="eN">0</span></div>
</div>
<canvas id="c" width="150" height="100"></canvas>
</div>
<script>
const W=150,H=100,N=W*H;
const cv=document.getElementById("c"),ctx=cv.getContext("2d");
const img=ctx.createImageData(W,H),buf=new Uint32Array(img.data.buffer);
const eS=document.getElementById("eS"),eR=document.getElementById("eR"),
eG=document.getElementById("eG"),eN=document.getElementById("eN");
const grid=new Uint8Array(N),shade=new Uint8Array(N),rest=new Uint8Array(N),pal=new Uint32Array(256);
const BG=0xff0b0705;
let step=0,total=0,resting=0,run=0,src=[],emit=1,cap=0,idle=0,mov=0;
function hsl(h,s,l){s/=100;l/=100;const k=n=>(n+h/30)%12,a=s*Math.min(l,1-l),
f=n=>Math.round(255*(l-a*Math.max(-1,Math.min(k(n)-3,9-k(n),1))));return[f(0),f(8),f(4)];}
function px(h,s,l){const c=hsl(h,s,l);return 4278190080|(c[2]<<16)|(c[1]<<8)|c[0];}
function reset(){
grid.fill(0);shade.fill(0);rest.fill(0);
step=0;total=0;resting=0;idle=0;run++;
const hue=Math.random()*360;
for(let i=0;i<256;i++)pal[i]=px((hue+i/11-11+360)%360,58+i/15,32+i/6.5);
mov=px(hue,95,80);
const k=1+(Math.random()*3|0);src=[];
for(let i=0;i<k;i++)src.push(14+(((W-28)*(k===1?0.5:i/(k-1)))|0));
emit=1+(Math.random()*3|0);
cap=2400+(Math.random()*3400|0);
}
function tick(){
if(total<cap)for(const x of src)for(let i=0;i<emit;i++){
const p=Math.max(0,Math.min(W-1,x+(Math.random()*3|0)-1));
if(!grid[p]){grid[p]=1;shade[p]=Math.random()*256|0;total++;}
}
const dir=step&1;
for(let y=H-2;y>=0;y--)for(let i=0;i<W;i++){
const x=dir?i:W-1-i,p=y*W+x,b=p+W;
if(!grid[p])continue;
let t=-1;
if(!grid[b])t=b;
else{
const dl=x>0&&!grid[b-1],dr=x<W-1&&!grid[b+1];
if(dl&&dr)t=Math.random()<0.5?b-1:b+1;else if(dl)t=b-1;else if(dr)t=b+1;
}
if(t>=0){grid[t]=1;shade[t]=shade[p];grid[p]=0;shade[p]=0;}
}
step++;
let cnt=0;
for(let p=0;p<N;p++){
rest[p]=0;
if(!grid[p])continue;
const y=(p/W)|0,x=p-y*W,b=p+W;
if(y===H-1||(grid[b]&&(x===0||grid[b-1])&&(x===W-1||grid[b+1]))){rest[p]=1;cnt++;}
}
resting=cnt;
let blocked=true;
for(const x of src)if(!grid[x])blocked=false;
if(blocked)idle+=3;
else if(total>=cap&&resting===total)idle+=2;
else idle=0;
if(idle>110)reset();
}
function draw(){
buf.fill(BG);
for(let p=0;p<N;p++)if(grid[p])buf[p]=rest[p]?pal[shade[p]]:mov;
ctx.putImageData(img,0,0);
eS.textContent=step.toLocaleString();
eR.textContent=resting.toLocaleString();
eG.textContent=total.toLocaleString();
eN.textContent=run;
}
let last=0,acc=0;
function frame(t){
requestAnimationFrame(frame);
if(!last)last=t;
acc+=Math.min(120,t-last);last=t;
let k=0;
while(acc>=16.6&&k<4){tick();acc-=16.6;k++;}
draw();
}
reset();requestAnimationFrame(frame);
</script>
</body>
</html>