1import * as THREE from "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js"; 2 3/* Publications brain explorer 4 - Full-screen three.js brain (same procedural mesh) 5 - 38 paper nodes on surface; mouse controls spin speed/direction 6 - Hover a node â card with title/venue/year 7 - Click â panel slides in from right with full details + search links 8 - Hover over node = brain slows; move away = resumes spin 9*/ 10 11const CAT = { 12 neuro: { color: "#55d6be", label: "Neurocritical Care AI" }, 13 ml: { color: "#e6b85c", label: "Foundation Models & Methods" }, 14 mind: { color: "#9edc76", label: "Music, Consciousness & Brain States" }, 15 imaging: { color: "#c9a6ff", label: "Biological Imaging & Signal Analysis" }, 16}; 17 18const PAPERS = [ 19 { y:2026, t:"ICP-WAVES: Intracranial Pressure Waveform Analysis and Visualization for Enhanced Signal Processing", v:"IEEE TBME", c:"neuro", auth:"Megjhani M, Li Y, Grassi G, et al." }, 20 { y:2025, t:"Application of a time series foundation model to noninvasively estimate intracranial pressure", v:"J Clin Monit Comput", c:"ml", auth:"Megjhani M, Weinerman B, Alalqum T, et al." }, 21 { y:2025, t:"Physiological Signals Boosted by Artificial Intelligence Save Time and Brain Function", v:"NEJM AI", c:"neuro", auth:"Park S, Li Y, Megjhani M" }, 22 { y:2025, t:"Let the Experts Speak: Improving Survival Prediction via Mixture-of-Experts Heads (CAMOE)", v:"NeurIPS", c:"ml", auth:"Morrill T, Puli A, Megjhani M, Park S, Zemel R" }, 23 { y:2025, t:"Mathematical models for hospital dynamics via information theory", v:"npj Digital Medicine", c:"ml", auth:"Megjhani M et al." }, 24 { y:2025, t:"Pressure reactivity and cerebral oximetry in aneurysmal subarachnoid hemorrhage", v:"Neurocritical Care", c:"neuro", auth:"Megjhani M et al." }, 25 { y:2025, t:"Bronchiolitis low-risk respiratory deterioration cohort study", v:"BMJ Open", c:"neuro", auth:"Megjhani M et al." }, 26 { y:2025, t:"Pulse-rate variability predicts length of stay in bronchiolitis PICU admissions", v:"J Clin Monit Comput", c:"neuro", auth:"Megjhani M et al." }, 27 { y:2025, t:"Pediatric VA-ECMO early risk factors for mortality", v:"Perfusion", c:"neuro", auth:"Megjhani M et al." }, 28 { y:2024, t:"Non-invasive pulse arrival time and cardiac index estimation", v:"Physiol Meas", c:"neuro", auth:"Megjhani M et al." }, 29 { y:2023, t:"Suboptimal cerebral perfusion pressure and ischemia after intracerebral hemorrhage", v:"Neurocritical Care", c:"neuro", auth:"Megjhani M et al." }, 30 { y:2023, t:"Heart rate and HRV as prognostic biomarkers after cardiac arrest", v:"Resuscitation Plus", c:"neuro", auth:"Megjhani M et al." }, 31 { y:2023, t:"Automatic ICP waveform morphology identification via wavelet analysis", v:"Physiol Meas", c:"neuro", auth:"Megjhani M et al." }, 32 { y:2023, t:"A Deep Learning Framework for Deriving Noninvasive Intracranial Pressure Waveforms from TCD", v:"Annals of Neurology", c:"neuro", auth:"Megjhani M, Terilli K, Weinerman B, et al." }, 33 { y:2023, t:"Oxygen reactivity and disturbed cerebral perfusion after subarachnoid hemorrhage", v:"Critical Care", c:"neuro", auth:"Megjhani M et al." }, 34 { y:2023, t:"Generalizable DCI detection across centers via federated learning", v:"IEEE BIBM", c:"ml", auth:"Megjhani M et al." }, 35 { y:2023, t:"Optimal cerebral perfusion pressure and brain tissue oxygen in aneurysmal SAH", v:"Stroke", c:"neuro", auth:"Megjhani M, Weiss M, Ford J, et al." }, 36 { y:2023, t:"Level of consciousness classification in a neurological intensive care unit", v:"Neurocritical Care", c:"mind", auth:"Megjhani M et al." }, 37 { y:2022, t:"Vector Angle Analysis of Multimodal Neuromonitoring for Continuous DCI Prediction", v:"Neurocritical Care", c:"neuro", auth:"Megjhani M, Weiss M, Kwon SB, et al." }, 38 { y:2022, t:"Optimal cerebral perfusion pressure during delayed cerebral ischemia after aneurysmal SAH", v:"Critical Care Medicine",c:"neuro", auth:"Megjhani M et al." }, 39 { y:2022, t:"Real-time machine learning deployment for delayed cerebral ischemia detection", v:"IEEE HI-POCT", c:"ml", auth:"Megjhani M et al." }, 40 { y:2021, t:"Dynamic Detection of Delayed Cerebral Ischemia: A Study in Three Centers", v:"Stroke", c:"neuro", auth:"Megjhani M, Terilli K, Weiss M, et al." }, 41 { y:2021, t:"Dynamic ICP waveform morphology predicts ventriculostomy-related infection", v:"Neurocritical Care", c:"neuro", auth:"Megjhani M et al." },
42 { y:2021, t:"Endotypes of hospitalized COVID-19 patients", v:"Frontiers in Medicine",c:"ml", auth:"Megjhani M et al." }, 43 { y:2020, t:"Machine learning to predict delayed cerebral ischemia and outcomes in subarachnoid hemorrhage", v:"Neurology", c:"ml", auth:"Megjhani M et al." }, 44 { y:2020, t:"Harnessing big data for neurocritical care precision medicine", v:"Curr Treat Options", c:"ml", auth:"Megjhani M et al." }, 45 { y:2019, t:"Detection of brain activation in unresponsive patients with acute brain injury", v:"NEJM", c:"mind", auth:"Megjhani M et al." }, 46 { y:2019, t:"HRV as a biomarker of neurocardiogenic injury after subarachnoid hemorrhage", v:"Neurocritical Care", c:"neuro", auth:"Megjhani M et al." }, 47 { y:2019, t:"Deep brain lesions and impaired consciousness after traumatic brain injury", v:"Scientific Reports", c:"mind", auth:"Megjhani M et al." }, 48 { y:2019, t:"Clustering ICP waveform morphology in patients with ventriculitis", v:"Physiol Meas", c:"neuro", auth:"Megjhani M et al." }, 49 { y:2018, t:"Active-learning framework for ICP waveform morphology identification", v:"Physiol Meas", c:"ml", auth:"Megjhani M et al." }, 50 { y:2018, t:"Dictionary learning improves delayed cerebral ischemia prediction", v:"Frontiers in Neurology",c:"ml", auth:"Megjhani M et al." }, 51 { y:2017, t:"Mobile EEG brain dynamics during art museum experience", v:"Front Hum Neurosci", c:"mind", auth:"Megjhani M et al." }, 52 { y:2017, t:"Morphological constraint spectral unmixing of biological tissues", v:"Bioinformatics", c:"imaging", auth:"Megjhani M, Roysam B" }, 53 { y:2015, t:"Your Brain on Art: Emergent Cortical Dynamics During Aesthetic Experiences", v:"Front Hum Neurosci", c:"mind", auth:"Megjhani M*, Kontson K*, Cruz-Garza JG, et al." }, 54 { y:2015, t:"Population-scale 3D reconstruction of microglial arbors from confocal stacks", v:"Bioinformatics", c:"imaging", auth:"Megjhani M, Rey-Villamizar N, et al." }, 55 { y:2015, t:"Unsupervised population profiling of microglial morphologies", v:"IEEE J-STSP", c:"imaging", auth:"Megjhani M et al." }, 56 { y:2014, t:"Automated image analysis of tissue around chronically implanted neural devices", v:"Front Neuroinform", c:"imaging", auth:"Megjhani M et al." }, 57]; 58 59// ---- value noise helpers (same as brain-scene.js) ----------------------- 60function hash3(x, y, z) { 61 const n = Math.sin(x * 127.1 + y * 311.7 + z * 74.7) * 43758.5453; 62 return n - Math.floor(n); 63} 64function smooth(t) { return t * t * (3 - 2 * t); } 65function vnoise(x, y, z) { 66 const xi = Math.floor(x), yi = Math.floor(y), zi = Math.floor(z); 67 const xf = x - xi, yf = y - yi, zf = z - zi; 68 const u = smooth(xf), v = smooth(yf), w = smooth(zf); 69 const lerp = (a, b, t) => a + (b - a) * t; 70 return lerp( 71 lerp(lerp(hash3(xi,yi,zi), hash3(xi+1,yi,zi), u), lerp(hash3(xi,yi+1,zi), hash3(xi+1,yi+1,zi), u), v), 72 lerp(lerp(hash3(xi,yi,zi+1), hash3(xi+1,yi,zi+1), u), lerp(hash3(xi,yi+1,zi+1), hash3(xi+1,yi+1,zi+1), u), v), 73 w 74 ); 75} 76function fbm(x, y, z) { 77 let f = 0, amp = 0.5, freq = 1; 78 for (let i = 0; i < 5; i++) { f += amp * vnoise(x*freq, y*freq, z*freq); freq *= 2.1; amp *= 0.48; } 79 return f; 80} 81 82const _v = new THREE.Vector3(); 83const _v2 = new THREE.Vector3(); 84 85// ridged, anisotropic noise â cortex-like gyral ridges (shared with brain-scene.js) 86function brainFold(dir) { 87 const wx = fbm(dir.x*1.4+1.2, dir.y*1.4+4.7, dir.z*1.4+8.3); 88 const wy = fbm(dir.x*1.4+9.2, dir.y*1.4+2.3, dir.z*1.4+1.7); 89 const F = 4.6; 90 const n1 = fbm(dir.x*F*1.1+wx*2.6, dir.y*F*1.9+wy*2.6, dir.z*F*0.7+wx*1.8); 91 let r1 = 1 - Math.abs(2 * n1 - 1); 92 r1 = r1 * r1 * (3 - 2 * r1); 93 const n2 = fbm(dir.x*F*2.4+5.1, dir.y*F*3.3+2.7, dir.z*F*1.6+9.4); 94 const r2 = 1 - Math.abs(2 * n2 - 1); 95 return r1 * 0.72 + r2 * 0.28; 96} 97function brainPoint(dir, out, scale) { 98 const r = 1 + 0.46 * (brainFold(dir) - 0.5); 99 out.copy(dir).multiplyScalar(r * scale); 100 out.x *= 0.96; out.z *= 1.26; out.y *= 0.94;
101 const z = out.z / scale; 102 const frontNarrow = 1 - 0.12 * smooth(Math.max(0, Math.min(1, (z - 0.25) / 0.85))); 103 const backNarrow = 1 - 0.18 * smooth(Math.max(0, Math.min(1, (-z - 0.70) / 0.60))); 104 out.x *= frontNarrow * backNarrow; 105 const fr = smooth(Math.max(0, Math.min(1, (z - 0.10) / 0.95))); 106 if (out.y > 0) out.y *= 1 + fr * 0.08; 107 // Sylvian fissure + temporal lobe 108 const sideAbs = Math.abs(out.x) / scale; 109 const sylLine = (-0.02 + z * 0.16) * scale; 110 const sylDist = (out.y - sylLine) / scale; 111 const syl = Math.exp(-(sylDist*sylDist) / 0.014) * smooth(Math.max(0, Math.min(1, (sideAbs - 0.28) / 0.35))); 112 out.addScaledVector(dir, -0.30 * scale * syl); 113 const tl = Math.max(0, (sylLine - out.y)/scale - 0.02) * smooth(Math.max(0, Math.min(1, (sideAbs - 0.24) / 0.4))); 114 out.y -= tl * 0.42 * scale; 115 out.x += Math.sign(out.x) * tl * 0.26 * scale; 116 // longitudinal fissure 117 const mid = Math.exp(-(out.x*out.x) / (0.0085*scale*scale)); 118 const top = smooth(Math.max(0, Math.min(1, (out.y/scale + 0.02) / 0.5))); 119 out.addScaledVector(dir, -0.70 * scale * mid * top); 120 // flat base 121 if (out.y < -0.30*scale) out.y = -0.30*scale + (out.y + 0.30*scale) * 0.40; 122 // occipital taper 123 if (z < -0.55) { 124 const t = (-z - 0.55) / 0.55; 125 out.x *= 1 - t * 0.34; 126 out.y -= t * t * 0.10 * scale; 127 } 128 // cerebellum 129 const zb = out.z/scale + 0.70, yb = out.y/scale + 0.30; 130 const cb = Math.exp(-(zb*zb)/0.055) * Math.exp(-(yb*yb)/0.045); 131 out.y -= cb * 0.14 * scale; 132 out.z -= cb * 0.08 * scale; 133 // brainstem nub 134 const zs = out.z/scale + 0.30, xs = out.x/scale; 135 const bs = Math.exp(-(zs*zs)/0.03) * Math.exp(-(xs*xs)/0.02) * Math.max(0, -out.y/scale - 0.34); 136 out.y -= bs * 0.5 * scale; 137 return out; 138} 139 140// ---- DOM refs ------------------------------------------------------- 141const canvas = document.getElementById("pub-canvas"); 142const card = document.getElementById("pub-card"); 143const cardTitle = document.getElementById("pc-title"); 144const cardMeta = document.getElementById("pc-meta"); 145const cardDot = document.getElementById("pc-dot"); 146const panel = document.getElementById("pub-panel"); 147const panelBody = document.getElementById("pub-panel-body"); 148const closeBtn = document.getElementById("pub-close"); 149const backdrop = document.getElementById("pub-backdrop"); 150const hint = document.getElementById("pub-hint"); 151 152if (!canvas) throw new Error("pub-canvas not found"); 153 154// ---- Three.js scene ------------------------------------------------- 155const scene = new THREE.Scene(); 156const camera = new THREE.PerspectiveCamera(55, 1, 0.1, 50); 157camera.position.set(0, 0.1, 5.0); 158 159const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true }); 160renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); 161 162scene.add(new THREE.AmbientLight(0xbfeee6, 0.6)); 163const key = new THREE.DirectionalLight(0xffffff, 0.7); 164key.position.set(2, 3, 4); 165scene.add(key); 166 167const world = new THREE.Group(); 168scene.add(world); 169 170// ---- build brain ---------------------------------------------------- 171const BSCALE = 1.55; 172const ico = new THREE.IcosahedronGeometry(1, 4); 173const srcPos = ico.getAttribute("position"); 174const faces = srcPos.count / 3; 175const dir = new THREE.Vector3(); 176const A = new THREE.Vector3(), B = new THREE.Vector3(), C = new THREE.Vector3(); 177 178const posArr = [], colArr = []; 179const sulcusC = new THREE.Color("#020a08"); 180const gyrusC = new THREE.Color("#dffdf7"); 181 182for (let k = 0; k < 18000; k++) { 183 const f = (Math.random() * faces) | 0; 184 A.set(srcPos.getX(f*3), srcPos.getY(f*3), srcPos.getZ(f*3)); 185 B.set(srcPos.getX(f*3+1), srcPos.getY(f*3+1), srcPos.getZ(f*3+1)); 186 C.set(srcPos.getX(f*3+2), srcPos.getY(f*3+2), srcPos.getZ(f*3+2)); 187 let u = Math.random(), v = Math.random(); 188 if (u + v > 1) { u = 1 - u; v = 1 - v; } 189 dir.copy(A).multiplyScalar(1-u-v).addScaledVector(B, u).addScaledVector(C, v).normalize(); 190 brainPoint(dir, _v, BSCALE); 191 const groove = Math.exp(-(_v.x*_v.x) / (0.010*BSCALE*BSCALE)) * (_v.y > -0.02*BSCALE ? 1 : 0); 192 if (groove > 0.38) continue; 193 posArr.push(_v.x, _v.y, _v.z);
194 const fv = brainFold(dir); 195 const bc = sulcusC.clone().lerp(gyrusC, Math.pow(fv, 0.65)); 196 colArr.push(bc.r, bc.g, bc.b); 197} 198 199const brainGeo = new THREE.BufferGeometry(); 200brainGeo.setAttribute("position", new THREE.BufferAttribute(Float32Array.from(posArr), 3)); 201brainGeo.setAttribute("color", new THREE.BufferAttribute(Float32Array.from(colArr), 3)); 202 203const brainMat = new THREE.ShaderMaterial({ 204 transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, 205 uniforms: { 206 uSize: { value: 0.028 }, 207 uScale: { value: 1 }, 208 uFade: { value: 1 }, 209 }, 210 vertexShader: ` 211 attribute vec3 color; 212 uniform float uSize; uniform float uScale; uniform float uFade; 213 varying vec3 vColor; varying float vAlpha; 214 void main() { 215 vColor = color; 216 vec4 mv = modelViewMatrix * vec4(position, 1.0); 217 float depth = clamp((-mv.z - 1.8) / 4.8, 0.0, 1.0); 218 vAlpha = (0.80 - depth * 0.48) * uFade; 219 gl_PointSize = uSize * uScale * (1.3 - depth * 0.38) / -mv.z; 220 gl_Position = projectionMatrix * mv; 221 } 222 `, 223 fragmentShader: ` 224 varying vec3 vColor; varying float vAlpha; 225 void main() { 226 vec2 c = gl_PointCoord - 0.5; 227 if (length(c) > 0.5) discard; 228 float a = smoothstep(0.5, 0.06, length(c)); 229 gl_FragColor = vec4(vColor, a * vAlpha); 230 } 231 `, 232}); 233world.add(new THREE.Points(brainGeo, brainMat)); 234 235// faint body 236const bodyGeo = new THREE.IcosahedronGeometry(1, 4); 237const bp = bodyGeo.getAttribute("position"); 238for (let i = 0; i < bp.count; i++) { 239 dir.set(bp.getX(i), bp.getY(i), bp.getZ(i)).normalize(); 240 brainPoint(dir, _v, BSCALE); 241 bp.setXYZ(i, _v.x, _v.y, _v.z); 242} 243bp.needsUpdate = true; 244bodyGeo.computeVertexNormals(); 245world.add(new THREE.Mesh(bodyGeo, new THREE.MeshStandardMaterial({ 246 color: 0x2f9e93, transparent: true, opacity: 0.06, 247 roughness: 0.8, metalness: 0, depthWrite: false, 248}))); 249 250// ---- paper nodes on surface ----------------------------------------- 251function glowTex() { 252 const cv = document.createElement("canvas"); 253 cv.width = cv.height = 64; 254 const ctx = cv.getContext("2d"); 255 const g = ctx.createRadialGradient(32,32,0,32,32,32); 256 g.addColorStop(0, "rgba(255,255,255,1)"); 257 g.addColorStop(0.35,"rgba(255,255,255,0.65)"); 258 g.addColorStop(1, "rgba(255,255,255,0)"); 259 ctx.fillStyle = g; ctx.fillRect(0,0,64,64); 260 return new THREE.CanvasTexture(cv); 261} 262const glowTexture = glowTex(); 263const nodeGroup = new THREE.Group(); 264world.add(nodeGroup); 265const nodeObjs = [], hitObjs = []; 266const golden = Math.PI * (3 - Math.sqrt(5)); 267 268for (let i = 0; i < PAPERS.length; i++) { 269 const y = 1 - (i / (PAPERS.length - 1)) * 2; 270 const rad = Math.sqrt(1 - y * y); 271 dir.set(Math.cos(golden * i) * rad, y, Math.sin(golden * i) * rad).normalize(); 272 brainPoint(dir, _v, BSCALE); 273 _v.addScaledVector(dir, 0.07); // lift off surface 274 275 const color = new THREE.Color(CAT[PAPERS[i].c].color); 276 const group = new THREE.Group(); 277 group.position.copy(_v); 278 279 // visible sphere 280 const core = new THREE.Mesh( 281 new THREE.SphereGeometry(0.022, 12, 12), 282 new THREE.MeshBasicMaterial({ color }) 283 ); 284 // glow sprite 285 const glow = new THREE.Sprite(new THREE.SpriteMaterial({ 286 map: glowTexture, color, transparent: true, opacity: 0.5, 287 blending: THREE.AdditiveBlending, depthWrite: false, 288 })); 289 glow.scale.setScalar(0.16); 290 // invisible hit sphere (larger for comfort) 291 const hit = new THREE.Mesh( 292 new THREE.SphereGeometry(0.08, 8, 8), 293 new THREE.MeshBasicMaterial({ transparent: true, opacity: 0, depthWrite: false }) 294 ); 295 hit.userData.index = i; 296 hitObjs.push(hit); 297 298 group.add(core, glow, hit); 299 group.userData = { hover: 0, base: color }; 300 nodeGroup.add(group); 301 nodeObjs.push(group); 302} 303 304// ---- resize --------------------------------------------------------- 305function resize() { 306 const w = window.innerWidth, h = window.innerHeight; 307 renderer.setSize(w, h, false); 308 camera.aspect = w / h; 309 camera.updateProjectionMatrix(); 310 brainMat.uniforms.uScale.value = renderer.domElement.height * 0.5; 311 312 // fit-to-width: on narrow/portrait screens shrink the brain so its longest 313 // axis (~2.0 world units half-extent, swings into view as it rotates) fits 314 const halfH = Math.tan(THREE.MathUtils.degToRad(camera.fov / 2)) * camera.position.z; 315 const halfW = halfH * camera.aspect; 316 world.scale.setScalar(Math.min(1, (halfW * 0.88) / 2.0)); 317} 318window.addEventListener("resize", resize); 319resize(); 320 321// ---- interaction ---------------------------------------------------- 322const mouse = new THREE.Vector2(0, 0); 323const pointer = new THREE.Vector2(0, 0); 324let rect = canvas.getBoundingClientRect(); 325let hovIdx = -1; 326let panelOpen = false;
327 328function updatePointer(e) { 329 mouse.x = (e.clientX / window.innerWidth) * 2 - 1; 330 mouse.y = -((e.clientY / window.innerHeight) * 2 - 1); 331 pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; 332 pointer.y = -(((e.clientY - rect.top) / rect.height) * 2 - 1); 333} 334window.addEventListener("pointermove", updatePointer); 335window.addEventListener("scroll", () => { rect = canvas.getBoundingClientRect(); }, { passive: true }); 336 337// drag (mouse or finger) rotates the brain directly, with inertia 338let dragging = false, dragMoved = 0, lastDragX = 0, dragVel = 0; 339canvas.addEventListener("pointerdown", (e) => { 340 updatePointer(e); // touch never fires pointermove before tap 341 dragging = true; 342 dragMoved = 0; 343 lastDragX = e.clientX; 344 canvas.setPointerCapture(e.pointerId); 345}); 346canvas.addEventListener("pointermove", (e) => { 347 if (!dragging || panelOpen) return; 348 const dx = e.clientX - lastDragX; 349 lastDragX = e.clientX; 350 dragMoved += Math.abs(dx); 351 dragVel = dx * 0.012; // ~half a screen drag = full revolution 352 autoAngle += dragVel; 353}); 354window.addEventListener("pointerup", () => { dragging = false; }); 355 356canvas.addEventListener("click", () => { 357 if (dragMoved > 8) return; // it was a drag, not a tap 358 if (hovIdx >= 0) openPanel(hovIdx); 359}); 360 361if (closeBtn) closeBtn.addEventListener("click", closePanel); 362if (backdrop) backdrop.addEventListener("click", closePanel); 363 364document.addEventListener("keydown", (e) => { 365 if (e.key === "Escape") closePanel(); 366}); 367 368function openPanel(idx) { 369 const p = PAPERS[idx]; 370 const cat = CAT[p.c]; 371 const qTitle = encodeURIComponent(p.t); 372 panelBody.innerHTML = ` 373 <span class="pp-cat" style="--cc:${cat.color}">${cat.label}</span> 374 <h2 class="pp-title">${p.t}</h2> 375 <p class="pp-auth">${p.auth}</p> 376 <div class="pp-meta"> 377 <span class="pp-venue">${p.v}</span> 378 <span class="pp-year">${p.y}</span> 379 </div> 380 <div class="pp-actions"> 381 <a class="pp-btn pp-btn--primary" href="https://scholar.google.com/scholar?q=${qTitle}" target="_blank" rel="noreferrer"> 382 Open on Google Scholar 383 </a> 384 <a class="pp-btn" href="https://pubmed.ncbi.nlm.nih.gov/?term=${qTitle}" target="_blank" rel="noreferrer"> 385 Search PubMed 386 </a> 387 </div> 388 `; 389 panel.classList.add("is-open"); 390 if (backdrop) backdrop.classList.add("is-open"); 391 panelOpen = true; 392 if (hint) hint.style.opacity = "0"; 393} 394 395function closePanel() { 396 panel.classList.remove("is-open"); 397 if (backdrop) backdrop.classList.remove("is-open"); 398 panelOpen = false; 399 if (hint) hint.style.opacity = ""; 400} 401 402// dev helper: ?open=N opens paper N's panel (also handy for deep links) 403const openParam = new URLSearchParams(window.location.search).get("open"); 404if (openParam !== null) { 405 const idx = Math.max(0, Math.min(PAPERS.length - 1, parseInt(openParam, 10) || 0)); 406 openPanel(idx); 407} 408 409// ---- animate -------------------------------------------------------- 410const raycaster = new THREE.Raycaster(); 411const tmp = new THREE.Vector3(); 412let autoAngle = 0; 413let smoothTiltX = 0; 414 415function animate() { 416 // constant slow auto-spin (mouse-X steering removed â it fought the drag 417 // and blocked full revolutions); hover slows; drag overrides with inertia 418 const hoverSlow = hovIdx >= 0 ? 0.05 : 1.0; 419 const spinSpeed = 0.0016 * hoverSlow; 420 if (!dragging) { 421 autoAngle += spinSpeed + dragVel; // dragVel = inertia after release 422 dragVel *= 0.94; 423 } 424 smoothTiltX += (-0.18 - mouse.y * 0.20 - smoothTiltX) * 0.04; 425 426 if (!panelOpen) { 427 world.rotation.y = autoAngle; 428 world.rotation.x = smoothTiltX; 429 } 430 431 // raycasting 432 raycaster.setFromCamera(pointer, camera); 433 const hits = raycaster.intersectObjects(hitObjs, false); 434 const newHov = hits.length ? hits[0].object.userData.index : -1; 435 436 // update node scales 437 nodeObjs.forEach((node, i) => { 438 const target = (i === newHov) ? 1.9 : 1.0; 439 node.userData.hover += (target - node.userData.hover) * 0.14; 440 node.scale.setScalar(node.userData.hover); 441 node.children[1].material.opacity = (0.45 + (node.userData.hover - 1) * 0.4); 442 }); 443 444 // update hover card 445 if (newHov !== hovIdx) { 446 hovIdx = newHov; 447 if (hovIdx >= 0) { 448 const p = PAPERS[hovIdx]; 449 cardTitle.textContent = p.t; 450 cardMeta.textContent = `${p.v} · ${p.y}`; 451 cardDot.style.setProperty("--dc", CAT[p.c].color); 452 cardDot.style.boxShadow = `0 0 10px ${CAT[p.c].color}`; 453 card.classList.add("is-visible"); 454 canvas.style.cursor = "pointer"; 455 } else { 456 card.classList.remove("is-visible"); 457 canvas.style.cursor = ""; 458 } 459 } 460 461 // reposition hover card every frame while visible 462 if (hovIdx >= 0 && !panelOpen) { 463 tmp.setFromMatrixPosition(nodeObjs[hovIdx].matrixWorld); 464 tmp.project(camera); 465 const sx = rect.left + (tmp.x * 0.5 + 0.5) * rect.width; 466 const sy = rect.top + (-tmp.y * 0.5 + 0.5) * rect.height; 467 card.style.transform = `translate(-50%, -120%) translate(${sx}px, ${sy}px)`; 468 } 469 470 renderer.render(scene, camera); 471 requestAnimationFrame(animate); 472} 473animate();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.