// Recreates the dense particle-cloud visual from the Financial Knowledge Graph // screenshot. Static visual — no controls wired up — but it pans and rotates // slowly so it feels alive and you can grab it with the mouse. window.ViewerScene = function ViewerScene() { const mountRef = React.useRef(null); React.useEffect(() => { const THREE = window.THREE; const mount = mountRef.current; if (!THREE || !mount) return; const w = () => mount.clientWidth; const h = () => mount.clientHeight; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(50, w() / h(), 0.1, 1000); camera.position.set(0, 0, 22); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setSize(w(), h()); renderer.setClearColor(0x000000, 0); mount.appendChild(renderer.domElement); const root = new THREE.Group(); scene.add(root); const palette = [ new THREE.Color('#7c7cff'), new THREE.Color('#ff6d6d'), new THREE.Color('#ffd66d'), new THREE.Color('#ff9a5b'), new THREE.Color('#ff6dc0'), new THREE.Color('#5ed694'), ]; const weights = [0.66, 0.13, 0.04, 0.10, 0.04, 0.03]; function pickColor(r) { let a = 0; for (let i = 0; i < weights.length; i++) { a += weights[i]; if (r < a) return palette[i]; } return palette[0]; } // Big point cloud: a dense core + halo, matching the screenshot's "blown // dandelion" look. const N = 13852; // matches the entity count from the screenshot, cute easter egg const pos = new Float32Array(N * 3); const col = new Float32Array(N * 3); const sz = new Float32Array(N); const cache = new Float32Array(N * 3); for (let i = 0; i < N; i++) { const layer = Math.random(); let r; if (layer < 0.50) r = Math.pow(Math.random(), 1.6) * 3.5; // core else if (layer < 0.92) r = 3.5 + Math.pow(Math.random(), 0.8) * 7.5; // mid else r = 11 + Math.pow(Math.random(), 1.4) * 8; // halo const t = Math.random() * Math.PI * 2; const p = Math.acos(2 * Math.random() - 1); const x = r * Math.sin(p) * Math.cos(t); const y = r * Math.sin(p) * Math.sin(t); const z = r * Math.cos(p); pos[i*3] = x; pos[i*3+1] = y; pos[i*3+2] = z; cache[i*3] = x; cache[i*3+1] = y; cache[i*3+2] = z; const c = pickColor(Math.random()); col[i*3] = c.r; col[i*3+1] = c.g; col[i*3+2] = c.b; sz[i] = layer < 0.5 ? 0.025 + Math.random() * 0.025 : 0.018 + Math.random() * 0.02; } const geom = new THREE.BufferGeometry(); geom.setAttribute('position', new THREE.BufferAttribute(pos, 3)); geom.setAttribute('color', new THREE.BufferAttribute(col, 3)); geom.setAttribute('size', new THREE.BufferAttribute(sz, 1)); const mat = new THREE.ShaderMaterial({ uniforms: { uPx: { value: renderer.getPixelRatio() } }, vertexShader: ` attribute float size; varying vec3 vColor; uniform float uPx; void main(){ vColor = color; vec4 mv = modelViewMatrix * vec4(position,1.0); gl_PointSize = size * (400.0 / -mv.z) * uPx; gl_Position = projectionMatrix * mv; }`, fragmentShader: ` varying vec3 vColor; void main(){ vec2 c = gl_PointCoord - 0.5; float d = length(c); float a = smoothstep(0.5,0.1,d); if(a < 0.02) discard; float core = smoothstep(0.32,0.0,d); gl_FragColor = vec4(vColor + core*0.5, a); }`, vertexColors: true, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, }); const points = new THREE.Points(geom, mat); root.add(points); // Edges — very faint, just enough to suggest connectivity const edgePos = []; const edgeCol = []; const base = new THREE.Color(0x42425a); const EDGES = 1800; for (let i = 0; i < EDGES; i++) { const a = Math.floor(Math.random() * N); let b = -1, bestD = 9999; for (let k = 0; k < 6; k++) { const bb = Math.floor(Math.random() * N); if (bb === a) continue; const dx = cache[a*3]-cache[bb*3]; const dy = cache[a*3+1]-cache[bb*3+1]; const dz = cache[a*3+2]-cache[bb*3+2]; const d = dx*dx+dy*dy+dz*dz; if (d < bestD && d > 0.2) { bestD = d; b = bb; } } if (b < 0 || bestD > 5) continue; edgePos.push(cache[a*3],cache[a*3+1],cache[a*3+2],cache[b*3],cache[b*3+1],cache[b*3+2]); edgeCol.push(base.r,base.g,base.b,base.r,base.g,base.b); } const egeom = new THREE.BufferGeometry(); egeom.setAttribute('position', new THREE.Float32BufferAttribute(edgePos, 3)); egeom.setAttribute('color', new THREE.Float32BufferAttribute(edgeCol, 3)); const emat = new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.14, blending: THREE.AdditiveBlending, depthWrite: false, }); const lines = new THREE.LineSegments(egeom, emat); root.add(lines); // mouse drag rotate (subtle) let dragging = false, lastX = 0, lastY = 0; let velY = 0, velX = 0; function down(e){ dragging = true; lastX = e.clientX; lastY = e.clientY; } function up(){ dragging = false; } function move(e){ if(!dragging) return; velY += (e.clientX - lastX) * 0.005; velX += (e.clientY - lastY) * 0.005; lastX = e.clientX; lastY = e.clientY; } mount.addEventListener('mousedown', down); window.addEventListener('mouseup', up); window.addEventListener('mousemove', move); let raf; const start = performance.now(); function tick() { const t = (performance.now() - start) / 1000; // ambient drift + drag inertia velY *= 0.94; velX *= 0.94; root.rotation.y += 0.0005 + velY; root.rotation.x += velX; root.rotation.x = Math.max(-1, Math.min(1, root.rotation.x)); root.rotation.z = Math.sin(t * 0.05) * 0.03; renderer.render(scene, camera); raf = requestAnimationFrame(tick); } tick(); const ro = new ResizeObserver(() => { camera.aspect = w() / h(); camera.updateProjectionMatrix(); renderer.setSize(w(), h()); }); ro.observe(mount); return () => { cancelAnimationFrame(raf); ro.disconnect(); mount.removeEventListener('mousedown', down); window.removeEventListener('mouseup', up); window.removeEventListener('mousemove', move); renderer.dispose(); geom.dispose(); egeom.dispose(); mat.dispose(); emat.dispose(); if (mount.contains(renderer.domElement)) mount.removeChild(renderer.domElement); }; }, []); return
; };