// Small 3D graph preview for a knowledge-graph card. Auto-rotates, with a // `seed` so each card has a unique-looking topology, and a `palette` so a // 'coming soon' card can render as desaturated. window.CardScene = function CardScene({ seed = 1, palette = 'mixed', spin = 1 }) { const mountRef = React.useRef(null); React.useEffect(() => { const THREE = window.THREE; const mount = mountRef.current; if (!THREE || !mount) return; function rng(s) { return function () { s = (s * 9301 + 49297) % 233280; return s / 233280; }; } const rand = rng(seed * 7919 + 1); const w = () => mount.clientWidth; const h = () => mount.clientHeight; const scene = new THREE.Scene(); scene.fog = new THREE.FogExp2(0x07070a, 0.05); const camera = new THREE.PerspectiveCamera(50, w() / h(), 0.1, 100); camera.position.set(0, 0, 16); 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 palettes = { mixed: ['#7c7cff', '#ff6d6d', '#ffd66d', '#ff9a5b', '#ff6dc0', '#5ed694'], bio: ['#5ed694', '#7c7cff', '#ffd66d', '#a5f0c9'], legal: ['#a78bfa', '#ff6d6d', '#7c7cff', '#ffd66d'], muted: ['#3a3a4a', '#42424f', '#4a4a5a'], }[palette] || palettes?.mixed || ['#7c7cff']; const colors3 = palettes.map((c) => new THREE.Color(c)); const N = 280; const pos = new Float32Array(N * 3); const col = new Float32Array(N * 3); const sz = new Float32Array(N); const cache = []; for (let i = 0; i < N; i++) { const isCore = rand() < 0.6; const r = isCore ? Math.pow(rand(), 0.6) * 3.5 : 3.5 + Math.pow(rand(), 1.3) * 4.5; const t = rand() * Math.PI * 2; const p = Math.acos(2 * rand() - 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.push([x, y, z]); const c = colors3[Math.floor(rand() * colors3.length)]; col[i * 3] = c.r; col[i * 3 + 1] = c.g; col[i * 3 + 2] = c.b; sz[i] = isCore ? 0.07 + rand() * 0.05 : 0.04 + rand() * 0.03; } 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 * (260.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.18,d); if(a < 0.02) discard; float core = smoothstep(0.35,0.0,d); gl_FragColor = vec4(vColor + core*0.55, a); }`, vertexColors: true, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, }); const points = new THREE.Points(geom, mat); root.add(points); // edges const edgePos = []; const edgeCol = []; const base = new THREE.Color(palette === 'muted' ? 0x2a2a36 : 0x4a4a66); for (let i = 0; i < 200; i++) { const a = Math.floor(rand() * N); let b = -1, bestD = 999; for (let k = 0; k < 6; k++) { const bb = Math.floor(rand() * N); if (bb === a) continue; const dx = cache[a][0] - cache[bb][0]; const dy = cache[a][1] - cache[bb][1]; const dz = cache[a][2] - cache[bb][2]; const d = dx * dx + dy * dy + dz * dz; if (d < bestD && d > 0.3) { bestD = d; b = bb; } } if (b < 0 || bestD > 6) continue; edgePos.push(...cache[a], ...cache[b]); 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.28, blending: THREE.AdditiveBlending, depthWrite: false, }); const lines = new THREE.LineSegments(egeom, emat); root.add(lines); let raf; const start = performance.now() - rand() * 8000; function tick() { const t = (performance.now() - start) / 1000; root.rotation.y = t * 0.18 * spin; root.rotation.x = Math.sin(t * 0.15) * 0.25; 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(); renderer.dispose(); geom.dispose(); egeom.dispose(); mat.dispose(); emat.dispose(); if (mount.contains(renderer.domElement)) mount.removeChild(renderer.domElement); }; }, [seed, palette, spin]); return
; };