dots / index.html
JarWithWater's picture
Upload index.html
12e0eae verified
Raw History Blame Contribute Delete
13.7 kB
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Нейроны</title>
<style>
body{margin:0;background:#000;overflow:hidden;font-family:monospace;color:#eee}
canvas{display:block}
.ui{position:fixed;top:10px;left:10px;z-index:10;font-size:12px;background:rgba(0,0,0,.8);padding:10px 14px;border-radius:6px;line-height:1.7}
.ui button{background:#4f46e5;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-family:monospace;font-size:11px;margin:2px}
.ui button:hover{background:#4338ca}
.ui button.active{background:#dc2626}
.stat{color:#7ec8ff}
</style>
</head>
<body>
<div class="ui">
N: <span class="stat" id="count">30k</span> ·
Связей: <span class="stat" id="edges">0</span> ·
Ср.связ/нейрон: <span class="stat" id="avg">0</span> ·
FPS: <span class="stat" id="fps">0</span><br>
<b>Форма:</b>
<button id="btnRandom" onclick="setShape('random')">Рандом</button>
<button id="btnSphere" onclick="setShape('sphere')">Шар</button>
<button id="btnPlane" onclick="setShape('plane')">Плоскость</button>
<button id="btnHalf" onclick="setShape('half')">Полукруг</button>
<button onclick="setN(30000)">30k</button>
<button onclick="setN(60000)">60k</button>
<button onclick="setN(100000)">100k</button><br>
<span style="opacity:.6">тяни — вращать · колесо — зум</span>
</div>
<canvas id="c"></canvas>
<script>
const canvas=document.getElementById('c');
const gl=canvas.getContext('webgl2')||canvas.getContext('webgl');
if(!gl){document.body.innerHTML='<h1>Нет WebGL</h1>';throw 0}
let W=canvas.width=innerWidth,H=canvas.height=innerHeight;
addEventListener('resize',()=>{W=canvas.width=innerWidth;H=canvas.height=innerHeight});
const BOX=1000;
const CONNECT_D=60;
const BREAK_D=CONNECT_D*1.5;
const MAX_CONN=10;
const MIN_DIST=22;
const DAMPING=0.93;
const DT=0.06;
const GRID_CELL=CONNECT_D;
const REBUILD=3;
const ROOT={x:BOX/2,y:15,z:BOX/2};
const ROOT_ATTRACT=650;
const ROOT_FORCE=25000;
const VS_P=`attribute vec3 aPos;uniform mat4 uProj,uView;uniform float uSize;
void main(){gl_Position=uProj*uView*vec4(aPos,1.0);gl_PointSize=uSize;}`;
const FS_P=`precision mediump float;uniform vec3 uColor;
void main(){vec2 d=gl_PointCoord-0.5;float r2=dot(d,d);
if(r2>0.25)discard;gl_FragColor=vec4(uColor,(1.0-r2*4.0)*0.85);}`;
const VS_L=`attribute vec3 aPos;uniform mat4 uProj,uView;
void main(){gl_Position=uProj*uView*vec4(aPos,1.0);}`;
const FS_L=`precision mediump float;uniform vec4 uColor;
void main(){gl_FragColor=uColor;}`;
function sh(t,s){const o=gl.createShader(t);gl.shaderSource(o,s);gl.compileShader(o);
if(!gl.getShaderParameter(o,gl.COMPILE_STATUS))console.error(gl.getShaderInfoLog(o));return o}
function mk(vs,fs){const p=gl.createProgram();gl.attachShader(p,sh(gl.VERTEX_SHADER,vs));
gl.attachShader(p,sh(gl.FRAGMENT_SHADER,fs));gl.linkProgram(p);return p}
const progP=mk(VS_P,FS_P),progL=mk(VS_L,FS_L);
function persp(f,a,n,fa){const t=1/Math.tan(f/2);return new Float32Array(
[t/a,0,0,0,0,t,0,0,0,0,(fa+n)/(n-fa),-1,0,0,(2*fa*n)/(n-fa),0])}
function lookAt(ex,ey,ez,cx,cy,cz,ux,uy,uz){
let zx=ex-cx,zy=ey-cy,zz=ez-cz;const zl=Math.hypot(zx,zy,zz);zx/=zl;zy/=zl;zz/=zl;
let xx=uy*zz-uz*zy,xy=uz*zx-ux*zz,xz=ux*zy-uy*zx;
const xl=Math.hypot(xx,xy,xz);xx/=xl;xy/=xl;xz/=xl;
const yx=zy*xz-zz*xy,yy=zz*xx-zx*xz,yz=zx*xy-zy*xx;
return new Float32Array([xx,yx,zx,0,xy,yy,zy,0,xz,yz,zz,0,
-(xx*ex+xy*ey+xz*ez),-(yx*ex+yy*ey+yz*ez),-(zx*ex+zy*ey+zz*ez),1])}
let N=30000;
let pos,vel,connCount,connList;
let edges=new Float32Array(0),edgeCount=0;
let posBuf,edgeBuf,rootBuf,boxBuf;
let curShape='random';
let forceBuf=null;
function initShape(shape){
curShape=shape;
forceBuf=null;
pos=new Float32Array(N*3);vel=new Float32Array(N*3);
connCount=new Int32Array(N);
connList=new Int32Array(N*MAX_CONN).fill(-1);
const cx=BOX/2,cy=BOX/2,cz=BOX/2;
for(let i=0;i<N;i++){
let x,y,z;
if(shape==='sphere'){
const u=Math.random()*2-1,phi=Math.random()*Math.PI*2,r=180*Math.cbrt(Math.random());
const s=Math.sqrt(1-u*u);
x=cx+r*s*Math.cos(phi);y=cy+r*u;z=cz+r*s*Math.sin(phi);
}else if(shape==='plane'){
x=cx+(Math.random()-0.5)*400;y=cy+(Math.random()-0.5)*400;z=cz+(Math.random()-0.5)*15;
}else if(shape==='half'){
const a=Math.random()*Math.PI,r=250*Math.sqrt(Math.random());
x=cx+Math.cos(a)*r;y=cy-100+Math.random()*20;z=cz+Math.sin(a)*r;
}else{
x=Math.random()*BOX;y=Math.random()*BOX;z=Math.random()*BOX;
}
pos[i*3]=x;pos[i*3+1]=y;pos[i*3+2]=z;
}
if(!posBuf)posBuf=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,posBuf);
gl.bufferData(gl.ARRAY_BUFFER,pos,gl.DYNAMIC_DRAW);
if(!edgeBuf)edgeBuf=gl.createBuffer();
edges=new Float32Array(N*MAX_CONN*6);edgeCount=0;
if(rootBuf)gl.deleteBuffer(rootBuf);
rootBuf=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,rootBuf);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([ROOT.x,ROOT.y,ROOT.z]),gl.STATIC_DRAW);
if(!boxBuf)boxBuf=gl.createBuffer();
const b=BOX;
const bv=new Float32Array([
0,0,0, b,0,0, b,0,0, b,0,b, b,0,b, 0,0,b, 0,0,b, 0,0,0,
0,b,0, b,b,0, b,b,0, b,b,b, b,b,b, 0,b,b, 0,b,b, 0,b,0,
0,0,0, 0,b,0, b,0,0, b,b,0, b,0,b, b,b,b, 0,0,b, 0,b,b
]);
gl.bindBuffer(gl.ARRAY_BUFFER,boxBuf);
gl.bufferData(gl.ARRAY_BUFFER,bv,gl.STATIC_DRAW);
}
function setShape(s){
document.querySelectorAll('.ui button').forEach(b=>b.classList.remove('active'));
const btn=document.getElementById('btn'+s[0].toUpperCase()+s.slice(1));
if(btn)btn.classList.add('active');
initShape(s);
}
function setN(n){
N=n;
document.getElementById('count').textContent=(n/1000).toFixed(0)+'k';
initShape(curShape);
}
let grid,gridDims;
function buildGrid(){
const cells=Math.ceil(BOX/GRID_CELL)+1;
gridDims=cells;
grid=new Array(cells*cells*cells);
for(let i=0;i<N;i++){
const gx=Math.floor(pos[i*3]/GRID_CELL);
const gy=Math.floor(pos[i*3+1]/GRID_CELL);
const gz=Math.floor(pos[i*3+2]/GRID_CELL);
const k=gx+cells*(gy+cells*gz);
if(!grid[k])grid[k]=[];
grid[k].push(i);
}
}
function key(x,y,z){return x+gridDims*(y+gridDims*z)}
function addConn(i,j){
if(i===j)return false;
if(connCount[i]>=MAX_CONN||connCount[j]>=MAX_CONN)return false;
for(let c=0;c<connCount[i];c++){
if(connList[i*MAX_CONN+c]===j)return false;
}
connList[i*MAX_CONN+connCount[i]]=j;connCount[i]++;
connList[j*MAX_CONN+connCount[j]]=i;connCount[j]++;
return true;
}
function physics(){
if(!forceBuf||forceBuf.length!==N*3)forceBuf=new Float32Array(N*3);
const force=forceBuf;
force.fill(0);
for(let i=0;i<N;i++){
const dx=ROOT.x-pos[i*3],dy=ROOT.y-pos[i*3+1],dz=ROOT.z-pos[i*3+2];
const d2=dx*dx+dy*dy+dz*dz+2000;
const d=Math.sqrt(d2);
if(d<ROOT_ATTRACT){
const f=ROOT_FORCE/d2;
force[i*3]+=dx*f;force[i*3+1]+=dy*f;force[i*3+2]+=dz*f;
}
}
for(let i=0;i<N;i++){
const px=pos[i*3],py=pos[i*3+1],pz=pos[i*3+2];
let fx=0,fy=0,fz=0;
for(let c=0;c<connCount[i];c++){
const j=connList[i*MAX_CONN+c];
const dx=pos[j*3]-px,dy=pos[j*3+1]-py,dz=pos[j*3+2]-pz;
const d=Math.sqrt(dx*dx+dy*dy+dz*dz)+0.001;
if(d>=BREAK_D)continue;
const diff=d-MIN_DIST;
const w=1.0+connCount[j]*0.1;
const pull=w*0.5*diff;
fx+=(dx/d)*pull;fy+=(dy/d)*pull;fz+=(dz/d)*pull;
}
const speedMul=1+connCount[i]*0.15;
force[i*3]+=fx*speedMul;force[i*3+1]+=fy*speedMul;force[i*3+2]+=fz*speedMul;
}
buildGrid();
for(let i=0;i<N;i++){
const px=pos[i*3],py=pos[i*3+1],pz=pos[i*3+2];
const gx=Math.floor(px/GRID_CELL),gy=Math.floor(py/GRID_CELL),gz=Math.floor(pz/GRID_CELL);
let checked=0;
for(let dx=-1;dx<=1&&checked<50;dx++)
for(let dy=-1;dy<=1&&checked<50;dy++)
for(let dz=-1;dz<=1&&checked<50;dz++){
const nx=gx+dx,ny=gy+dy,nz=gz+dz;
if(nx<0||ny<0||nz<0||nx>=gridDims||ny>=gridDims||nz>=gridDims)continue;
const bucket=grid[key(nx,ny,nz)];
if(!bucket)continue;
for(let k=0;k<bucket.length;k++){
const j=bucket[k];
if(j===i)continue;
const ddx=pos[j*3]-px,ddy=pos[j*3+1]-py,ddz=pos[j*3+2]-pz;
const d2=ddx*ddx+ddy*ddy+ddz*ddz;
if(d2>MIN_DIST*MIN_DIST)continue;
const d=Math.sqrt(d2)+0.001;
const push=(MIN_DIST-d)*3;
force[i*3]-=(ddx/d)*push;force[i*3+1]-=(ddy/d)*push;force[i*3+2]-=(ddz/d)*push;
checked++;
if(checked>50)break;
}
}
}
for(let i=0;i<N;i++){
let vx=vel[i*3]+force[i*3]*DT;
let vy=vel[i*3+1]+force[i*3+1]*DT;
let vz=vel[i*3+2]+force[i*3+2]*DT;
vx*=DAMPING;vy*=DAMPING;vz*=DAMPING;
let nx=pos[i*3]+vx*DT,ny=pos[i*3+1]+vy*DT,nz=pos[i*3+2]+vz*DT;
if(nx<0){nx=0;vx*=-0.5}if(nx>BOX){nx=BOX;vx*=-0.5}
if(ny<0){ny=0;vy*=-0.5}if(ny>BOX){ny=BOX;vy*=-0.5}
if(nz<0){nz=0;vz*=-0.5}if(nz>BOX){nz=BOX;vz*=-0.5}
pos[i*3]=nx;pos[i*3+1]=ny;pos[i*3+2]=nz;
vel[i*3]=vx;vel[i*3+1]=vy;vel[i*3+2]=vz;
}
}
function breakLongEdges(){
const B2=BREAK_D*BREAK_D;
for(let i=0;i<N;i++){
let w=0;
for(let c=0;c<connCount[i];c++){
const j=connList[i*MAX_CONN+c];
const dx=pos[j*3]-pos[i*3],dy=pos[j*3+1]-pos[i*3+1],dz=pos[j*3+2]-pos[i*3+2];
if(dx*dx+dy*dy+dz*dz>B2)continue;
connList[i*MAX_CONN+w]=j;w++;
}
for(let c=w;c<MAX_CONN;c++)connList[i*MAX_CONN+c]=-1;
connCount[i]=w;
}
}
function findNewEdges(){
buildGrid();
for(let i=0;i<N;i++){
if(connCount[i]>=MAX_CONN)continue;
const px=pos[i*3],py=pos[i*3+1],pz=pos[i*3+2];
const gx=Math.floor(px/GRID_CELL),gy=Math.floor(py/GRID_CELL),gz=Math.floor(pz/GRID_CELL);
outer:
for(let dx=-1;dx<=1;dx++)
for(let dy=-1;dy<=1;dy++)
for(let dz=-1;dz<=1;dz++){
if(connCount[i]>=MAX_CONN)break outer;
const nx=gx+dx,ny=gy+dy,nz=gz+dz;
if(nx<0||ny<0||nz<0||nx>=gridDims||ny>=gridDims||nz>=gridDims)continue;
const bucket=grid[key(nx,ny,nz)];
if(!bucket)continue;
for(let k=0;k<bucket.length;k++){
if(connCount[i]>=MAX_CONN)break outer;
const j=bucket[k];
if(j<=i)continue;
if(connCount[j]>=MAX_CONN)continue;
const ddx=pos[j*3]-px,ddy=pos[j*3+1]-py,ddz=pos[j*3+2]-pz;
if(ddx*ddx+ddy*ddy+ddz*ddz>CONNECT_D*CONNECT_D)continue;
addConn(i,j);
}
}
}
}
function rebuildEdges(){
edgeCount=0;
let ev=0;
const maxE=edges.length/6;
for(let i=0;i<N&&ev<maxE;i++){
for(let c=0;c<connCount[i];c++){
const j=connList[i*MAX_CONN+c];
if(j<i)continue;
edges[ev++]=pos[i*3];edges[ev++]=pos[i*3+1];edges[ev++]=pos[i*3+2];
edges[ev++]=pos[j*3];edges[ev++]=pos[j*3+1];edges[ev++]=pos[j*3+2];
edgeCount++;
if(ev>=maxE)break;
}
if(ev>=maxE)break;
}
gl.bindBuffer(gl.ARRAY_BUFFER,edgeBuf);
gl.bufferSubData(gl.ARRAY_BUFFER,0,edges.subarray(0,ev));
}
let camAngle=0.7,camElev=0.4,camDist=2200;
let drag=false,lx=0,ly=0;
canvas.addEventListener('mousedown',e=>{drag=true;lx=e.clientX;ly=e.clientY});
canvas.addEventListener('mouseup',()=>drag=false);
canvas.addEventListener('mousemove',e=>{
if(!drag)return;
camAngle+=(e.clientX-lx)*0.006;
camElev=Math.max(-1.4,Math.min(1.4,camElev+(e.clientY-ly)*0.006));
lx=e.clientX;ly=e.clientY;
});
canvas.addEventListener('wheel',e=>{
camDist=Math.max(400,Math.min(5000,camDist+e.deltaY*1.5));
},{passive:true});
let frames=0,lastFps=performance.now();
function draw(){
gl.viewport(0,0,W,H);
gl.clearColor(0,0,0,1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA,gl.ONE);
const proj=persp(Math.PI/3,W/H,1,8000);
const cx=BOX/2,cy=BOX/2,cz=BOX/2;
const ex=cx+Math.cos(camAngle)*Math.cos(camElev)*camDist;
const ey=cy+Math.sin(camElev)*camDist;
const ez=cz+Math.sin(camAngle)*Math.cos(camElev)*camDist;
const view=lookAt(ex,ey,ez,cx,cy,cz,0,1,0);
// Коробка
gl.useProgram(progL);
gl.uniformMatrix4fv(gl.getUniformLocation(progL,'uProj'),false,proj);
gl.uniformMatrix4fv(gl.getUniformLocation(progL,'uView'),false,view);
gl.uniform4f(gl.getUniformLocation(progL,'uColor'),0.3,0.5,0.8,0.35);
gl.bindBuffer(gl.ARRAY_BUFFER,boxBuf);
const aPL=gl.getAttribLocation(progL,'aPos');
gl.enableVertexAttribArray(aPL);
gl.vertexAttribPointer(aPL,3,gl.FLOAT,false,0,0);
gl.drawArrays(gl.LINES,0,24);
// Связи
if(edgeCount>0){
gl.uniform4f(gl.getUniformLocation(progL,'uColor'),0.35,0.55,1.0,0.12);
gl.bindBuffer(gl.ARRAY_BUFFER,edgeBuf);
gl.vertexAttribPointer(aPL,3,gl.FLOAT,false,0,0);
gl.drawArrays(gl.LINES,0,edgeCount*2);
}
// Точки
gl.useProgram(progP);
gl.uniformMatrix4fv(gl.getUniformLocation(progP,'uProj'),false,proj);
gl.uniformMatrix4fv(gl.getUniformLocation(progP,'uView'),false,view);
gl.uniform1f(gl.getUniformLocation(progP,'uSize'),N>60000?1.2:2.2);
gl.uniform3f(gl.getUniformLocation(progP,'uColor'),0.55,0.78,1.0);
gl.bindBuffer(gl.ARRAY_BUFFER,posBuf);
gl.bufferSubData(gl.ARRAY_BUFFER,0,pos);
const aP2=gl.getAttribLocation(progP,'aPos');
gl.enableVertexAttribArray(aP2);
gl.vertexAttribPointer(aP2,3,gl.FLOAT,false,0,0);
gl.drawArrays(gl.POINTS,0,N);
// Ствол
gl.uniform1f(gl.getUniformLocation(progP,'uSize'),20.0);
gl.uniform3f(gl.getUniformLocation(progP,'uColor'),1.0,0.3,0.4);
gl.bindBuffer(gl.ARRAY_BUFFER,rootBuf);
gl.vertexAttribPointer(aP2,3,gl.FLOAT,false,0,0);
gl.drawArrays(gl.POINTS,0,1);
frames++;
const now=performance.now();
if(now-lastFps>500){
document.getElementById('fps').textContent=Math.round(frames*1000/(now-lastFps));
document.getElementById('edges').textContent=edgeCount.toLocaleString();
let total=0;for(let i=0;i<N;i++)total+=connCount[i];
document.getElementById('avg').textContent=(total/N).toFixed(1);
frames=0;lastFps=now;
}
}
initShape('random');
document.getElementById('btnRandom').classList.add('active');
let frameCount=0;
(function loop(){
physics();
breakLongEdges();
if(frameCount++%REBUILD===0)findNewEdges();
rebuildEdges();
draw();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>