suvadityamuk/devfest-geometry-lab
0
1<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Matter, sampled · Geometry Lab</title><style>2:root{color-scheme:dark;--bg:#080f20;--panel:#111d35;--ink:#edf3ff;--muted:#a5b5d1;--coral:#ff927c;--blue:#76bcff}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.55 system-ui,sans-serif}main{max-width:1440px;margin:auto;padding:24px 5vw}header{display:flex;justify-content:space-between;gap:30px;align-items:start}.eyebrow{font:12px/1.4 monospace;letter-spacing:.18em;text-transform:uppercase;color:var(--coral)}h1{font-size:clamp(36px,4vw,52px);line-height:1.02;letter-spacing:-.065em;margin:12px 0}p{color:var(--muted);max-width:720px}.lede{font-size:16px;max-width:850px;margin:10px 0}.badge{border:1px solid #31415e;border-radius:30px;padding:8px 16px;font:12px monospace;white-space:nowrap}.lab{display:grid;grid-template-columns:minmax(0,1fr) 290px;border:1px solid #2b3954;border-radius:20px;overflow:hidden;margin-top:22px;background:var(--panel)}.stage{position:relative;min-height:490px;background:radial-gradient(ellipse at center,#132b48,#080f20)}canvas{display:block;width:100%;height:490px;touch-action:none;cursor:grab}canvas:active{cursor:grabbing}.stage-label{position:absolute;top:22px;left:24px;pointer-events:none;font:12px monospace;color:var(--blue)}.hint{position:absolute;bottom:18px;left:24px;font-size:12px;color:var(--muted);pointer-events:none}.controls{padding:26px;border-left:1px solid #2b3954}h2{font-size:20px;letter-spacing:-.025em}button{font:inherit;color:inherit;background:#1a2a45;border:1px solid #3b506f;border-radius:8px;padding:10px 13px;cursor:pointer}button:hover{border-color:var(--blue)}button[aria-pressed=true]{background:var(--coral);color:#111a2b;border-color:var(--coral)}button:focus-visible,a:focus-visible,input:focus-visible{outline:3px solid #fff;outline-offset:4px}.modes{display:grid;gap:8px}.modes button{text-align:left}.control{margin-top:24px}label{display:flex;justify-content:space-between;font-size:13px}input{width:100%;accent-color:var(--coral);margin:12px 0 0}.small{font-size:12px}.metric{margin:24px 0;color:var(--blue);font:14px monospace}.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:26px;margin:32px 0}.card{border-top:1px solid #34435d;padding-top:12px}.card p{font-size:14px}.number{color:var(--coral);font:12px monospace}.note{padding:22px 26px;background:#101a2d;border-radius:12px;font-size:14px}a{color:var(--blue)}footer{display:flex;flex-wrap:wrap;justify-content:space-between;gap:16px;margin:30px 0;color:var(--muted);font-size:12px}@media(max-width:850px){header{display:block}.badge{display:inline-block}.lab{grid-template-columns:1fr}.controls{border-left:0;border-top:1px solid #2b3954}.modes{grid-template-columns:repeat(3,1fr)}.cards{grid-template-columns:1fr}canvas{height:430px}.stage{min-height:430px}}3</style></head><body><main><header><div><div class="eyebrow">Spatial intelligence / Geometry lab</div><h1>Matter, sampled.</h1><p class="lede">One surface. Three ways to describe it. Explore how a cloud of coordinates becomes connected geometry—or a field of soft, overlapping light.</p></div><span class="badge">INTERACTIVE FIELD NOTES · 01</span></header><section class="lab" aria-label="Interactive 3D representation explorer"><div class="stage"><div class="stage-label" id="stageLabel">01 / POINT CLOUD</div><canvas id="scene" tabindex="0" aria-label="Rotating torus shown as a point cloud. Drag or use arrow keys to rotate. Use representation buttons to switch rendering."></canvas><div class="hint">DRAG TO ORBIT · ARROW KEYS TO ROTATE · SPACE TO PAUSE</div></div><aside class="controls"><h2>Choose a representation</h2><div class="modes"><button aria-pressed="true" data-mode="points">01 Point cloud</button><button aria-pressed="false" data-mode="mesh">02 Triangle mesh</button><button aria-pressed="false" data-mode="splats">03 Gaussian splats</button></div><div class="control"><label for="density">Sampling density <output id="densityOut">32 × 16</output></label><input id="density" type="range" min="12" max="64" step="4" value="32"></div><div class="control"><label for="size">Splat footprint <output id="sizeOut">18 px</output></label><input id="size" type="range" min="4" max="40" value="18"></div><div class="control"><label for="opacity">Splat opacity <output id="opacityOut">65%</output></label><input id="opacity" type="range" min="10" max="100" value="65"></div><p class="small">Footprint and opacity affect splat mode. Try sparse samples with a wide footprint, then tighten the splats.</p><div class="metric" id="metric"></div><button id="motion" aria-pressed="true">Pause rotation</button> <button id="reset" style="margin-top:8px">Reset view</button><p id="description" class="small" aria-live="polite"></p></aside></section><section class="cards"><article class="card"><span class="number">01 / POSITION</span><h2>Points say “where.”</h2><p>Each sample stores a location in 3D. A point cloud has no inherent edges or faces. More samples describe finer detail, but a surface must still be inferred.</p></article><article class="card"><span class="number">02 / CONNECTIVITY</span><h2>Meshes say “connected.”</h2><p>Triangles connect neighboring vertices into an explicit surface. This view exposes that connectivity as a wireframe: each four-corner patch becomes two triangles.</p></article><article class="card"><span class="number">03 / APPEARANCE</span><h2>Splats say “contribute.”</h2><p>Soft elliptical footprints overlap and blend. Real 3D Gaussian splatting learns positions, covariances, opacity and view-dependent color from photographs.</p></article></section><div class="note"><strong>What this experiment does—and does not—show.</strong><p style="margin-bottom:0">All three views use the same procedurally sampled torus. The splats here are illustrative, screen-space Gaussian ellipses composited back to front. They are not trained 3D Gaussians: this demo does not project learned 3D covariance, optimize against photographs, or reproduce a production 3DGS renderer. The wireframe reveals topology rather than rendering an opaque surface.</p></div><footer><span>Built for exploration · Runs entirely in your browser · No uploads</span><a href="https://repo-sam.inria.fr/fungraph/3d-gaussian-splatting/" target="_blank" rel="noopener noreferrer">Read the original 3D Gaussian Splatting work ↗</a><nav aria-label="Companion labs"><a href="https://huggingface.co/spaces/suvadityamuk/devfest-video-lab" target="_blank" rel="noopener noreferrer">Video lab ↗</a> · <a href="https://huggingface.co/spaces/suvadityamuk/devfest-world-lab" target="_blank" rel="noopener noreferrer">World lab ↗</a> · <a href="https://docs.google.com/presentation/d/14ZYt0JO8AFpGjzvV3Jahe9h74ki1ir3tD_MqVDX8JlU" target="_blank" rel="noopener noreferrer">Talk slides ↗</a></nav></footer></main><script>4'use strict';5const canvas=document.querySelector('canvas'),ctx=canvas.getContext('2d');6const density=document.querySelector('#density'),size=document.querySelector('#size'),opacity=document.querySelector('#opacity');7const descriptions={points:'A sparse set of locations. Rotate the object: without edges, depth depends on motion, shading and overlap.',mesh:'The same samples, now connected. Diagonal edges split each periodic surface patch into two triangles.',splats:'The same samples, now soft footprints. Adjust opacity and size to see holes fill and overlapping contributions accumulate.'};8let mode='points',yaw=.5,pitch=.45,running=!matchMedia('(prefers-reduced-motion: reduce)').matches,points=[],nu=32,nv=16,width=800,height=570,last=0,drag=null;9function build(){nu=+density.value;nv=nu/2;points=[];for(let i=0;i<nu;i++)for(let j=0;j<nv;j++){const u=i/nu*Math.PI*2,v=j/nv*Math.PI*2,r=1.48+.57*Math.cos(v);points.push({x:r*Math.cos(u),y:.57*Math.sin(v),z:r*Math.sin(u),u,v});}document.querySelector('#densityOut').textContent=`${nu} × ${nv}`;update();}10function update(){document.querySelector('#sizeOut').textContent=size.value+' px';document.querySelector('#opacityOut').textContent=opacity.value+'%';document.querySelector('#metric').textContent=mode==='mesh'?`${points.length.toLocaleString()} vertices / ${(2*points.length).toLocaleString()} triangles`:`${points.length.toLocaleString()} ${mode==='points'?'samples':'Gaussian footprints'}`;document.querySelector('#description').textContent=descriptions[mode];document.querySelector('#stageLabel').textContent={points:'01 / POINT CLOUD',mesh:'02 / TRIANGLE MESH',splats:'03 / GAUSSIAN SPLATS'}[mode];canvas.setAttribute('aria-label',`Rotating torus shown as ${mode}. Drag or use arrow keys to rotate.`);}11function resize(){const r=canvas.getBoundingClientRect(),dpr=Math.min(devicePixelRatio||1,2);width=r.width;height=r.height;canvas.width=Math.round(width*dpr);canvas.height=Math.round(height*dpr);ctx.setTransform(dpr,0,0,dpr,0,0);}12function project(p){const x=p.x*Math.cos(yaw)+p.z*Math.sin(yaw),z=-p.x*Math.sin(yaw)+p.z*Math.cos(yaw),y=p.y*Math.cos(pitch)-z*Math.sin(pitch),depth=p.y*Math.sin(pitch)+z*Math.cos(pitch),s=Math.min(width,height)*.94/(5.8+depth);return{x:width/2+x*s,y:height/2+y*s,z:depth,s};}13function render(t){if(running&&!drag)yaw+=Math.min((t-last)/1000,.05)*.22;last=t;ctx.clearRect(0,0,width,height);ctx.strokeStyle='#39507435';ctx.lineWidth=1;for(let i=-4;i<=4;i++){ctx.beginPath();ctx.moveTo(width/2+i*50,height*.78);ctx.lineTo(width/2+i*100,height);ctx.stroke();}const pp=points.map(project);if(mode==='mesh'){ctx.lineWidth=.8;for(let i=0;i<nu;i++)for(let j=0;j<nv;j++){const a=pp[i*nv+j],b=pp[((i+1)%nu)*nv+j],c=pp[i*nv+(j+1)%nv],d=pp[((i+1)%nu)*nv+(j+1)%nv];ctx.strokeStyle=`rgba(118,188,255,${.18+(.65*(2.1-a.z)/4.2)})`;ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.lineTo(c.x,c.y);ctx.lineTo(a.x,a.y);ctx.moveTo(b.x,b.y);ctx.lineTo(d.x,d.y);ctx.stroke();}}else{const order=pp.map((p,i)=>({...p,i})).sort((a,b)=>b.z-a.z);for(const p of order){const warm=(Math.sin(points[p.i].u)+1)/2,r=Math.round(118+137*warm),g=Math.round(188-42*warm),b=Math.round(255-131*warm);if(mode==='points'){ctx.fillStyle=`rgba(${r},${g},${b},${.4+.6*(2.1-p.z)/4.2})`;ctx.beginPath();ctx.arc(p.x,p.y,Math.max(1.1,p.s*.018),0,Math.PI*2);ctx.fill();}else{const radius=+size.value*p.s/90;ctx.save();ctx.translate(p.x,p.y);ctx.rotate(points[p.i].u+yaw);ctx.scale(1,.64);const gradient=ctx.createRadialGradient(0,0,0,0,0,radius);const alpha=+opacity.value/100;gradient.addColorStop(0,`rgba(${r},${g},${b},${alpha})`);gradient.addColorStop(.45,`rgba(${r},${g},${b},${alpha*.37})`);gradient.addColorStop(1,`rgba(${r},${g},${b},0)`);ctx.fillStyle=gradient;ctx.fillRect(-radius,-radius,radius*2,radius*2);ctx.restore();}}}requestAnimationFrame(render);}14function motion(){document.querySelector('#motion').textContent=running?'Pause rotation':'Play rotation';document.querySelector('#motion').setAttribute('aria-pressed',String(running));}15document.querySelectorAll('[data-mode]').forEach(button=>button.addEventListener('click',()=>{mode=button.dataset.mode;document.querySelectorAll('[data-mode]').forEach(b=>b.setAttribute('aria-pressed',String(b===button)));update();}));density.addEventListener('input',build);size.addEventListener('input',update);opacity.addEventListener('input',update);document.querySelector('#motion').onclick=()=>{running=!running;motion();};document.querySelector('#reset').onclick=()=>{yaw=.5;pitch=.45;};canvas.addEventListener('pointerdown',e=>{drag={x:e.clientX,y:e.clientY};canvas.setPointerCapture(e.pointerId);});canvas.addEventListener('pointermove',e=>{if(!drag)return;yaw+=(e.clientX-drag.x)*.008;pitch=Math.max(-1.5,Math.min(1.5,pitch+(e.clientY-drag.y)*.008));drag={x:e.clientX,y:e.clientY};});['pointerup','pointercancel','lostpointercapture'].forEach(event=>canvas.addEventListener(event,()=>drag=null));canvas.addEventListener('keydown',e=>{if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown',' '].includes(e.key))e.preventDefault();if(e.key==='ArrowLeft')yaw-=.1;if(e.key==='ArrowRight')yaw+=.1;if(e.key==='ArrowUp')pitch=Math.max(-1.5,pitch-.1);if(e.key==='ArrowDown')pitch=Math.min(1.5,pitch+.1);if(e.key===' '){running=!running;motion();}});new ResizeObserver(resize).observe(canvas);build();resize();motion();requestAnimationFrame(render);16</script></body></html>17 