1// ââ Research: fanned theme deck â modal ââââââââââââââââââââââââââââââââââââââ 2 3// Pastel VIBGYOR tints, echoing the instance-diagram blobs. 4const researchThemes = [ 5 { 6 blurb: "Tumours at every scale.", 7 id: "segmentation", 8 interactive: "niivue", 9 intro: 10 "The core of my PhD is segmenting neuro-oncology images at every granularity. Drag, scroll and switch modality on a real BraTS multi-metastasis case below.", 11 orb3d: "head", 12 papers: [ 13 { 14 title: "SegPatch", 15 url: "https://github.com/aymuos15/SegPatch", 16 venue: "ISBI 2024", 17 }, 18 ], 19 tint: "#b8a0d8", 20 title: "Segmentation", 21 }, 22 { 23 blurb: "When averaging hides the small lesions.", 24 id: "instance-metrics", 25 interactive: "instance", 26 intro: 27 "Overlap metrics favour big structures, so tiny lesions barely dent the score. I build instance-aware losses that weight every lesion equally. Toggle to see the effect.", 28 orb3d: "sphere", 29 papers: [ 30 { 31 title: "Cluster Dice", 32 url: "https://github.com/aymuos15/Cluster-Dice", 33 venue: "SPIE Medical Imaging 2025", 34 }, 35 { 36 title: 37 "Instance Awareness of Multi-class Semantic Segmentation Loss Functions", 38 url: "https://github.com/BrainImageAnalysis/instance-loss", 39 venue: "CVPR CV4Clinical Workshop 2026", 40 }, 41 ], 42 tint: "#8b9bd4", 43 title: "Instance Metrics", 44 }, 45]; 46 47// ââ Instance Imbalance Interactive Diagram (mounts into a given container) ââââ 48function buildInstanceDiagram(container) { 49 if (!container) { 50 return; 51 } 52 53 const blobs = [ 54 { r: 34, x: 14, y: 40 }, 55 { r: 24, x: 38, y: 28 }, 56 { r: 18, x: 62, y: 58 }, 57 { r: 13, x: 28, y: 72 }, 58 { r: 9, x: 76, y: 32 }, 59 { r: 6, x: 50, y: 16 }, 60 { r: 5, x: 82, y: 68 }, 61 { r: 4, x: 90, y: 18 }, 62 ]; 63 64 const totalArea = blobs.reduce((s, b) => s + b.r * b.r, 0); 65 const N = blobs.length; 66 for (const b of blobs) { 67 b.diceW = (b.r * b.r) / totalArea; 68 b.instW = 1 / N; 69 } 70 71 // Pastel VIBGYOR instance colors 72 const vibgyor = [ 73 "#b8a0d8", 74 "#8b9bd4", 75 "#7cbcd4", 76 "#8cc8a0", 77 "#d4cc80", 78 "#d8a878", 79 "#d48888", 80 "#d4a0b8", 81 ]; 82 83 let mode = "dice"; 84 85 // ââ Label type tabs ââ 86 const labels = document.createElement("div"); 87 labels.className = "diagram-labels"; 88 89 const lblSem = document.createElement("button"); 90 lblSem.className = "label-tab active"; 91 lblSem.textContent = "Semantic Labels"; 92 lblSem.addEventListener("click", () => setMode("dice")); 93 94 const lblInst = document.createElement("button"); 95 lblInst.className = "label-tab"; 96 lblInst.textContent = "Instance Labels"; 97 lblInst.addEventListener("click", () => setMode("instance")); 98 99 labels.appendChild(lblSem); 100 labels.appendChild(lblInst); 101 102 // ââ Scatter view ââ 103 const scatter = document.createElement("div"); 104 scatter.className = "diagram-scatter"; 105 for (const [i, b] of blobs.entries()) { 106 const el = document.createElement("div"); 107 el.className = "diagram-blob"; 108 el.style.left = `${b.x}%`; 109 el.style.top = `${b.y}%`; 110 el.style.width = `${b.r * 2}px`; 111 el.style.height = `${b.r * 2}px`; 112 113 const ring = document.createElement("div"); 114 ring.className = "blob-ring"; 115 el.appendChild(ring); 116 117 b.el = el; 118 b.ring = ring; 119 scatter.appendChild(el); 120 121 el.addEventListener("mouseenter", () => hi(i)); 122 el.addEventListener("mouseleave", lo); 123 } 124 125 // ââ Controls row: toggle + scores ââ 126 const controls = document.createElement("div"); 127 controls.className = "diagram-controls"; 128 129 const toggle = document.createElement("div"); 130 toggle.className = "diagram-toggle"; 131 132 const btnD = document.createElement("button"); 133 btnD.className = "diagram-btn active"; 134 btnD.textContent = "Dice Metric"; 135 btnD.addEventListener("click", () => setMode("dice")); 136 137 const btnI = document.createElement("button"); 138 btnI.className = "diagram-btn"; 139 btnI.textContent = "Instance-Aware Dice Metric"; 140 btnI.addEventListener("click", () => setMode("instance")); 141 142 toggle.appendChild(btnD); 143 toggle.appendChild(btnI); 144 145 // ââ Score cards ââ 146 const scores = document.createElement("div"); 147 scores.className = "diagram-scores"; 148 149 function makeScoreCard(label) { 150 const card = document.createElement("div"); 151 card.className = "score-card"; 152 const lbl = document.createElement("span"); 153 lbl.className = "score-label"; 154 lbl.textContent = label; 155 const val = document.createElement("span"); 156 val.className = "score-value"; 157 card.appendChild(lbl); 158 card.appendChild(val); 159 return { card, val }; 160 } 161 162 const dscCard = makeScoreCard("DSC"); 163 const pqCard = makeScoreCard("PQ"); 164 scores.appendChild(dscCard.card); 165 scores.appendChild(pqCard.card); 166 167 controls.appendChild(toggle); 168 controls.appendChild(scores); 169 170 const scoreTargets = { 171 dice: { dsc: 0.95, pq: 0.38 }, 172 instance: { dsc: 0.88, pq: 0.76 }, 173 }; 174 let currentDsc = 0, 175 currentPq = 0; 176 177 // ââ Vertical contribution bars ââ 178 const barsWrap = document.createElement("div"); 179 barsWrap.className = "diagram-bars"; 180 for (const [i, b] of blobs.entries()) { 181 const col = document.createElement("div"); 182 col.className = "diagram-bar-col"; 183 184 const pct = document.createElement("span"); 185 pct.className = "bar-pct"; 186 187 const track = document.createElement("div"); 188 track.className = "bar-track-v"; 189 190 const fill = document.createElement("div"); 191 fill.className = "bar-fill-v"; 192 track.appendChild(fill); 193 194 const dot = document.createElement("div"); 195 dot.className = "bar-dot"; 196 const dotSize = Math.max(4, Math.round(b.r * 0.45)); 197 dot.style.width = `${dotSize}px`; 198 dot.style.height = `${dotSize}px`; 199 200 col.appendChild(pct); 201 col.appendChild(track); 202 col.appendChild(dot); 203 204 b.fill = fill; 205 b.pct = pct; 206 b.col = col; 207 barsWrap.appendChild(col); 208 209 col.addEventListener("mouseenter", () => hi(i)); 210 col.addEventListener("mouseleave", lo); 211 } 212 213 // ââ Caption ââ 214 const caption = document.createElement("p"); 215 caption.className = "diagram-caption"; 216 217 const top = document.createElement("div"); 218 top.className = "diagram-top"; 219 top.appendChild(scatter); 220 top.appendChild(barsWrap); 221 222 container.appendChild(labels); 223 container.appendChild(top); 224 container.appendChild(controls); 225 container.appendChild(caption); 226 227 // ââ Updates ââ 228 function setMode(m) { 229 if (m === mode) { 230 return; 231 } 232 mode = m; 233 btnD.classList.toggle("active", mode === "dice"); 234 btnI.classList.toggle("active", mode === "instance"); 235 btnI.classList.toggle("rainbow", mode === "instance"); 236 lblSem.classList.toggle("active", mode === "dice"); 237 lblInst.classList.toggle("active", mode === "instance"); 238 lblInst.classList.toggle("rainbow", mode === "instance"); 239 update(); 240 } 241 242 function animateScore(el, from, to, flagLow) { 243 const duration = 500; 244 const start = performance.now(); 245 function tick(now) { 246 const p = Math.min(1, (now - start) / duration); 247 const ease = 1 - (1 - p) ** 3; 248 const v = from + (to - from) * ease; 249 el.textContent = v.toFixed(2); 250 if (p < 1) { 251 requestAnimationFrame(tick); 252 } 253 } 254 requestAnimationFrame(tick); 255 el.classList.toggle("low", flagLow && to < 0.5); 256 el.classList.toggle("high", flagLow && to >= 0.5); 257 } 258
259 function update() { 260 const maxW = Math.max( 261 ...blobs.map((b) => (mode === "dice" ? b.diceW : b.instW)) 262 ); 263 for (const [i, b] of blobs.entries()) { 264 const w = mode === "dice" ? b.diceW : b.instW; 265 const n = w / maxW; 266 267 b.el.style.opacity = 0.12 + 0.88 * n; 268 b.ring.style.transform = `scale(${1 + 0.5 * n})`; 269 b.ring.style.opacity = 0.08 + 0.72 * n; 270 b.fill.style.height = `${n * 100}%`; 271 b.pct.textContent = `${(w * 100).toFixed(1)}%`; 272 273 if (mode === "instance") { 274 b.el.style.setProperty("--blob-color", vibgyor[i]); 275 b.col.style.setProperty("--blob-color", vibgyor[i]); 276 } else { 277 b.el.style.removeProperty("--blob-color"); 278 b.col.style.removeProperty("--blob-color"); 279 } 280 } 281 282 const t = scoreTargets[mode]; 283 animateScore(dscCard.val, currentDsc, t.dsc, false); 284 animateScore(pqCard.val, currentPq, t.pq, true); 285 currentDsc = t.dsc; 286 currentPq = t.pq; 287 288 caption.textContent = 289 mode === "dice" 290 ? "With the standard Dice Score, the largest instance owns 48.5% of the metric. The 3 smallest share just 3.2%. Missing them barely changes the score." 291 : "Instance-aware: every instance contributes 12.5% to the score, regardless of size. Small lesions are weighted equally."; 292 } 293 294 // ââ Hover cross-highlighting ââ 295 function hi(idx) { 296 container.classList.add("has-highlight"); 297 for (const [i, b] of blobs.entries()) { 298 const on = i === idx; 299 b.el.classList.toggle("highlighted", on); 300 b.col.classList.toggle("highlighted", on); 301 } 302 } 303 304 function lo() { 305 container.classList.remove("has-highlight"); 306 for (const b of blobs) { 307 b.el.classList.remove("highlighted"); 308 b.col.classList.remove("highlighted"); 309 } 310 } 311 312 update(); 313} 314 315// ââ Deck + modal âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 316(() => { 317 const deck = document.getElementById("research-deck"); 318 const modal = document.getElementById("research-modal"); 319 const modalBody = document.getElementById("research-modal-body"); 320 const interactivesHolder = document.getElementById("research-interactives"); 321 if (!(deck && modal && modalBody)) { 322 return; 323 } 324 325 // ââ Build the fanned deck ââ 326 const N = researchThemes.length; 327 const center = (N - 1) / 2; 328 for (const [i, theme] of researchThemes.entries()) { 329 const offset = i - center; 330 const rot = offset * 5; 331 const ty = Math.abs(offset) * 11; 332 const z = 10 - Math.abs(offset); 333 334 const card = document.createElement("button"); 335 card.className = "research-card"; 336 card.type = "button"; 337 card.dataset.id = theme.id; 338 card.style.setProperty("--tint", theme.tint); 339 card.style.setProperty("--rot", `${rot}deg`); 340 card.style.setProperty("--ty", `${ty}px`); 341 card.style.setProperty("--z", z); 342 card.setAttribute("aria-label", `${theme.title} â ${theme.blurb}`); 343 344 let orbHtml = '<span class="research-card-orb"></span>'; 345 if (theme.orb3d === "head") { 346 orbHtml = 347 '<span class="research-card-orb research-card-orb--scan"><canvas class="research-card-orb-scan" width="96" height="96" aria-hidden="true"></canvas></span>'; 348 } else if (theme.orb3d === "sphere") { 349 orbHtml = 350 '<span class="research-card-orb research-card-orb--scan"><canvas class="research-card-orb-sphere" width="112" height="112" aria-hidden="true"></canvas></span>'; 351 } 352 353 card.innerHTML = ` 354 ${orbHtml} 355 <span class="research-card-label">${theme.title}</span> 356 <span class="research-card-blurb">${theme.blurb}</span>`; 357 358 card.addEventListener("click", () => openModal(theme.id)); 359 deck.appendChild(card); 360 } 361 362 // ââ Modal ââ 363 let lastFocused = null; 364 let openInteractive = null; // track a reparented live node to return on close 365 366 function buildModalBody(theme) { 367 modalBody.innerHTML = ""; 368 modalBody.style.setProperty("--tint", theme.tint); 369 370 const title = document.createElement("h3"); 371 title.className = "research-modal-title"; 372 title.id = "research-modal-title"; 373 title.textContent = theme.title; 374 modalBody.appendChild(title); 375 376 const intro = document.createElement("p"); 377 intro.className = "research-modal-intro"; 378 intro.textContent = theme.intro; 379 modalBody.appendChild(intro); 380 381 if (theme.interactive === "instance") { 382 const mount = document.createElement("div"); 383 mount.className = "instance-diagram"; 384 modalBody.appendChild(mount); 385 buildInstanceDiagram(mount); 386 } else if (theme.interactive === "niivue") { 387 const mount = document.createElement("div"); 388 mount.className = "research-niivue-mount"; 389 modalBody.appendChild(mount); 390 const viewer = document.getElementById("niivue-viewer"); 391 if (viewer) { 392 mount.appendChild(viewer); 393 openInteractive = viewer; 394 if (window.NiiViewer) { 395 window.NiiViewer.activate(); 396 // Nudge NiiVue to size itself to the now-visible modal. 397 requestAnimationFrame(() => 398 window.dispatchEvent(new Event("resize")) 399 ); 400 setTimeout(() => window.dispatchEvent(new Event("resize")), 350); 401 } 402 } 403 } 404 405 if (theme.papers?.length) { 406 const heading = document.createElement("h4"); 407 heading.className = "research-modal-heading"; 408 heading.textContent = theme.papers.length > 1 ? "Papers" : "Paper"; 409 modalBody.appendChild(heading); 410 411 const list = document.createElement("ul"); 412 list.className = "research-papers"; 413 for (const p of theme.papers) { 414 const li = document.createElement("li"); 415 li.className = "research-paper"; 416 li.innerHTML = ` 417 <a class="research-paper-title" href="${p.url}" target="_blank" rel="noopener">${p.title}</a>
418 <span class="research-paper-venue">${p.venue}</span>`; 419 list.appendChild(li); 420 } 421 modalBody.appendChild(list); 422 } 423 424 if (typeof colorizeLinks === "function") { 425 colorizeLinks(); 426 } 427 } 428 429 function returnInteractive() { 430 if (openInteractive && interactivesHolder) { 431 if (window.NiiViewer) { 432 window.NiiViewer.deactivate(); 433 } 434 interactivesHolder.appendChild(openInteractive); 435 openInteractive = null; 436 } 437 } 438 439 function openModal(id) { 440 const theme = researchThemes.find((t) => t.id === id); 441 if (!theme) { 442 return; 443 } 444 445 lastFocused = document.activeElement; 446 buildModalBody(theme); 447 448 modal.hidden = false; 449 requestAnimationFrame(() => modal.classList.add("open")); 450 document.body.classList.add("research-modal-open"); 451 452 const closeBtn = modal.querySelector(".research-modal-close"); 453 if (closeBtn) { 454 closeBtn.focus(); 455 } 456 } 457 458 function closeModal() { 459 if (modal.hidden) { 460 return; 461 } 462 returnInteractive(); 463 modal.classList.remove("open"); 464 document.body.classList.remove("research-modal-open"); 465 466 const finish = () => { 467 modal.hidden = true; 468 modalBody.innerHTML = ""; 469 modal.removeEventListener("transitionend", finish); 470 }; 471 modal.addEventListener("transitionend", finish); 472 // Fallback if transitionend doesn't fire (e.g. reduced motion). 473 setTimeout(finish, 400); 474 475 if (lastFocused?.focus) { 476 lastFocused.focus(); 477 } 478 } 479 for (const el of modal.querySelectorAll("[data-close]")) { 480 el.addEventListener("click", closeModal); 481 } 482 483 document.addEventListener("keydown", (e) => { 484 if (modal.hidden) { 485 return; 486 } 487 if (e.key === "Escape") { 488 closeModal(); 489 return; 490 } 491 if (e.key === "Tab") { 492 const focusable = modal.querySelectorAll( 493 'button, a[href], select, [tabindex]:not([tabindex="-1"])' 494 ); 495 if (!focusable.length) { 496 return; 497 } 498 const [first] = focusable; 499 const last = focusable.at(-1); 500 if (e.shiftKey && document.activeElement === first) { 501 e.preventDefault(); 502 last.focus(); 503 } else if (!e.shiftKey && document.activeElement === last) { 504 e.preventDefault(); 505 first.focus(); 506 } 507 } 508 }); 509})();
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.