DotGrid
~1.4 KB JSmove the cursor · click to ripple
<!-- DotGrid · interactive spring dot-grid · vanilla · no eval/no third-party · MIT -->
<div id="dotgrid" style="position:relative;width:100%;height:280px"></div>
<script>
(function(){
var host=document.getElementById('dotgrid'),cv=document.createElement('canvas');
host.appendChild(cv);var ctx=cv.getContext('2d');if(!ctx)return;
var GAP=26,DOT=2.4,PROX=110,ACCENT=[124,140,255],BASE=[130,138,160],
reduce=matchMedia('(prefers-reduced-motion:reduce)').matches,
fine=matchMedia('(pointer:fine)').matches,
dpr=Math.min(devicePixelRatio||1,2),W,H,cols,rows,sx,sy,dots=[],px=-9e9,py=-9e9,raf,run=false,last=0;
function build(){var r=host.getBoundingClientRect();W=r.width;H=r.height;
cv.width=W*dpr|0;cv.height=H*dpr|0;cv.style.width=W+'px';cv.style.height=H+'px';ctx.setTransform(dpr,0,0,dpr,0,0);
cols=Math.floor(W/GAP);rows=Math.floor(H/GAP);sx=(W-(cols-1)*GAP)/2;sy=(H-(rows-1)*GAP)/2;dots=[];
for(var y=0;y<rows;y++)for(var x=0;x<cols;x++)dots.push({x:sx+x*GAP,y:sy+y*GAP,ox:0,oy:0,vx:0,vy:0});}
function draw(){ctx.clearRect(0,0,W,H);for(var i=0;i<dots.length;i++){var p=dots[i],dx=p.x-px,dy=p.y-py,
dd=dx*dx+dy*dy,t=dd<PROX*PROX?1-Math.sqrt(dd)/PROX:0;
ctx.fillStyle=t?'rgba('+((BASE[0]+(ACCENT[0]-BASE[0])*t)|0)+','+((BASE[1]+(ACCENT[1]-BASE[1])*t)|0)+','+((BASE[2]+(ACCENT[2]-BASE[2])*t)|0)+','+(0.18+0.72*t)+')':'rgba(130,138,160,.18)';
ctx.beginPath();ctx.arc(p.x+p.ox,p.y+p.oy,DOT,0,6.283);ctx.fill();}}
function step(){var m=false;for(var i=0;i<dots.length;i++){var p=dots[i];if(p.ox||p.oy||p.vx||p.vy){
p.vx+=(-170*p.ox-11*p.vx)/60;p.vy+=(-170*p.oy-11*p.vy)/60;p.ox+=p.vx/60;p.oy+=p.vy/60;
if(Math.abs(p.ox)<.05&&Math.abs(p.oy)<.05&&Math.abs(p.vx)<.6){p.ox=p.oy=p.vx=p.vy=0;}else m=true;}}
draw();if(m||performance.now()-last<120)raf=requestAnimationFrame(step);else run=false;}
function kick(){if(!run){run=true;raf=requestAnimationFrame(step);}}
build();draw();addEventListener('resize',function(){build();draw();},{passive:true});
if(!reduce&&fine){
host.addEventListener('pointermove',function(e){var r=cv.getBoundingClientRect();px=e.clientX-r.left;py=e.clientY-r.top;last=performance.now();kick();});
host.addEventListener('pointerdown',function(e){var r=cv.getBoundingClientRect(),cx=e.clientX-r.left,cy=e.clientY-r.top;
for(var i=0;i<dots.length;i++){var p=dots[i],dx=p.x-cx,dy=p.y-cy,dist=Math.hypot(dx,dy);
if(dist<160){var f=1-dist/160,n=dist||1;p.vx+=(dx/n)*420*f*f;p.vy+=(dy/n)*420*f*f;}}last=performance.now();kick();});}
})();
</script>